From 2852b1f80ba3a81f9e425e98f1a35580c4847e0e Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Wed, 15 Jul 2026 12:54:51 +0200 Subject: [PATCH] loading more --- .../tracker/meshnet_tracker/dashboard.html | 28 +++++++++++++++---- packages/tracker/meshnet_tracker/server.py | 15 ++++++---- tests/test_dashboard.py | 3 ++ tests/test_dynamic_routing.py | 6 +++- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/tracker/meshnet_tracker/dashboard.html b/packages/tracker/meshnet_tracker/dashboard.html index 279d61a..f6615e1 100644 --- a/packages/tracker/meshnet_tracker/dashboard.html +++ b/packages/tracker/meshnet_tracker/dashboard.html @@ -332,6 +332,7 @@
+
@@ -1812,8 +1813,8 @@ 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, nodeId) { - const result = await apiCall("/v1/models/load", "POST", { model, node_id: nodeId, force: true }); +async function requestAdminModelLoad(model, nodeId, replacing) { + const result = await apiCall("/v1/models/load", "POST", { model, node_id: nodeId, force: replacing }); 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}.`); @@ -1887,14 +1888,31 @@ $("admin-model-placement").addEventListener("click", event => { function chooseModelPlacementNode(action, model) { const dialog = $("model-placement-dialog"); const select = $("model-placement-node"); - const aliases = new Set([model].filter(Boolean)); - const nodes = (lastNetworkMap?.nodes || []).filter(node => action === "load" || aliases.has(node.model) || aliases.has(node.hf_repo)); + const targetAlias = modelAliasKey(model); + const nodes = (lastNetworkMap?.nodes || []).filter(node => action === "load" || + modelAliasKey(node.model) === targetAlias || modelAliasKey(node.hf_repo) === targetAlias); if (!nodes.length) return showAdminModelPlacementStatus(`No node can ${action} ${model}.`, true); $("model-placement-dialog-title").textContent = `${action === "load" ? "Load" : "Release"} ${model} on a node`; select.innerHTML = nodes.map(node => ``).join(""); + const replace = $("model-placement-replace"); + const replaceConfirm = $("model-placement-replace-confirm"); + const selectedNode = () => nodes.find(node => node.node_id === select.value); + const updateReplacementWarning = () => { + const node = selectedNode(); + const occupied = action === "load" && node && node.shard_start != null && node.shard_end != null && + modelAliasKey(node.hf_repo || node.model) !== targetAlias; + replace.style.display = occupied ? "" : "none"; + replaceConfirm.checked = false; + }; + select.onchange = updateReplacementWarning; + updateReplacementWarning(); dialog.onclose = () => { if (dialog.returnValue !== "confirm") return; - if (action === "load") void requestAdminModelLoad(model, select.value); + const replacing = replace.style.display !== "none"; + if (replacing && !replaceConfirm.checked) { + return showAdminModelPlacementStatus("Confirm unloading the existing model before replacing it.", true); + } + if (action === "load") void requestAdminModelLoad(model, select.value, replacing); else void releaseAdminModel(model, select.value); }; dialog.showModal(); diff --git a/packages/tracker/meshnet_tracker/server.py b/packages/tracker/meshnet_tracker/server.py index 4e7875d..2d5cb76 100644 --- a/packages/tracker/meshnet_tracker/server.py +++ b/packages/tracker/meshnet_tracker/server.py @@ -1535,7 +1535,9 @@ def _request_model_load_locked( return None -def _force_model_load_locked(server: "_TrackerHTTPServer", model_key: str) -> dict | None: +def _force_model_load_locked( + server: "_TrackerHTTPServer", model_key: str, node_id: str | None = None, +) -> 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"): @@ -1544,8 +1546,11 @@ def _force_model_load_locked(server: "_TrackerHTTPServer", model_key: str) -> di # 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.pending_new_assignment is None] + candidates = [ + node for node in server.registry.values() + if node.pending_new_assignment is None + and (node_id is None or node.node_id == node_id) + ] if not candidates: return None node = max(candidates, key=lambda item: item.benchmark_tokens_per_sec) @@ -4917,8 +4922,8 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler): with server.lock: self._purge_expired_nodes() assignment = _request_model_load_locked(server, model, node_id) - if assignment is None and node_id is None and body.get("force") is True: - assignment = _force_model_load_locked(server, model) + if assignment is None and body.get("force") is True: + assignment = _force_model_load_locked(server, model, node_id) if assignment is None: self._send_json(409, {"error": "no ready joined node has an available model slot and sufficient capacity"}) return diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index a6254b6..8a31040 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -126,6 +126,9 @@ def test_dashboard_exposes_admin_model_inventory_and_release_controls(): assert 'id="model-placement-dialog"' in html assert "chooseModelPlacementNode" in html assert "node_id: nodeId" in html + assert "modelAliasKey(node.model)" in html + assert 'id="model-placement-replace"' in html + assert "force: replacing" in html def test_network_map_includes_node_friendly_name(): diff --git a/tests/test_dynamic_routing.py b/tests/test_dynamic_routing.py index cc82284..5fa202d 100644 --- a/tests/test_dynamic_routing.py +++ b/tests/test_dynamic_routing.py @@ -370,7 +370,11 @@ def test_admin_can_replace_a_served_model_and_release_it(): 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(), + data=json.dumps({ + "model": "qwen2.5-0.5b-instruct", + "node_id": node["node_id"], + "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"]