feat(us-016): tracker route for HF models, endpoint dedup, purge logging
Tracker /v1/route now resolves HF model nodes (by hf_repo or short name) in addition to preset models, using the same greedy interval-cover logic. This allows distributed inference routing across two nodes each holding half the model. Endpoint dedup: re-registering the same endpoint atomically replaces the old entry so stale registrations don't accumulate across node restarts. Purge logging: tracker now prints when a node expires due to missed heartbeats so operators can see dead nodes being removed. Timing fix: heartbeat timeout raised from 30s to 90s (3 missed beats); node heartbeat interval lowered from 30s to 20s to maintain margin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -574,6 +574,96 @@ def test_network_assign_gap_found_field():
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_route_finds_hf_model_across_two_nodes():
|
||||
"""Tracker /v1/route returns ordered route for HF model even without a preset."""
|
||||
import json as _json
|
||||
import urllib.request as _ur
|
||||
|
||||
tracker = TrackerServer()
|
||||
port = tracker.start()
|
||||
try:
|
||||
def register(endpoint, shard_start, shard_end):
|
||||
data = _json.dumps({
|
||||
"endpoint": endpoint,
|
||||
"model": "Qwen2.5-0.5B-Instruct",
|
||||
"hf_repo": "Qwen/Qwen2.5-0.5B-Instruct",
|
||||
"num_layers": 24,
|
||||
"shard_start": shard_start,
|
||||
"shard_end": shard_end,
|
||||
"hardware_profile": {},
|
||||
"score": 1.0,
|
||||
}).encode()
|
||||
req = _ur.Request(
|
||||
f"http://127.0.0.1:{port}/v1/nodes/register",
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with _ur.urlopen(req) as r:
|
||||
r.read()
|
||||
|
||||
register("http://127.0.0.1:9300", 0, 11)
|
||||
register("http://127.0.0.1:9301", 12, 23)
|
||||
|
||||
# Route by hf_repo (full identifier).
|
||||
resp = _get_json(
|
||||
f"http://127.0.0.1:{port}/v1/route?model=Qwen/Qwen2.5-0.5B-Instruct"
|
||||
)
|
||||
assert resp["route"] == ["http://127.0.0.1:9300", "http://127.0.0.1:9301"]
|
||||
|
||||
# Route also works by short model name.
|
||||
resp2 = _get_json(
|
||||
f"http://127.0.0.1:{port}/v1/route?model=Qwen2.5-0.5B-Instruct"
|
||||
)
|
||||
assert resp2["route"] == ["http://127.0.0.1:9300", "http://127.0.0.1:9301"]
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_register_deduplicates_same_endpoint():
|
||||
"""Re-registering the same endpoint replaces the old entry, not duplicates it."""
|
||||
import json as _json
|
||||
import urllib.request as _ur
|
||||
|
||||
tracker = TrackerServer()
|
||||
port = tracker.start()
|
||||
try:
|
||||
def register(shard_start, shard_end):
|
||||
data = _json.dumps({
|
||||
"endpoint": "http://127.0.0.1:9400",
|
||||
"model": "Qwen2.5-0.5B-Instruct",
|
||||
"hf_repo": "Qwen/Qwen2.5-0.5B-Instruct",
|
||||
"num_layers": 24,
|
||||
"shard_start": shard_start,
|
||||
"shard_end": shard_end,
|
||||
"hardware_profile": {},
|
||||
"score": 1.0,
|
||||
}).encode()
|
||||
req = _ur.Request(
|
||||
f"http://127.0.0.1:{port}/v1/nodes/register",
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with _ur.urlopen(req) as r:
|
||||
return _json.loads(r.read())
|
||||
|
||||
register(0, 23) # initial full-model registration
|
||||
register(12, 23) # re-register with corrected shard range
|
||||
|
||||
# After re-register, tracker should see only one node at 12-23 for this endpoint.
|
||||
# If both were still registered, the gap scan would find no gap (0-23 still covers).
|
||||
# With dedup, the old 0-23 is gone and a real gap 0-11 exists.
|
||||
assign_resp = _get_json(
|
||||
f"http://127.0.0.1:{port}/v1/network/assign?device=cpu&vram_mb=0"
|
||||
"&hf_repo=Qwen/Qwen2.5-0.5B-Instruct"
|
||||
)
|
||||
assert assign_resp["gap_found"] is True
|
||||
assert assign_resp["shard_start"] == 0, "old 0-23 entry should have been replaced"
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_startup_cpu_fallback(tmp_path, monkeypatch):
|
||||
"""Node starts with CPU warning when no GPU is detected."""
|
||||
import meshnet_node.startup as startup_mod
|
||||
|
||||
Reference in New Issue
Block a user