feat: auto-join network — node discovers missing shards from tracker

Tracker:
- _NodeEntry gains hf_repo + num_layers fields (parsed from register body)
- GET /v1/network/assign — finds the biggest uncovered shard gap across
  registered HF-model nodes; returns {hf_repo, shard_start, shard_end, num_layers}
- Returns 503 when no HF-model nodes are registered yet

Node startup:
- When model_id is set: registers with tracker including hf_repo + num_layers
  so other nodes can auto-join this model
- When model_id is empty/None: queries /v1/network/assign, gets assigned the
  missing layers, loads TorchNodeServer with the assigned shard automatically
- Fixes empty-string model_id leaking from DEFAULTS (treats "" same as None)

Usage: `meshnet-node start --tracker http://localhost:8080 --quantization bfloat16`
Node discovers what to serve and joins the network without any model flags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dobromir Popov
2026-06-30 00:22:33 +03:00
parent 1bdfce657d
commit a7cc377d13
2 changed files with 206 additions and 3 deletions

View File

@@ -84,7 +84,7 @@ def run_startup(
if probationary_line is not None:
print(f" {probationary_line}", flush=True)
if model_id is not None:
if model_id: # treat "" the same as None — no explicit model given
# Auto-detect shard range from model config if not explicitly provided
if shard_start is None or shard_end is None:
detected = _detect_num_layers(model_id)
@@ -116,6 +116,28 @@ def run_startup(
shard_label = f"layers {shard_start}{shard_end}"
public_host = advertise_host or (socket.getfqdn() if host == "0.0.0.0" else host)
endpoint = f"http://{public_host}:{actual_port}"
# Register with tracker so other nodes can auto-join this model.
total_layers = getattr(node.backend, "total_layers", None)
try:
_post_json(
f"{tracker_url}/v1/nodes/register",
{
"endpoint": endpoint,
"model": model_id.split("/")[-1],
"hf_repo": model_id,
"num_layers": total_layers,
"shard_start": shard_start,
"shard_end": shard_end,
"hardware_profile": hw,
"wallet_address": address,
"quantization": quantization,
"score": 1.0,
"tracker_mode": (shard_start == 0),
},
)
except Exception as exc:
print(f" Warning: tracker registration failed: {exc}", flush=True)
print(
f"\n{'=' * 32}\n"
f"meshnet-node ready\n"
@@ -132,7 +154,74 @@ def run_startup(
if shard_start is not None or shard_end is not None:
raise ValueError("--shard-start / --shard-end require --model-id")
# 3. Shard assignment from tracker
# 3a. Auto-join: query tracker for network-wide HF model assignment.
print("Querying tracker for network assignment...", flush=True)
assign_qs = urllib.parse.urlencode({"device": device, "vram_mb": vram_mb})
try:
net_assignment = _get_json(f"{tracker_url}/v1/network/assign?{assign_qs}")
assigned_hf_repo: str | None = net_assignment.get("hf_repo")
except Exception:
assigned_hf_repo = None
if assigned_hf_repo:
assigned_shard_start: int = net_assignment["shard_start"]
assigned_shard_end: int = net_assignment["shard_end"]
assigned_num_layers: int = net_assignment["num_layers"]
print(
f" Assigned: {assigned_hf_repo} "
f"layers {assigned_shard_start}{assigned_shard_end} "
f"(of {assigned_num_layers})",
flush=True,
)
print("Loading real PyTorch model shard...", flush=True)
node = TorchNodeServer(
host=host,
port=port,
model_id=assigned_hf_repo,
shard_start=assigned_shard_start,
shard_end=assigned_shard_end,
quantization=quantization,
tracker_url=tracker_url,
)
actual_port = node.start()
public_host = advertise_host or (socket.getfqdn() if host == "0.0.0.0" else host)
endpoint = f"http://{public_host}:{actual_port}"
try:
_post_json(
f"{tracker_url}/v1/nodes/register",
{
"endpoint": endpoint,
"model": assigned_hf_repo.split("/")[-1],
"hf_repo": assigned_hf_repo,
"num_layers": assigned_num_layers,
"shard_start": assigned_shard_start,
"shard_end": assigned_shard_end,
"hardware_profile": hw,
"wallet_address": address,
"quantization": quantization,
"score": 1.0,
"tracker_mode": (assigned_shard_start == 0),
},
)
except Exception as exc:
print(f" Warning: tracker registration failed: {exc}", flush=True)
shard_count = assigned_shard_end - assigned_shard_start + 1
print(
f"\n{'=' * 32}\n"
f"meshnet-node ready (auto-joined)\n"
f" Wallet: {address}\n"
f" Model ID: {assigned_hf_repo}\n"
f" Shard: layers {assigned_shard_start}{assigned_shard_end} "
f"({shard_count} of {assigned_num_layers})\n"
f" Quantization: {quantization}\n"
f" Endpoint: {endpoint}\n"
f" Hardware: {device.upper()}\n"
f"{'=' * 32}",
flush=True,
)
return node
# 3b. Shard assignment from tracker (stub-model / preset-based path)
print("Querying tracker for shard assignment...", flush=True)
assign_qs = urllib.parse.urlencode({
"model": model,