From 29351d621742a9d8e90626c11773e8c1721d5961 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 14:05:37 +0200 Subject: [PATCH 01/15] chore: ignore local model cache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9298b32..3c6cbc2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ dist/ !.env.testnet .rocm-local/* .pytest-tmp/* +.cache/ # Local tracker/node sqlite databases (never commit runtime state) *.sqlite From e2f3ae32b86102c5d26528eb7fd2b8aafcae07c0 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 15:16:23 +0200 Subject: [PATCH 02/15] feat: let admins manage model placement --- packages/node/meshnet_node/torch_server.py | 22 ++++++ .../tracker/meshnet_tracker/dashboard.html | 34 ++++++++- packages/tracker/meshnet_tracker/server.py | 69 ++++++++++++++++++- tests/test_dashboard.py | 11 +++ tests/test_dynamic_routing.py | 33 +++++++++ 5 files changed, 166 insertions(+), 3 deletions(-) diff --git a/packages/node/meshnet_node/torch_server.py b/packages/node/meshnet_node/torch_server.py index e58cf6d..7bbabac 100644 --- a/packages/node/meshnet_node/torch_server.py +++ b/packages/node/meshnet_node/torch_server.py @@ -1545,6 +1545,26 @@ class TorchNodeServer: def apply_tracker_directives(self, directives: list[dict]) -> dict | None: """Apply tracker shard directives (LOAD_SHARD replace, ADD_SHARD load-more).""" + drop_directive = next( + (directive for directive in reversed(directives) if directive.get("action") == "DROP_SHARD"), + None, + ) + if drop_directive is not None: + model_id = str(drop_directive.get("model") or "") + removed = self._backends.pop(model_id, None) + if removed is None: + return None + if self._backends: + self._backend = next(iter(self._backends.values())) + self._tracker_mode = self._backend.shard_start == 0 + else: + self._backend = None + self._tracker_mode = False + if self._server is not None: + self._server.backends = dict(self._backends) + self._server.backend = self._backend + self._server.tracker_mode = self._tracker_mode + return {"action": "DROP_SHARD", "model": model_id} add_directive = next( (directive for directive in reversed(directives) if directive.get("action") == "ADD_SHARD"), None, @@ -1574,6 +1594,8 @@ class TorchNodeServer: flush=True, ) try: + if replacing: + self._backends.clear() new_backend = _load_backend(model_id, shard_start, shard_end, quantization, self._cache_dir) except TypeError: new_backend = _load_backend(model_id, shard_start, shard_end, quantization) diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index 03fd932..b3b0c79 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -288,6 +288,7 @@

Node pending payouts

admin login required

Settlement history

admin login required

Tracker hive

loading…
+

Model placement

admin login required

All accounts (admin)

Strikes / bans / forfeitures

admin login required

Client balances

admin login required
@@ -1791,6 +1792,34 @@ async function requestSelectedModelLoad() { $("chat-status").textContent = `load queued on ${short(assignment.node_id || "node")} for layers ${assignment.shard_start}-${assignment.shard_end}`; } +async function requestAdminModelLoad(model) { + const result = await apiCall("/v1/models/load", "POST", { model, force: true }); + if (!result.ok) return alert(result.data.error || "model load request failed"); + await refreshActiveTab(true); +} + +async function releaseAdminModel(model) { + if (!confirm(`Release ${model} from every serving node?`)) return; + const result = await apiCall("/v1/models/release", "POST", { model }); + if (!result.ok) return alert(result.data.error || "model release request failed"); + await refreshActiveTab(true); +} + +function renderAdminModelPlacement(models, map) { + const nodes = (map && map.nodes) || []; + const rows = ((models && models.data) || []).map(model => { + const aliases = new Set([model.id, model.hf_repo, ...(model.aliases || [])].filter(Boolean)); + const serving = nodes.filter(node => aliases.has(node.model) || aliases.has(node.hf_repo)).length; + const downloaded = nodes.filter(node => (node.downloaded_models || []).some(item => aliases.has(item.model) || aliases.has(item.hf_repo))).length; + const actions = ` ` + + ``; + return [esc(model.name || model.id), String(serving), String(downloaded), actions]; + }); + $("admin-model-placement").innerHTML = rows.length + ? table(["model", "serving nodes", "downloaded on nodes", "admin action"], rows) + : '
no model presets configured
'; +} + function chatAuthToken() { if (accountApiKeys.length) return accountApiKeys[0]; return null; @@ -2427,14 +2456,17 @@ async function fetchAdminTab() { fetchJson("/v1/console"), fetchJson("/v1/billing/summary"), fetchJson("/v1/registry/wallets"), + fetchJson("/v1/models"), + fetchJson("/v1/network/map"), ]; if (isAdmin) fetches.push(apiCall("/v1/admin/accounts")); const results = await Promise.all(fetches); - const [raft, consoleData, summary, wallets, adminResp] = results; + const [raft, consoleData, summary, wallets, models, map, adminResp] = results; renderIfChanged("hive", raft, renderHive); renderIfChanged("console", consoleData, renderConsole); renderIfChanged("billing-summary", summary, data => renderBilling(data)); renderIfChanged("fraud", { wallets, summary }, data => renderFraud(data.wallets, data.summary)); + renderIfChanged("admin-model-placement", { models, map }, data => renderAdminModelPlacement(data.models, data.map)); if (adminResp && adminResp.ok) { renderIfChanged("admin", adminResp.data.accounts || [], accounts => { const rows = accounts.map(a => { diff --git a/packages/tracker/meshnet_tracker/server.py b/packages/tracker/meshnet_tracker/server.py index 0a428ef..32c7239 100644 --- a/packages/tracker/meshnet_tracker/server.py +++ b/packages/tracker/meshnet_tracker/server.py @@ -1528,6 +1528,49 @@ def _request_model_load_locked(server: "_TrackerHTTPServer", model_key: str) -> return None +def _force_model_load_locked(server: "_TrackerHTTPServer", model_key: str) -> dict | None: + """Replace the fastest ready assignment after an explicit admin eviction.""" + resolved_name, preset = _resolve_model_preset(server.model_presets, model_key) + if preset is None or not preset.get("hf_repo"): + return None + start, end = _preset_layer_bounds(preset) + candidates = [node for node in server.registry.values() + if node.status == "ready" and node.pending_new_assignment is None + and _has_usable_quantization(node)] + if not candidates: + return None + node = max(candidates, key=lambda item: item.benchmark_tokens_per_sec) + shard_end = min(end, start + min(_node_layer_capacity(node, preset), end - start + 1) - 1) + if shard_end < start: + return None + quantization = _node_quantization(node, preset) + directive = _load_directive(node, str(preset["hf_repo"]), start, shard_end, quantization) + replaced = node.hf_repo or node.model + node.model, node.hf_repo = resolved_name, str(preset["hf_repo"]) + node.shard_start, node.shard_end, node.quantization = start, shard_end, quantization + node.managed_assignment, node.pending_new_assignment = True, directive + node.pending_directives.append(directive) + _tracker_log(server, "warn", "model load forced", node_id=node.node_id, + model=resolved_name, replaced_model=replaced, shard=f"{start}-{shard_end}") + return {"node_id": node.node_id, "model": resolved_name, "hf_repo": preset["hf_repo"], + "shard_start": start, "shard_end": shard_end, "replaced_model": replaced} + + +def _release_model_locked(server: "_TrackerHTTPServer", model_key: str) -> int: + """Queue DROP_SHARD for every served shard and remove it from routing immediately.""" + resolved_name, preset = _resolve_model_preset(server.model_presets, model_key) + if preset is None: + return 0 + released = 0 + for node in server.registry.values(): + if not _node_matches_preset(node, resolved_name, preset) or node.shard_start is None or node.shard_end is None: + continue + node.pending_directives.append(_drop_directive(node, str(preset.get("hf_repo") or resolved_name), node.shard_start, node.shard_end, node.quantization or "bfloat16")) + node.status = "loading" + released += 1 + return released + + def _preferred_node_quantization( node: _NodeEntry, preset: dict, @@ -3043,6 +3086,9 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): if self.path == "/v1/models/load": self._handle_model_load_request() return + if self.path == "/v1/models/release": + self._handle_model_release_request() + return if self.path == "/v1/models/vote": self._handle_model_coverage_vote() return @@ -3170,8 +3216,6 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): seen_ids: set[str] = set() for name, preset in server.model_presets.items(): model_nodes = [node for node in alive if _node_matches_preset(node, name, preset)] - if not model_nodes and not preset.get("recommended"): - continue required_start, required_end = _preset_layer_bounds(preset) coverage = _coverage_percentage( model_nodes, @@ -4834,11 +4878,32 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): with server.lock: self._purge_expired_nodes() assignment = _request_model_load_locked(server, model) + if assignment is None and body.get("force") is True: + assignment = _force_model_load_locked(server, model) if assignment is None: self._send_json(409, {"error": "no ready joined node has an available model slot and sufficient capacity"}) return self._send_json(202, {"status": "queued", "assignment": assignment}) + def _handle_model_release_request(self): + server: _TrackerHTTPServer = self.server # type: ignore[assignment] + if not self._require_role("admin", "validator"): + return + body = self._read_json_body() + if body is None: + return + model = body.get("model") + if not isinstance(model, str) or not model.strip(): + self._send_json(400, {"error": "model is required"}) + return + with server.lock: + self._purge_expired_nodes() + released = _release_model_locked(server, model) + if not released: + self._send_json(404, {"error": "no served shards found for model"}) + return + self._send_json(202, {"status": "release_queued", "released": released}) + def _handle_model_coverage_vote(self): """Record a rolling wish-list signal for an unavailable precision.""" server: _TrackerHTTPServer = self.server # type: ignore[assignment] diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index dbe7283..0f2a545 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -100,6 +100,17 @@ def test_dashboard_allows_admin_to_request_selected_model_load(): assert '$("request-model-load").style.display = enabled ? "" : "none"' in html +def test_dashboard_exposes_admin_model_inventory_and_release_controls(): + "Admin placement controls show the full model inventory and can release capacity." + html = _dashboard_html() + + assert 'id="admin-model-placement"' in html + assert "renderAdminModelPlacement" in html + assert '"/v1/models/release"' in html + assert "requestAdminModelLoad" in html + assert "releaseAdminModel" in html + + def test_network_map_includes_node_friendly_name(): "Network map includes node friendly name\n\nTags: dashboard, http" tracker = TrackerServer() diff --git a/tests/test_dynamic_routing.py b/tests/test_dynamic_routing.py index 83a9d2f..85c7ad6 100644 --- a/tests/test_dynamic_routing.py +++ b/tests/test_dynamic_routing.py @@ -355,6 +355,39 @@ def test_admin_model_load_request_queues_directive_on_joined_node(): assert heartbeat["directives"][0]["model"] == "Qwen/Qwen2.5-0.5B-Instruct" +def test_admin_can_replace_a_served_model_and_release_it(): + "Forced admin placement replaces a served shard; release queues DROP_SHARD." + tracker = TrackerServer(enable_billing=False, validator_service_token="test-admin") + port = tracker.start() + try: + node = _post_json( + f"http://127.0.0.1:{port}/v1/nodes/register", + {"endpoint": "http://127.0.0.1:9912", "model": "stub-model", + "shard_start": 0, "shard_end": 3, "managed_assignment": True, + "max_loaded_shards": 1, "memory_mb": 1, + "hardware_profile": {"host_id": "full-host"}}, + ) + headers = {"Content-Type": "application/json", "Authorization": "Bearer test-admin"} + load = urllib.request.Request( + f"http://127.0.0.1:{port}/v1/models/load", + data=json.dumps({"model": "qwen2.5-0.5b-instruct", "force": True}).encode(), + headers=headers, method="POST") + with urllib.request.urlopen(load) as response: + assert json.loads(response.read())["assignment"]["node_id"] == node["node_id"] + heartbeat = _post_json(f"http://127.0.0.1:{port}/v1/nodes/{node['node_id']}/heartbeat", {}) + assert heartbeat["directives"][0]["action"] == "LOAD_SHARD" + release = urllib.request.Request( + f"http://127.0.0.1:{port}/v1/models/release", + data=json.dumps({"model": "qwen2.5-0.5b-instruct"}).encode(), headers=headers, method="POST") + with urllib.request.urlopen(release) as response: + assert json.loads(response.read())["released"] == 1 + heartbeat = _post_json(f"http://127.0.0.1:{port}/v1/nodes/{node['node_id']}/heartbeat", {}) + finally: + tracker.stop() + + assert heartbeat["directives"][0]["action"] == "DROP_SHARD" + + def test_endpoint_key_distinguishes_same_port_different_hosts(): "Endpoint key distinguishes same port different hosts\n\nTags: http, performance, routing, tracker" from meshnet_node.torch_server import _clamp_downstream_hops, _endpoint_key From 1e64a5b2b9cca2d2d77b4448455ee633eca57f0c Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 15:29:11 +0200 Subject: [PATCH 03/15] new dash update --- packages/tracker/meshnet_tracker/dashboard.html | 3 ++- tests/test_dashboard.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index b3b0c79..5c5a16e 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -22,8 +22,9 @@ border-bottom:1px solid var(--border); flex-shrink:0; } header h1 { font-size:16px; margin:0; color:var(--accent); } header .meta { color:var(--dim); font-size:12px; } - main { display:grid; grid-template-columns:repeat(auto-fit,minmax(340px,1fr)); + main { display:grid; grid-template-columns:1fr; gap:14px; padding:14px 20px; } + main > section { grid-column:1 / -1; width:100%; min-width:0; } body.chat-tab-active main { flex:1; min-height:0; display:flex; flex-direction:column; padding:0; gap:0; overflow:hidden; diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 0f2a545..bb3873c 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -39,7 +39,8 @@ def test_dashboard_served_with_all_panels(): assert "resolveModelGroup" in html assert "buildModelAliasMap" in html assert "modelAliasKey(raw)" in html - assert "main { display:grid; grid-template-columns:repeat(auto-fit,minmax(340px,1fr));" in html + assert "main { display:grid; grid-template-columns:1fr;" in html + assert "main > section { grid-column:1 / -1; width:100%; min-width:0; }" in html assert 'onclick="clearConsole()"' in html assert "let consoleClearedAt = 0;" in html finally: From 348b003d6ed16c9d539ab6acb3a265ef39e58635 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 15:55:24 +0200 Subject: [PATCH 04/15] fix: restore responsive dashboard panel grid --- packages/tracker/meshnet_tracker/dashboard.html | 8 +++++++- tests/test_dashboard.py | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index 5c5a16e..8b9b94e 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -24,7 +24,7 @@ header .meta { color:var(--dim); font-size:12px; } main { display:grid; grid-template-columns:1fr; gap:14px; padding:14px 20px; } - main > section { grid-column:1 / -1; width:100%; min-width:0; } + main > section { width:100%; min-width:0; } body.chat-tab-active main { flex:1; min-height:0; display:flex; flex-direction:column; padding:0; gap:0; overflow:hidden; @@ -72,6 +72,12 @@ background:transparent; color:var(--dim); padding:5px 0 8px; } .dashboard-tabs button.active { color:var(--accent); border-bottom-color:var(--accent); } .wide { grid-column:1 / -1; } + /* Compact status cards fan out on desktop; tables remain readable at half width. */ + @media (min-width:900px) { + main { grid-template-columns:repeat(4,minmax(0,1fr)); } + main > section { grid-column:span 1; } + .wide { grid-column:span 2; } + } section[hidden] { display:none !important; } section.chat-section { padding:0; border:0; border-radius:0; background:var(--bg); min-height:0; diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index bb3873c..c379ec8 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -39,8 +39,9 @@ def test_dashboard_served_with_all_panels(): assert "resolveModelGroup" in html assert "buildModelAliasMap" in html assert "modelAliasKey(raw)" in html - assert "main { display:grid; grid-template-columns:1fr;" in html - assert "main > section { grid-column:1 / -1; width:100%; min-width:0; }" in html + assert "@media (min-width:900px)" in html + assert "grid-template-columns:repeat(4,minmax(0,1fr));" in html + assert ".wide { grid-column:span 2; }" in html assert 'onclick="clearConsole()"' in html assert "let consoleClearedAt = 0;" in html finally: From 905ea16ce008d50d2b1b3a93b4508d4c2cef9f78 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 16:55:52 +0300 Subject: [PATCH 05/15] feat: complete route session baseline benchmark --- .claude/memory/project-status.md | 4 ++++ .../issues/01-baseline-profiling-harness.md | 15 ++++++------ .../prd.json | 4 ++-- .../meshnet_node/route_session_benchmark.py | 24 ++++++++++++++++--- tests/test_route_session_benchmark.py | 13 ++++++++-- 5 files changed, 45 insertions(+), 15 deletions(-) diff --git a/.claude/memory/project-status.md b/.claude/memory/project-status.md index 8c26a8b..3fb0fc7 100644 --- a/.claude/memory/project-status.md +++ b/.claude/memory/project-status.md @@ -8,6 +8,10 @@ metadata: # Project Status (2026-07-13) +## Distributed inference performance (2026-07-14) + +`DIP-001` is done in `.scratch/distributed-inference-performance/`: the deterministic two-node Route Session stub benchmark covers direct/relay plus cached/stateless prefill and decode. Its JSON and concise summary explicitly attribute model execution, activation encode/decode, compression, connection setup, relay queueing, local HTTP forwarding, and end-to-end seam latency. `PYTHONPATH=packages/node pytest -q tests/test_route_session_benchmark.py` passed (7); the fixture assertion checks output-token identity and connection attempts. + > Doc reconciliation 2026-07-13: `docs/prd.json` tracks US-001…US-050 (048 memory budget, 049 mainnet pilot, 050 Qwen demand placement). ADRs 0025–0026 added (TAI phase B/C, assignment ownership). All 35 user stories in docs/prd.json are done (35/35), including the reward-system arc US-030…US-035 completed 2026-07-02: diff --git a/.scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md b/.scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md index 0847134..79d1170 100644 --- a/.scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md +++ b/.scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md @@ -1,4 +1,4 @@ -Status: ready-for-agent +Status: done (2026-07-14) # 01 — Baseline and profiling harness @@ -12,16 +12,15 @@ sizes and connection counts without requiring a real model or external host. ## Acceptance criteria -- [ ] The harness runs a fixed prompt and fixed generated-token count through a +- [x] The harness runs a fixed prompt and fixed generated-token count through a two-node route in direct and relay modes. -- [ ] It reports p50/p95 per-token latency, per-hop latency, payload bytes, +- [x] It reports p50/p95 per-token latency, per-hop latency, payload bytes, compression ratio, connection attempts, and queue wait. -- [ ] It distinguishes prefill from decode and cached from stateless mode. -- [ ] It emits machine-readable JSON suitable for CI artifacts and a concise +- [x] It distinguishes prefill from decode and cached from stateless mode. +- [x] It emits machine-readable JSON suitable for CI artifacts and a concise human-readable summary. -- [ ] A test fixture can assert connection attempts and output token identity. +- [x] A test fixture can assert connection attempts and output token identity. ## Blocked by -None - can start immediately. - +None - completed. Verified with `PYTHONPATH=packages/node pytest -q tests/test_route_session_benchmark.py` (7 passed). diff --git a/.scratch/distributed-inference-performance/prd.json b/.scratch/distributed-inference-performance/prd.json index a9000e7..7184324 100644 --- a/.scratch/distributed-inference-performance/prd.json +++ b/.scratch/distributed-inference-performance/prd.json @@ -15,8 +15,8 @@ "Can assert connection count and output token identity" ], "priority": 1, - "passes": false, - "notes": "Source issue: .scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md", + "passes": true, + "notes": "Completed 2026-07-14. Deterministic direct/relay and cached/stateless stub benchmark with JSON/summary attribution; focused test suite passes (7). Source issue: .scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md", "dependsOn": [] }, { diff --git a/packages/node/meshnet_node/route_session_benchmark.py b/packages/node/meshnet_node/route_session_benchmark.py index f83eb5f..fdcfef4 100644 --- a/packages/node/meshnet_node/route_session_benchmark.py +++ b/packages/node/meshnet_node/route_session_benchmark.py @@ -44,6 +44,7 @@ class SeamSample: cache_mode: CacheMode model_ms: float encode_ms: float + activation_decode_ms: float framing_ms: float metadata_ms: float copy_allocation_ms: float @@ -52,6 +53,7 @@ class SeamSample: decompression_ms: float connection_setup_ms: float queue_wait_ms: float + local_http_forwarding_ms: float transport_ms: float seam_latency_ms: float payload_bytes: int @@ -120,6 +122,10 @@ def _summary(samples: list[SeamSample]) -> dict[str, float | int]: "compression_cpu_ms": round( sum(sample.compression_ms + sample.decompression_ms for sample in samples), 4 ), + "model_execution_ms": round(sum(sample.model_ms for sample in samples), 4), + "activation_encoding_ms": round(sum(sample.encode_ms for sample in samples), 4), + "activation_decoding_ms": round(sum(sample.activation_decode_ms for sample in samples), 4), + "local_http_forwarding_ms": round(sum(sample.local_http_forwarding_ms for sample in samples), 4), "peak_buffered_bytes": max((sample.copy_allocation_bytes for sample in samples), default=0), } @@ -159,6 +165,7 @@ class _StubTransport: queue_wait_ms = 0.0 if self.mode == "direct" else 0.18 + (0.05 if token_index is not None and token_index % 2 else 0.0) model_ms = 1.6 if phase == "prefill" else 0.45 encode_ms = 0.16 if phase == "prefill" else 0.06 + activation_decode_ms = 0.055 if phase == "prefill" else 0.02 # Keep framing/metadata/copy costs explicit rather than hiding them in # serialization or transport time. The stub owns one binary frame and # one response body per hop; no base64 body is modeled. @@ -168,20 +175,26 @@ class _StubTransport: copy_allocation_bytes = wire_bytes + payload_bytes compression_ms = 0.09 if self.scenario.compression else 0.0 decompression_ms = 0.07 if self.scenario.compression else 0.0 + # Both routes finish by forwarding the decoded activation to the local + # tail-node HTTP handler; relay adds its own queue before that hop. + local_http_forwarding_ms = 0.11 if self.mode == "direct" else 0.16 transport_ms = (0.32 if self.mode == "direct" else 0.61) + wire_bytes / 100_000 seam_latency_ms = round( - model_ms + encode_ms + framing_ms + metadata_ms + copy_allocation_ms - + compression_ms + decompression_ms + connection_setup_ms + queue_wait_ms + transport_ms, + model_ms + encode_ms + activation_decode_ms + framing_ms + metadata_ms + copy_allocation_ms + + compression_ms + decompression_ms + connection_setup_ms + queue_wait_ms + transport_ms + + local_http_forwarding_ms, 4, ) return SeamSample( phase=phase, token_index=token_index, session_id=self.session_id, activation_id=f"benchmark-activation-{self._activation_count}", seam="head->tail", mode=self.mode, cache_mode=self.cache_mode, model_ms=model_ms, encode_ms=encode_ms, + activation_decode_ms=activation_decode_ms, framing_ms=framing_ms, metadata_ms=metadata_ms, copy_allocation_ms=copy_allocation_ms, copy_allocation_bytes=copy_allocation_bytes, compression_ms=compression_ms, decompression_ms=decompression_ms, connection_setup_ms=connection_setup_ms, queue_wait_ms=queue_wait_ms, + local_http_forwarding_ms=local_http_forwarding_ms, transport_ms=round(transport_ms, 4), seam_latency_ms=seam_latency_ms, payload_bytes=payload_bytes, wire_bytes=wire_bytes, compression_ratio=round(payload_bytes / wire_bytes, 4), connection_attempted=connection_attempted, @@ -329,9 +342,10 @@ def run_real_model_lan_benchmark(url: str, *, model: str, timeout: float = 120.0 sample = SeamSample( phase="decode", token_index=0, session_id=session_id, activation_id="lan-activation-1", seam="head->tail", mode="direct", cache_mode="cached", model_ms=0.0, encode_ms=0.0, + activation_decode_ms=0.0, framing_ms=0.0, metadata_ms=0.0, copy_allocation_ms=0.0, copy_allocation_bytes=0, compression_ms=0.0, decompression_ms=0.0, connection_setup_ms=elapsed_ms, - queue_wait_ms=0.0, transport_ms=elapsed_ms, seam_latency_ms=elapsed_ms, + queue_wait_ms=0.0, local_http_forwarding_ms=0.0, transport_ms=elapsed_ms, seam_latency_ms=elapsed_ms, payload_bytes=len(body), wire_bytes=len(body) + len(response_body), compression_ratio=1.0, connection_attempted=True, ) @@ -354,6 +368,10 @@ def format_summary(report: dict) -> str: f"{decode['tokens_per_sec']:.1f} tok/s; {decode['bytes_per_token']:.0f} B/tok; " f"seam {seam['payload_bytes']}/{seam['wire_bytes']} B " f"({seam['compression_ratio']:.2f}x); connections {run['connections']['attempts']}; " + f"model/encode/decode {decode['model_execution_ms']:.2f}/" + f"{decode['activation_encoding_ms']:.2f}/{decode['activation_decoding_ms']:.2f} ms; " + f"compression {decode['compression_cpu_ms']:.2f} ms; " + f"HTTP {decode['local_http_forwarding_ms']:.2f} ms; " f"queue p95 {decode['p95_queue_wait_ms']:.2f} ms" ) return "\n".join(lines) diff --git a/tests/test_route_session_benchmark.py b/tests/test_route_session_benchmark.py index c1a9294..603bd50 100644 --- a/tests/test_route_session_benchmark.py +++ b/tests/test_route_session_benchmark.py @@ -32,12 +32,18 @@ def test_matrix_reports_direct_relay_prefill_decode_and_machine_readable_metrics assert {"p50_latency_ms", "p95_latency_ms", "payload_bytes", "compression_ratio", "connection_attempts", "p95_queue_wait_ms"} <= set(run["phases"]["decode"]) sample = run["samples"][0] + assert sample["model_ms"] > 0 + assert sample["encode_ms"] > 0 + assert sample["activation_decode_ms"] > 0 assert sample["framing_ms"] > 0 assert sample["metadata_ms"] > 0 assert sample["copy_allocation_ms"] > 0 assert sample["copy_allocation_bytes"] >= sample["payload_bytes"] + assert sample["local_http_forwarding_ms"] > 0 assert len(run["samples"]) == 1 + len(run["output_tokens"]) - assert {"tokens_per_sec", "bytes_per_token", "compression_cpu_ms", "peak_buffered_bytes"} <= set(run["phases"]["decode"]) + assert {"tokens_per_sec", "bytes_per_token", "compression_cpu_ms", "peak_buffered_bytes", + "model_execution_ms", "activation_encoding_ms", "activation_decoding_ms", + "local_http_forwarding_ms"} <= set(run["phases"]["decode"]) def test_cached_sessions_reuse_one_connection_and_preserve_stub_tokens(): @@ -74,7 +80,10 @@ def test_cli_writes_json_artifact_and_human_summary(tmp_path, capsys): report = json.loads(output.read_text()) assert report["schema_version"] == 1 assert "Route Session benchmark" in capsys.readouterr().out - assert "relay" in format_summary(report) + summary = format_summary(report) + assert "relay" in summary + assert "model/encode/decode" in summary + assert "HTTP" in summary def test_performance_gate_checks_comparison_identity_session_and_cleanup(): From 35af1e21de9b59842a09f21ecc75f0b9c238d3dc Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 16:00:37 +0200 Subject: [PATCH 06/15] fix: make model placement controls observable --- .../tracker/meshnet_tracker/dashboard.html | 30 ++++++++++++++----- tests/test_dashboard.py | 3 ++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index 8b9b94e..bbddc6c 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -295,7 +295,7 @@

Node pending payouts

admin login required

Settlement history

admin login required

Tracker hive

loading…
-

Model placement

admin login required
+

Model placement

Choose a model to load or release.
admin login required

All accounts (admin)

Strikes / bans / forfeitures

admin login required

Client balances

admin login required
@@ -1801,25 +1801,34 @@ async function requestSelectedModelLoad() { async function requestAdminModelLoad(model) { const result = await apiCall("/v1/models/load", "POST", { model, force: true }); - if (!result.ok) return alert(result.data.error || "model load request failed"); + if (!result.ok) return showAdminModelPlacementStatus(result.data.error || "model load request failed", true); + const assignment = result.data.assignment || {}; + showAdminModelPlacementStatus(`Load queued on ${short(assignment.node_id || "node")} for ${model}.`); await refreshActiveTab(true); } async function releaseAdminModel(model) { - if (!confirm(`Release ${model} from every serving node?`)) return; const result = await apiCall("/v1/models/release", "POST", { model }); - if (!result.ok) return alert(result.data.error || "model release request failed"); + if (!result.ok) return showAdminModelPlacementStatus(result.data.error || "model release request failed", true); + showAdminModelPlacementStatus(`Release queued for ${result.data.released || 0} node(s) serving ${model}.`); await refreshActiveTab(true); } +function showAdminModelPlacementStatus(message, isError) { + const status = $("admin-model-placement-status"); + status.textContent = message; + status.className = isError ? "bad" : "ok"; +} + function renderAdminModelPlacement(models, map) { const nodes = (map && map.nodes) || []; const rows = ((models && models.data) || []).map(model => { const aliases = new Set([model.id, model.hf_repo, ...(model.aliases || [])].filter(Boolean)); const serving = nodes.filter(node => aliases.has(node.model) || aliases.has(node.hf_repo)).length; - const downloaded = nodes.filter(node => (node.downloaded_models || []).some(item => aliases.has(item.model) || aliases.has(item.hf_repo))).length; - const actions = ` ` + - ``; + const downloaded = nodes.filter(node => aliases.has(node.model) || aliases.has(node.hf_repo) || + (node.downloaded_models || []).some(item => aliases.has(item.model) || aliases.has(item.hf_repo))).length; + const actions = ` ` + + ``; return [esc(model.name || model.id), String(serving), String(downloaded), actions]; }); $("admin-model-placement").innerHTML = rows.length @@ -1827,6 +1836,13 @@ function renderAdminModelPlacement(models, map) { : '
no model presets configured
'; } +$("admin-model-placement").addEventListener("click", event => { + const load = event.target.closest("[data-admin-model-load]"); + const release = event.target.closest("[data-admin-model-release]"); + if (load) void requestAdminModelLoad(load.dataset.adminModelLoad); + if (release) void releaseAdminModel(release.dataset.adminModelRelease); +}); + function chatAuthToken() { if (accountApiKeys.length) return accountApiKeys[0]; return null; diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index c379ec8..9492b12 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -111,6 +111,9 @@ def test_dashboard_exposes_admin_model_inventory_and_release_controls(): assert '"/v1/models/release"' in html assert "requestAdminModelLoad" in html assert "releaseAdminModel" in html + assert 'data-admin-model-load=' in html + assert 'data-admin-model-release=' in html + assert "admin-model-placement-status" in html def test_network_map_includes_node_friendly_name(): From 7b3399760eb6bcffd66eb60c514c17f6b69a6627 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 17:09:04 +0300 Subject: [PATCH 07/15] chore: wrap up completed story metadata --- .scratch/dashboard-test-runner/PRD.md | 6 ++ .scratch/dashboard-test-runner/prd.json | 7 +- .../prd.json | 67 ++++++++++++++----- .scratch/node-capability-admission/prd.json | 22 +++--- docs/issues/42-gguf-llamacpp-node-backend.md | 21 +++--- 5 files changed, 81 insertions(+), 42 deletions(-) diff --git a/.scratch/dashboard-test-runner/PRD.md b/.scratch/dashboard-test-runner/PRD.md index e7782a2..ef01546 100644 --- a/.scratch/dashboard-test-runner/PRD.md +++ b/.scratch/dashboard-test-runner/PRD.md @@ -12,4 +12,10 @@ Provide an opt-in, admin-only tracker Dashboard Testing tab that dynamically dis - One active run. - Real inference stays separately environment-gated and excluded from default suites. +## Operator workflow + +See [`docs/dev/dashboard-test-runner.md`](../../docs/dev/dashboard-test-runner.md) +for launch configuration, default safe suites vs the gated real-inference suite, +and required environment variables. + See `prd.json` for executable Ralph user stories and acceptance criteria. diff --git a/.scratch/dashboard-test-runner/prd.json b/.scratch/dashboard-test-runner/prd.json index 334e875..d57977e 100644 --- a/.scratch/dashboard-test-runner/prd.json +++ b/.scratch/dashboard-test-runner/prd.json @@ -51,15 +51,16 @@ "uv run pytest tests/test_dashboard.py tests/test_dynamic_routing.py -q passes." ], "priority": 3, - "passes": false, + "passes": true, "notes": "Do not reintroduce --enable-test-runner without implementing its CLI argument in US-001.", "dependsOn": [ "US-001", "US-002" - ] + ], + "completionNotes": "Completed by agent" } ], "metadata": { - "updatedAt": "2026-07-11T17:02:30.520Z" + "updatedAt": "2026-07-12T01:58:06.286Z" } } \ No newline at end of file diff --git a/.scratch/distributed-inference-performance/prd.json b/.scratch/distributed-inference-performance/prd.json index 7184324..9006c66 100644 --- a/.scratch/distributed-inference-performance/prd.json +++ b/.scratch/distributed-inference-performance/prd.json @@ -16,8 +16,9 @@ ], "priority": 1, "passes": true, - "notes": "Completed 2026-07-14. Deterministic direct/relay and cached/stateless stub benchmark with JSON/summary attribution; focused test suite passes (7). Source issue: .scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md", - "dependsOn": [] + "notes": "Source issue: .scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md", + "dependsOn": [], + "completionNotes": "Completed by agent" }, { "id": "DIP-002", @@ -31,9 +32,12 @@ "Tests cover binary, JSON, timeout, disconnect, cancellation, and cleanup" ], "priority": 2, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/distributed-inference-performance/issues/02-relay-session-compatibility.md", - "dependsOn": ["DIP-001"] + "dependsOn": [ + "DIP-001" + ], + "completionNotes": "Completed by agent" }, { "id": "DIP-003", @@ -47,9 +51,12 @@ "Benchmark shows healthy-session connection count independent of token count" ], "priority": 3, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/distributed-inference-performance/issues/03-http-keepalive.md", - "dependsOn": ["DIP-001"] + "dependsOn": [ + "DIP-001" + ], + "completionNotes": "Completed by agent" }, { "id": "DIP-004", @@ -63,9 +70,12 @@ "Tests verify cadence and cleanup" ], "priority": 4, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/distributed-inference-performance/issues/04-seam-telemetry.md", - "dependsOn": ["DIP-001"] + "dependsOn": [ + "DIP-001" + ], + "completionNotes": "Completed by agent" }, { "id": "DIP-005", @@ -79,9 +89,12 @@ "Tests cover compressible, incompressible, threshold, malformed, and legacy bodies" ], "priority": 5, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/distributed-inference-performance/issues/05-adaptive-compression.md", - "dependsOn": ["DIP-001"] + "dependsOn": [ + "DIP-001" + ], + "completionNotes": "Completed by agent" }, { "id": "DIP-006", @@ -95,9 +108,12 @@ "Wire and token-output regression tests pass" ], "priority": 6, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/distributed-inference-performance/issues/06-activation-framing-copies.md", - "dependsOn": ["DIP-001"] + "dependsOn": [ + "DIP-001" + ], + "completionNotes": "Completed by agent" }, { "id": "DIP-007", @@ -111,9 +127,13 @@ "Tests cover chunking, slow consumers, failure, and legacy peers" ], "priority": 7, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/distributed-inference-performance/issues/07-prefill-backpressure.md", - "dependsOn": ["DIP-001", "DIP-004"] + "dependsOn": [ + "DIP-001", + "DIP-004" + ], + "completionNotes": "Completed by agent" }, { "id": "DIP-008", @@ -127,9 +147,20 @@ "Gate verifies token identity, session stability, and resource cleanup" ], "priority": 8, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/distributed-inference-performance/issues/08-end-to-end-performance-gate.md", - "dependsOn": ["DIP-002", "DIP-003", "DIP-004", "DIP-005", "DIP-006", "DIP-007"] + "dependsOn": [ + "DIP-002", + "DIP-003", + "DIP-004", + "DIP-005", + "DIP-006", + "DIP-007" + ], + "completionNotes": "Completed by agent" } - ] -} + ], + "metadata": { + "updatedAt": "2026-07-12T02:35:28.752Z" + } +} \ No newline at end of file diff --git a/.scratch/node-capability-admission/prd.json b/.scratch/node-capability-admission/prd.json index dad87ae..1cfe28d 100644 --- a/.scratch/node-capability-admission/prd.json +++ b/.scratch/node-capability-admission/prd.json @@ -35,11 +35,12 @@ "Full pytest passes or an exact unrelated blocker is recorded" ], "priority": 2, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/node-capability-admission/issues/02-doctor-real-forward.md", "dependsOn": [ "NCA-001" - ] + ], + "completionNotes": "Completed by agent" }, { "id": "NCA-003", @@ -54,12 +55,13 @@ "Full pytest passes or an exact unrelated blocker is recorded" ], "priority": 3, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/node-capability-admission/issues/03-fail-closed-startup-admission.md", "dependsOn": [ "NCA-001", "NCA-002" - ] + ], + "completionNotes": "Completed by agent" }, { "id": "NCA-004", @@ -76,12 +78,13 @@ "Full pytest passes or an exact unrelated blocker is recorded" ], "priority": 4, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/node-capability-admission/issues/04-tracker-validated-capability-routing.md", "dependsOn": [ "NCA-001", "NCA-003" - ] + ], + "completionNotes": "Completed by agent" }, { "id": "NCA-005", @@ -96,15 +99,16 @@ "Full pytest passes or an exact unrelated blocker is recorded" ], "priority": 5, - "passes": false, + "passes": true, "notes": "Source issue: .scratch/node-capability-admission/issues/05-docs-hardware-lane-contract.md", "dependsOn": [ "NCA-002", "NCA-004" - ] + ], + "completionNotes": "Completed by agent" } ], "metadata": { - "updatedAt": "2026-07-11T19:16:52.768Z" + "updatedAt": "2026-07-12T01:54:03.030Z" } } \ No newline at end of file diff --git a/docs/issues/42-gguf-llamacpp-node-backend.md b/docs/issues/42-gguf-llamacpp-node-backend.md index dc26ac4..3a33c05 100644 --- a/docs/issues/42-gguf-llamacpp-node-backend.md +++ b/docs/issues/42-gguf-llamacpp-node-backend.md @@ -1,9 +1,16 @@ # US-042 — GGUF/llama.cpp node backend Status: planned -Priority: High (whole-model GGUF shortcut; distributed path in [ADR-0024](../adr/0024-distributed-gguf-runtime.md)) +Priority: High (unlocks DeepSeek-V4-Flash on volunteer hardware — the pool's core value) Stage: Draft design +## Goal + +Run **DeepSeek-V4-Flash** as the first real large-model target on volunteer +hardware via GGUF/llama.cpp. This epic is no longer GLM-oriented: the initial +objective is to prove that DeepSeek-V4-Flash can load and serve correctly on +consumer/unified-memory nodes, then expand from there. + ## Context The node backend is transformers-only (`model_backend.py` → @@ -35,17 +42,7 @@ to it (single-hop route). Smallest step, no cross-node activation work, and already useful: Strix Halo 128 GB serves DeepSeek-V4-Flash IQ3_XXS (114 GB) via llama.cpp Vulkan today. -Recommended sequencing: **C first** (US-042), then **ADR-0024 benchmark gate** (DGR-001), then distributed native worker (DGR-002+). Direction B (llama.cpp RPC) is rejected per ADR-0024. - -## Runtime sequencing - -| Stage | Track | Delivers | -|---|---|---| -| **C — Whole-model GGUF** | US-042 (this issue) | Single-hop llama.cpp, billing, relay streaming | -| **0 — Benchmark gate** | ADR-0024 DGR-001 | Safetensors vs GGUF measured contract | -| **1 — Distributed GGUF** | ADR-0024 `.scratch/distributed-gguf-runtime/` | gRPC C++ worker, layer-range GGUF | - -Phase C uses the existing tracker hop path (whole model, one node). ADR-0024 direction A (layer-range GGUF + activations) merges into the native worker track after the benchmark gate — not in parallel with phase C on the same backend without an integration plan. +Recommended sequencing: C first (small, real value), then A/B investigation. ## Also in scope From 8cb00e951f6ec11f74e40ec9179f1f5ddfd3d304 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 16:11:18 +0200 Subject: [PATCH 08/15] feat: show admin node pool capacity --- packages/node/meshnet_node/hardware.py | 24 +++++++++++---- .../tracker/meshnet_tracker/dashboard.html | 29 ++++++++++++++++++- packages/tracker/meshnet_tracker/server.py | 5 ++++ tests/test_dashboard.py | 3 ++ 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/packages/node/meshnet_node/hardware.py b/packages/node/meshnet_node/hardware.py index 7c97a06..9bffa9a 100644 --- a/packages/node/meshnet_node/hardware.py +++ b/packages/node/meshnet_node/hardware.py @@ -2,6 +2,7 @@ import json import os +import shutil import subprocess import time @@ -183,6 +184,17 @@ def with_forced_cpu(hw: dict) -> dict: return forced +def _with_model_drive(profile: dict) -> dict: + """Attach free space for the default model cache drive to tracker diagnostics.""" + try: + cache_root = os.path.expanduser("~/.cache/meshnet/shards") + profile["model_drive_free_bytes"] = shutil.disk_usage(os.path.expanduser("~")).free + profile["model_drive_path"] = cache_root + except OSError: + pass + return profile + + def detect_hardware() -> dict: """Detect GPU model and available VRAM. Returns hardware profile dict.""" ram_mb = _detect_ram_mb() @@ -208,23 +220,23 @@ def detect_hardware() -> dict: } if torch_gpu is not None and torch_gpu.get("gcn_arch"): profile["gcn_arch"] = torch_gpu["gcn_arch"] - return profile + return _with_model_drive(profile) except ImportError: pass torch_inventory = _gpu_inventory_profile(torch_gpu, ram_mb) if torch_inventory is not None: - return torch_inventory + return _with_model_drive(torch_inventory) nvidia_gpu = _gpu_inventory_profile(_detect_nvidia_smi_gpu_memory(), ram_mb) if nvidia_gpu is not None: - return nvidia_gpu + return _with_model_drive(nvidia_gpu) windows_gpu = _gpu_inventory_profile(_detect_windows_gpu_memory(), ram_mb) if windows_gpu is not None: - return windows_gpu + return _with_model_drive(windows_gpu) - return { + return _with_model_drive({ "device": "cpu", "gpu_name": None, "vram_mb": 0, @@ -232,7 +244,7 @@ def detect_hardware() -> dict: "shared_vram_mb": 0, "ram_mb": ram_mb, "cuda_available": False, - } + }) def benchmark_throughput_checked(device_str: str = "cpu") -> tuple[float, bool, str | None]: diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index bbddc6c..ef4b282 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -296,6 +296,7 @@

Settlement history

admin login required

Tracker hive

loading…

Model placement

Choose a model to load or release.
admin login required
+

Total node pool

admin login required

All accounts (admin)

Strikes / bans / forfeitures

admin login required

Client balances

admin login required
@@ -1789,7 +1790,7 @@ async function requestSelectedModelLoad() { if (!selectedChatModel) return; const button = $("request-model-load"); if (button) button.disabled = true; - const result = await apiCall("/v1/models/load", "POST", { model: selectedChatModel }); + const result = await apiCall("/v1/models/load", "POST", { model: selectedChatModel, force: isAdmin }); if (button) button.disabled = false; if (!result.ok) { alert(result.data.error || "model load request failed"); @@ -1820,6 +1821,31 @@ function showAdminModelPlacementStatus(message, isError) { status.className = isError ? "bad" : "ok"; } +function gib(bytes) { return bytes == null ? "not reported" : `${(Number(bytes) / 1073741824).toFixed(1)} GiB`; } + +function renderAdminNodePool(map) { + const groups = {}; + for (const node of (map && map.nodes) || []) { + const account = node.wallet_address || "unbound account"; + (groups[account] = groups[account] || []).push(node); + } + let html = ""; + for (const [account, nodes] of Object.entries(groups).sort(([a], [b]) => a.localeCompare(b))) { + html += `
${esc(short(account, 20))} ${nodes.length} node(s)
`; + html += table(["node", "assignment", "state / slots", "RAM", "GPU / VRAM", "model drive"], nodes.map(node => { + const hw = node.hardware_profile || {}; + const cap = node.capacity || {}; + const disk = hw.model_drive_free_bytes ?? hw.model_path_free_bytes ?? hw.disk_free_bytes; + const gpu = hw.gpu_name || (hw.cuda_available ? "CUDA GPU" : "CPU only"); + return [nodeDisplayCell(node), esc(node.hf_repo || node.model || "unassigned"), + esc(`${node.stats?.status || "?"} · ${cap.loaded_slots ?? "?"}/${cap.max_loaded_shards ?? node.max_loaded_shards ?? "?"} slots`), + esc(gib(node.ram_bytes || (hw.ram_mb && hw.ram_mb * 1048576))), + esc(`${gpu} · ${gib(node.vram_bytes || (hw.vram_mb && hw.vram_mb * 1048576))}`), esc(gib(disk))]; + })); + } + $("admin-node-pool").innerHTML = html || '
no nodes registered
'; +} + function renderAdminModelPlacement(models, map) { const nodes = (map && map.nodes) || []; const rows = ((models && models.data) || []).map(model => { @@ -2490,6 +2516,7 @@ async function fetchAdminTab() { renderIfChanged("billing-summary", summary, data => renderBilling(data)); renderIfChanged("fraud", { wallets, summary }, data => renderFraud(data.wallets, data.summary)); renderIfChanged("admin-model-placement", { models, map }, data => renderAdminModelPlacement(data.models, data.map)); + renderIfChanged("admin-node-pool", map, renderAdminNodePool); if (adminResp && adminResp.ok) { renderIfChanged("admin", adminResp.data.accounts || [], accounts => { const rows = accounts.map(a => { diff --git a/packages/tracker/meshnet_tracker/server.py b/packages/tracker/meshnet_tracker/server.py index 32c7239..1ea2144 100644 --- a/packages/tracker/meshnet_tracker/server.py +++ b/packages/tracker/meshnet_tracker/server.py @@ -3366,6 +3366,11 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): "endpoint": node.endpoint, "relay_addr": node.relay_addr, "peer_id": node.peer_id, + "wallet_address": node.wallet_address, + "hardware_profile": dict(node.hardware_profile), + "ram_bytes": node.ram_bytes, + "vram_bytes": node.vram_bytes, + "max_loaded_shards": node.max_loaded_shards, } for node in tracker_nodes ], diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 9492b12..d3c1234 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -114,6 +114,9 @@ def test_dashboard_exposes_admin_model_inventory_and_release_controls(): assert 'data-admin-model-load=' in html assert 'data-admin-model-release=' in html assert "admin-model-placement-status" in html + assert 'id="admin-node-pool"' in html + assert "renderAdminNodePool" in html + assert "model drive" in html def test_network_map_includes_node_friendly_name(): From 21e6c861470682a06be6919be32a6c7beac34fb6 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 16:37:42 +0200 Subject: [PATCH 09/15] fix: let admin placement recover joined nodes --- packages/tracker/meshnet_tracker/server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/tracker/meshnet_tracker/server.py b/packages/tracker/meshnet_tracker/server.py index 1ea2144..149ff3d 100644 --- a/packages/tracker/meshnet_tracker/server.py +++ b/packages/tracker/meshnet_tracker/server.py @@ -1534,15 +1534,15 @@ def _force_model_load_locked(server: "_TrackerHTTPServer", model_key: str) -> di if preset is None or not preset.get("hf_repo"): return None start, end = _preset_layer_bounds(preset) + # An explicit admin eviction is permitted to recover a stuck/loading node + # and to use the preset default precision. It must only avoid a node that + # already has another assignment in flight. candidates = [node for node in server.registry.values() - if node.status == "ready" and node.pending_new_assignment is None - and _has_usable_quantization(node)] + if node.pending_new_assignment is None] if not candidates: return None node = max(candidates, key=lambda item: item.benchmark_tokens_per_sec) - shard_end = min(end, start + min(_node_layer_capacity(node, preset), end - start + 1) - 1) - if shard_end < start: - return None + shard_end = min(end, start + max(1, min(_node_layer_capacity(node, preset), end - start + 1)) - 1) quantization = _node_quantization(node, preset) directive = _load_directive(node, str(preset["hf_repo"]), start, shard_end, quantization) replaced = node.hf_repo or node.model From b661590ac7bf0a142cdf1c68a637a1dd2c1233ca Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 17:47:20 +0200 Subject: [PATCH 10/15] log window bigger --- packages/tracker/meshnet_tracker/dashboard.html | 5 +++-- packages/tracker/meshnet_tracker/server.py | 2 +- tests/test_dashboard.py | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index ef4b282..8f4df39 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -212,7 +212,7 @@ .chat-compose button:disabled { opacity:.45; cursor:not-allowed; } .console { background:var(--bg); border:1px solid var(--border); border-radius:6px; - min-height:160px; max-height:280px; overflow:auto; padding:7px 9px; + min-height:160px; max-height:520px; overflow-y:auto; overflow-x:auto; padding:7px 9px; white-space:pre-wrap; word-break:break-word; font-size:11px; } .console-line { padding:1px 0; border-bottom:1px solid #161b22; } @@ -1083,6 +1083,7 @@ function renderBillingUsage(records) { } let consoleClearedAt = 0; +const CONSOLE_MAX_LINES = 1000; function clearConsole() { consoleClearedAt = Date.now() / 1000; @@ -1096,7 +1097,7 @@ function renderConsole(data) { $("console").innerHTML = '
no console events
'; return; } - $("console").innerHTML = events.slice(-120).map(e => { + $("console").innerHTML = events.slice(-CONSOLE_MAX_LINES).map(e => { const level = String(e.level || "info"); const cls = level === "error" ? "console-level-error" : level === "warn" ? "console-level-warn" : "console-level-info"; const fields = e.fields && Object.keys(e.fields).length ? " " + JSON.stringify(e.fields) : ""; diff --git a/packages/tracker/meshnet_tracker/server.py b/packages/tracker/meshnet_tracker/server.py index 149ff3d..cbb3663 100644 --- a/packages/tracker/meshnet_tracker/server.py +++ b/packages/tracker/meshnet_tracker/server.py @@ -86,7 +86,7 @@ from .model_files import files_for_layer_range, snapshot_dir_for_repo from .raft import RaftNode -_CONSOLE_LIMIT = 300 +_CONSOLE_LIMIT = 1000 _PROXY_PROGRESS_LOG_INTERVAL = 5.0 _SESSION_COOKIE_NAME = "meshnet_session" diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index d3c1234..f387d21 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -44,6 +44,9 @@ def test_dashboard_served_with_all_panels(): assert ".wide { grid-column:span 2; }" in html assert 'onclick="clearConsole()"' in html assert "let consoleClearedAt = 0;" in html + assert "max-height:520px; overflow-y:auto; overflow-x:auto;" in html + assert "const CONSOLE_MAX_LINES = 1000;" in html + assert "events.slice(-CONSOLE_MAX_LINES)" in html finally: tracker.stop() From ba7c656364367afc32305d9f4a7c01dc3a05d219 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 14 Jul 2026 20:33:02 +0200 Subject: [PATCH 11/15] node metrics --- .claude/memory/project-status.md | 5 +++ _DEV_NOTES.md | 7 +-- .../tracker/meshnet_tracker/dashboard.html | 45 ++++++++++++++++--- packages/tracker/meshnet_tracker/server.py | 26 ++++++++--- tests/test_dashboard.py | 6 +++ tests/test_tracker_routing.py | 37 +++++++++++++++ 6 files changed, 109 insertions(+), 17 deletions(-) diff --git a/.claude/memory/project-status.md b/.claude/memory/project-status.md index 3fb0fc7..adbf7e7 100644 --- a/.claude/memory/project-status.md +++ b/.claude/memory/project-status.md @@ -8,6 +8,11 @@ metadata: # Project Status (2026-07-13) +## Selected-node model placement (2026-07-14) + +- Admin Model placement now opens a node selector for load and release; the control-plane accepts optional `node_id` and targets only that registry assignment. Multi-model serving remains supported through `ADD_SHARD` and `max_loaded_shards`. +- Total node pool resource values are rendered from `/v1/network/map`'s `node.capacity` contract. Route selection remains assignment/capability/throughput/queue based; capacity is used for placement and falls back to tracker defaults only if a node truly omits it. + ## Distributed inference performance (2026-07-14) `DIP-001` is done in `.scratch/distributed-inference-performance/`: the deterministic two-node Route Session stub benchmark covers direct/relay plus cached/stateless prefill and decode. Its JSON and concise summary explicitly attribute model execution, activation encode/decode, compression, connection setup, relay queueing, local HTTP forwarding, and end-to-end seam latency. `PYTHONPATH=packages/node pytest -q tests/test_route_session_benchmark.py` passed (7); the fixture assertion checks output-token identity and connection attempts. diff --git a/_DEV_NOTES.md b/_DEV_NOTES.md index 28f7987..acb8dc4 100644 --- a/_DEV_NOTES.md +++ b/_DEV_NOTES.md @@ -16,12 +16,9 @@ .\.venv\Scripts\meshnet-node.exe start http://192.168.0.179:8081 --model-id Qwen/Qwen2.5-0.5B-Instruct --advertise-host 192.168.0.20 - .\.venv\Scripts\meshnet-node.exe start --tracker http://ai.neuron.d-popov.com --model-id Qwen/Qwen2.5-0.5B-Instruct --advertise-host 192.168.0.20 + .\.venv\Scripts\meshnet-node.exe start --tracker http://ai.neuron.d-popov.com --model Qwen/Qwen2.5-0.5B-Instruct --advertise-host 192.168.0.20 - we .\.venv\Scripts\meshnet-node.exe start ` - --tracker http://192.168.0.179:8081 ` - --model Qwen/Qwen2.5-0.5B-Instruct ` - --advertise-host 192.168.0.20 + we .\.venv\Scripts\meshnet-node.exe start --tracker http://192.168.0.179:8081 --model Qwen/Qwen2.5-0.5B-Instruct # trackers: https://meshnet.2.d-popov.com https://ai.neuron.d-popov.com diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index 8f4df39..279d61a 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -44,12 +44,15 @@ .empty { color:var(--dim); font-style:italic; } .pill { display:inline-block; padding:0 7px; border-radius:9px; border:1px solid var(--border); font-size:11px; } - input, button { font:inherit; color:var(--fg); background:var(--bg); + input, button, select { font:inherit; color:var(--fg); background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:5px 8px; } input { width:100%; margin-bottom:6px; } button { cursor:pointer; color:var(--accent); } button:hover { border-color:var(--accent); } button.small { font-size:11px; padding:1px 7px; } + dialog { color:var(--fg); background:var(--panel); border:1px solid var(--border); border-radius:8px; min-width:min(420px,calc(100vw - 32px)); } + dialog::backdrop { background:rgba(0,0,0,.55); } + .placement-dialog-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:12px; } .form-row { display:flex; gap:8px; } .form-row button { white-space:nowrap; } .error-msg { color:var(--bad); font-size:12px; min-height:16px; } @@ -324,6 +327,14 @@
no test output yet
+ +
+
+ + +
+
+