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 d4b9174..e71414f 100644 --- a/packages/node/meshnet_node/startup.py +++ b/packages/node/meshnet_node/startup.py @@ -193,22 +193,33 @@ def _configure_torch_threads( return active -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 ( @@ -218,6 +229,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( @@ -869,6 +897,7 @@ def run_startup( hf_repo: str | None = assignment.get("hf_repo") peers: list[dict] = assignment.get("peers", []) model_sources: list[dict] = [] if tracker_source_disabled else assignment.get("model_sources", []) + assignment_bytes_per_layer = _assignment_bytes_per_layer(assignment, quantization) print(f" Shard: layers {shard_start}-{shard_end} of {assigned_model}", flush=True) # 4. Download shard @@ -955,7 +984,7 @@ def run_startup( f"meshnet-node ready\n" f" Wallet: {address}\n" f" Shard: layers {shard_start}-{shard_end} ({assigned_model})\n" - f" {_shard_budget_line(memory_budget_mb, memory_budget_source, assignment.get('model_layers_end', shard_end) + 1, quantization)}\n" + f" {_shard_budget_line(memory_budget_mb, memory_budget_source, assignment.get('model_layers_end', shard_end) + 1, quantization, bytes_per_layer=assignment_bytes_per_layer)}\n" f" Endpoint: {endpoint}\n" f" Node ID: {node_id}\n" f" Hardware: {hw_str}\n" 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…

Node throughput

loading…

Console output

loading…
+

Inference history

loading...