diff --git a/.claude/memory/project-status.md b/.claude/memory/project-status.md
index 65f2246..446f722 100644
--- a/.claude/memory/project-status.md
+++ b/.claude/memory/project-status.md
@@ -42,3 +42,4 @@ Historical handoff note: `/mnt/c/Users/popov/Downloads/neuron-tai-alpha-handoff-
- Verification: downloader/startup targeted subset passes (`pytest tests/test_node_startup.py -k "download_shard or same_shard"`). Full `tests/test_node_startup.py` has 46 passed and 4 unrelated Windows chmod/path separator failures.
- Live Windows confirmation: `meshnet-node start --tracker http://192.168.0.179:8080 --model Qwen3.6-35B-A3B` reuses `F:\_STORAGE\models\qwen3.6-35b-a3b`, prints `Cached at`, registers, and reaches ready as node `5gMLrmyB-26b1f8a4204a`.
- Follow-up fix: preset-model startup now starts the heartbeat thread after registration; without this, the node appeared briefly on the dashboard and was purged on first inference/route after heartbeat expiry. Tracker dashboard now has a "Console output" panel backed by `/v1/console` for node register/expiry, routing failures, and proxy events.
+- Qwen3.6-35B-A3B reserve-based split is expected: an 79 GB CPU node may be assigned layers 0-36, and a second node fills 37-39. Do not "fix" this by bypassing the 20% assignment reserve unless the shard-planning policy changes.
diff --git a/packages/node/meshnet_node/startup.py b/packages/node/meshnet_node/startup.py
index af570b4..72d98a4 100644
--- a/packages/node/meshnet_node/startup.py
+++ b/packages/node/meshnet_node/startup.py
@@ -139,22 +139,33 @@ def _hardware_label(device: str, gpu_name: str | None = None) -> str:
return "CPU"
-def _max_assignable_layers(memory_mb: int, total_layers: int | None) -> int:
+def _max_assignable_layers(
+ memory_mb: int,
+ total_layers: int | None,
+ bytes_per_layer: int | None = None,
+) -> int:
if total_layers is None or total_layers <= 0 or memory_mb <= 0:
return 0
budget_bytes = memory_mb * 1024 * 1024
- return min(total_layers, int((budget_bytes * 0.8) // _DEFAULT_BYTES_PER_LAYER))
+ layer_bytes = bytes_per_layer or _DEFAULT_BYTES_PER_LAYER
+ return min(total_layers, int((budget_bytes * 0.8) // layer_bytes))
-def _shard_budget_line(memory_mb: int, memory_source: str, total_layers: int | None, quantization: str) -> str:
+def _shard_budget_line(
+ memory_mb: int,
+ memory_source: str,
+ total_layers: int | None,
+ quantization: str,
+ bytes_per_layer: int | None = None,
+) -> str:
memory_gb = memory_mb / 1024
gb_str = f"{memory_gb:.1f} GB"
budget_quantization = "bfloat16" if quantization == "auto" else quantization
if total_layers is None or total_layers <= 0:
return f"Memory budget: {gb_str} {memory_source}; shard budget: unknown model layer count"
- max_layers = _max_assignable_layers(memory_mb, total_layers)
+ max_layers = _max_assignable_layers(memory_mb, total_layers, bytes_per_layer=bytes_per_layer)
# Remaining capacity after one full model load (rough estimate)
- shard_bytes = max_layers * _DEFAULT_BYTES_PER_LAYER
+ shard_bytes = max_layers * (bytes_per_layer or _DEFAULT_BYTES_PER_LAYER)
remaining_gb = (memory_mb * 1024 * 1024 - shard_bytes) / (1024 ** 3)
remaining_str = f"; {remaining_gb:.1f} GB remaining after full load" if remaining_gb > 1 else ""
return (
@@ -164,6 +175,23 @@ def _shard_budget_line(memory_mb: int, memory_source: str, total_layers: int | N
)
+def _assignment_bytes_per_layer(assignment: dict, quantization: str) -> int | None:
+ bytes_per_layer = assignment.get("bytes_per_layer")
+ if isinstance(bytes_per_layer, int) and bytes_per_layer > 0:
+ return bytes_per_layer
+ if not isinstance(bytes_per_layer, dict):
+ return None
+ keys = [quantization, "bfloat16", "bf16", "int8", "nf4"]
+ for key in keys:
+ value = bytes_per_layer.get(key)
+ if isinstance(value, int) and value > 0:
+ return value
+ for value in bytes_per_layer.values():
+ if isinstance(value, int) and value > 0:
+ return value
+ return None
+
+
def _post_json(url: str, payload: dict, timeout: float = 10.0) -> dict:
data = json.dumps(payload).encode()
req = urllib.request.Request(
diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html
index 39c88de..f9793d7 100644
--- a/packages/tracker/meshnet_tracker/dashboard.html
+++ b/packages/tracker/meshnet_tracker/dashboard.html
@@ -75,6 +75,7 @@
Model usage (RPM)
loading…
+ Inference history
loading...