grouping
This commit is contained in:
@@ -728,11 +728,11 @@ def _coverage_percentage(
|
|||||||
return round((covered / required_layers) * 100, 2)
|
return round((covered / required_layers) * 100, 2)
|
||||||
|
|
||||||
|
|
||||||
def _served_model_copies(
|
def _served_model_copies(
|
||||||
nodes: list[_NodeEntry],
|
nodes: list[_NodeEntry],
|
||||||
required_start: int,
|
required_start: int,
|
||||||
required_end: int,
|
required_end: int,
|
||||||
) -> float:
|
) -> float:
|
||||||
required_layers = required_end - required_start + 1
|
required_layers = required_end - required_start + 1
|
||||||
if required_layers <= 0:
|
if required_layers <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
@@ -751,14 +751,60 @@ def _served_model_copies(
|
|||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
complete_copies = min(layer_counts)
|
complete_copies = min(layer_counts)
|
||||||
residual_layers = sum(1 for count in layer_counts if count > complete_copies)
|
residual_layers = sum(1 for count in layer_counts if count > complete_copies)
|
||||||
return round(complete_copies + (residual_layers / required_layers), 2)
|
return round(complete_copies + (residual_layers / required_layers), 2)
|
||||||
|
|
||||||
|
|
||||||
def _preset_layer_bounds(preset: dict) -> tuple[int, int]:
|
def _model_health_summary(
|
||||||
start = int(preset.get("layers_start", 0))
|
server: "_TrackerHTTPServer",
|
||||||
if "layers_end" in preset:
|
model: str | None,
|
||||||
return start, int(preset["layers_end"])
|
hf_repo: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
lookup = hf_repo or model
|
||||||
|
if not lookup:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
resolved_name, preset = _resolve_model_preset(server.model_presets, lookup)
|
||||||
|
if preset is not None:
|
||||||
|
required_start, required_end = _preset_layer_bounds(preset)
|
||||||
|
model_nodes = [
|
||||||
|
node for node in server.registry.values()
|
||||||
|
if _node_matches_preset(node, resolved_name, preset) # type: ignore[arg-type]
|
||||||
|
]
|
||||||
|
model_id = resolved_name or lookup
|
||||||
|
else:
|
||||||
|
model_nodes = [
|
||||||
|
node for node in server.registry.values()
|
||||||
|
if _node_matches_model(node, lookup)
|
||||||
|
and node.shard_start is not None
|
||||||
|
and node.shard_end is not None
|
||||||
|
and node.num_layers is not None
|
||||||
|
]
|
||||||
|
if not model_nodes:
|
||||||
|
return {
|
||||||
|
"model": lookup,
|
||||||
|
"served_model_copies": 0.0,
|
||||||
|
"coverage_percentage": 0.0,
|
||||||
|
"node_count": 0,
|
||||||
|
}
|
||||||
|
required_start = 0
|
||||||
|
required_end = max(node.num_layers for node in model_nodes if node.num_layers is not None) - 1
|
||||||
|
model_id = hf_repo or model or lookup
|
||||||
|
|
||||||
|
return {
|
||||||
|
"model": model_id,
|
||||||
|
"required_start": required_start,
|
||||||
|
"required_end": required_end,
|
||||||
|
"served_model_copies": _served_model_copies(model_nodes, required_start, required_end),
|
||||||
|
"coverage_percentage": _coverage_percentage(model_nodes, required_start, required_end),
|
||||||
|
"node_count": len(model_nodes),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _preset_layer_bounds(preset: dict) -> tuple[int, int]:
|
||||||
|
start = int(preset.get("layers_start", 0))
|
||||||
|
if "layers_end" in preset:
|
||||||
|
return start, int(preset["layers_end"])
|
||||||
return start, start + int(preset["total_layers"]) - 1
|
return start, start + int(preset["total_layers"]) - 1
|
||||||
|
|
||||||
|
|
||||||
@@ -1288,27 +1334,32 @@ def _drop_directive(node: _NodeEntry, model: str, start: int, end: int, quantiza
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _purge_expired_nodes_locked(server: "_TrackerHTTPServer") -> list[str]:
|
def _purge_expired_nodes_locked(server: "_TrackerHTTPServer") -> list[str]:
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
expired_ids = [
|
expired_ids = [
|
||||||
node_id for node_id, entry in server.registry.items()
|
node_id for node_id, entry in server.registry.items()
|
||||||
if (now - entry.last_heartbeat) > server.heartbeat_timeout
|
if (now - entry.last_heartbeat) > server.heartbeat_timeout
|
||||||
]
|
]
|
||||||
for node_id in expired_ids:
|
expired_entries: list[tuple[str, _NodeEntry]] = []
|
||||||
entry = server.registry.pop(node_id)
|
for node_id in expired_ids:
|
||||||
_tracker_log(
|
entry = server.registry.pop(node_id)
|
||||||
server,
|
expired_entries.append((node_id, entry))
|
||||||
"warn",
|
if expired_ids:
|
||||||
"node expired",
|
_rebalance_all_locked(server)
|
||||||
node_id=node_id,
|
for node_id, entry in expired_entries:
|
||||||
endpoint=entry.endpoint,
|
_tracker_log(
|
||||||
model=entry.model,
|
server,
|
||||||
shard=f"{entry.shard_start}-{entry.shard_end}",
|
"warn",
|
||||||
heartbeat_timeout_seconds=server.heartbeat_timeout,
|
"node expired",
|
||||||
)
|
node_id=node_id,
|
||||||
if expired_ids:
|
endpoint=entry.endpoint,
|
||||||
_rebalance_all_locked(server)
|
model=entry.model,
|
||||||
return expired_ids
|
hf_repo=entry.hf_repo,
|
||||||
|
shard=f"{entry.shard_start}-{entry.shard_end}",
|
||||||
|
heartbeat_timeout_seconds=server.heartbeat_timeout,
|
||||||
|
model_health=_model_health_summary(server, entry.model, entry.hf_repo),
|
||||||
|
)
|
||||||
|
return expired_ids
|
||||||
|
|
||||||
|
|
||||||
def _rebalance_model_locked(server: "_TrackerHTTPServer", model: str) -> None:
|
def _rebalance_model_locked(server: "_TrackerHTTPServer", model: str) -> None:
|
||||||
@@ -2170,39 +2221,8 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
result[model] = server.stats.get_node_model_stats(node.node_id, model)
|
result[model] = server.stats.get_node_model_stats(node.node_id, model)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def model_supply_for(node: _NodeEntry) -> dict:
|
def model_supply_for(node: _NodeEntry) -> dict:
|
||||||
model = node.model or node.hf_repo
|
return _model_health_summary(server, node.model, node.hf_repo)
|
||||||
if not model:
|
|
||||||
return {}
|
|
||||||
resolved_name, preset = _resolve_model_preset(server.model_presets, model)
|
|
||||||
if preset is not None:
|
|
||||||
required_start, required_end = _preset_layer_bounds(preset)
|
|
||||||
model_nodes = [
|
|
||||||
n for n in nodes
|
|
||||||
if _node_matches_preset(n, resolved_name, preset) # type: ignore[arg-type]
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
model_nodes = [
|
|
||||||
n for n in nodes
|
|
||||||
if _node_matches_model(n, model)
|
|
||||||
and n.shard_start is not None
|
|
||||||
and n.shard_end is not None
|
|
||||||
and n.num_layers is not None
|
|
||||||
]
|
|
||||||
if not model_nodes:
|
|
||||||
return {}
|
|
||||||
required_start = 0
|
|
||||||
required_end = max(n.num_layers for n in model_nodes if n.num_layers is not None) - 1
|
|
||||||
return {
|
|
||||||
"model": resolved_name or model,
|
|
||||||
"required_start": required_start,
|
|
||||||
"required_end": required_end,
|
|
||||||
"served_model_copies": _served_model_copies(
|
|
||||||
model_nodes,
|
|
||||||
required_start,
|
|
||||||
required_end,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
self._send_json(200, {
|
self._send_json(200, {
|
||||||
"relay_url": server.relay_url,
|
"relay_url": server.relay_url,
|
||||||
@@ -3088,41 +3108,50 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
with server.lock:
|
with server.lock:
|
||||||
self._purge_expired_nodes()
|
self._purge_expired_nodes()
|
||||||
# Dedup: if this endpoint is already registered, remove the old entry first.
|
# Dedup: if this endpoint is already registered, remove the old entry first.
|
||||||
stale_ids = [
|
stale_ids = [
|
||||||
eid for eid, e in server.registry.items()
|
eid for eid, e in server.registry.items()
|
||||||
if e.endpoint == entry.endpoint.rstrip("/")
|
if e.endpoint == entry.endpoint.rstrip("/")
|
||||||
]
|
]
|
||||||
for eid in stale_ids:
|
stale_entries: list[tuple[str, _NodeEntry]] = []
|
||||||
old = server.registry.pop(eid)
|
for eid in stale_ids:
|
||||||
_tracker_log(
|
old = server.registry.pop(eid)
|
||||||
server,
|
stale_entries.append((eid, old))
|
||||||
"info",
|
server.registry[node_id] = entry
|
||||||
"node re-registered",
|
if entry.managed_assignment:
|
||||||
old_node_id=eid,
|
if entry.hf_repo:
|
||||||
node_id=node_id,
|
_rebalance_hf_model_locked(server, entry.hf_repo)
|
||||||
endpoint=old.endpoint,
|
else:
|
||||||
)
|
|
||||||
server.registry[node_id] = entry
|
|
||||||
if entry.managed_assignment:
|
|
||||||
if entry.hf_repo:
|
|
||||||
_rebalance_hf_model_locked(server, entry.hf_repo)
|
|
||||||
else:
|
|
||||||
_rebalance_model_locked(server, model)
|
_rebalance_model_locked(server, model)
|
||||||
assignment_directive = entry.pending_directives[-1] if entry.pending_directives else None
|
assignment_directive = entry.pending_directives[-1] if entry.pending_directives else None
|
||||||
if assignment_directive is not None:
|
if assignment_directive is not None:
|
||||||
entry.pending_directives.clear()
|
entry.pending_directives.clear()
|
||||||
|
|
||||||
_tracker_log(
|
model_health = _model_health_summary(server, entry.model, entry.hf_repo)
|
||||||
server,
|
for eid, old in stale_entries:
|
||||||
"info",
|
_tracker_log(
|
||||||
"node registered",
|
server,
|
||||||
node_id=node_id,
|
"info",
|
||||||
|
"node re-registered",
|
||||||
|
old_node_id=eid,
|
||||||
|
node_id=node_id,
|
||||||
|
endpoint=old.endpoint,
|
||||||
|
old_model=old.model,
|
||||||
|
old_hf_repo=old.hf_repo,
|
||||||
|
old_model_health=_model_health_summary(server, old.model, old.hf_repo),
|
||||||
|
model_health=model_health,
|
||||||
|
)
|
||||||
|
_tracker_log(
|
||||||
|
server,
|
||||||
|
"info",
|
||||||
|
"node registered",
|
||||||
|
node_id=node_id,
|
||||||
endpoint=entry.endpoint,
|
endpoint=entry.endpoint,
|
||||||
model=entry.model,
|
model=entry.model,
|
||||||
hf_repo=entry.hf_repo,
|
hf_repo=entry.hf_repo,
|
||||||
shard=f"{entry.shard_start}-{entry.shard_end}",
|
shard=f"{entry.shard_start}-{entry.shard_end}",
|
||||||
tracker_mode=entry.tracker_mode,
|
tracker_mode=entry.tracker_mode,
|
||||||
)
|
model_health=model_health,
|
||||||
|
)
|
||||||
|
|
||||||
shard_info = f"layers {shard_start}-{shard_end}" if shard_start is not None else "unsharded"
|
shard_info = f"layers {shard_start}-{shard_end}" if shard_start is not None else "unsharded"
|
||||||
repo_info = f" [{hf_repo}]" if hf_repo else ""
|
repo_info = f" [{hf_repo}]" if hf_repo else ""
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""US-035: tracker web dashboard — served from any tracker, embedded asset."""
|
"""US-035: tracker web dashboard — served from any tracker, embedded asset."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from meshnet_contracts import LocalSolanaContracts
|
from meshnet_contracts import LocalSolanaContracts
|
||||||
@@ -91,3 +92,45 @@ def test_console_endpoint_exposes_tracker_events():
|
|||||||
tracker.stop()
|
tracker.stop()
|
||||||
|
|
||||||
assert any(event["message"] == "node registered" for event in data["events"])
|
assert any(event["message"] == "node registered" for event in data["events"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_node_lifecycle_events_include_model_health():
|
||||||
|
tracker = TrackerServer(heartbeat_timeout=0.05)
|
||||||
|
port = tracker.start()
|
||||||
|
try:
|
||||||
|
body = json.dumps({
|
||||||
|
"endpoint": "http://127.0.0.1:9002",
|
||||||
|
"model": "console-health-test",
|
||||||
|
"hf_repo": "example/console-health-test",
|
||||||
|
"num_layers": 4,
|
||||||
|
"shard_start": 0,
|
||||||
|
"shard_end": 1,
|
||||||
|
"hardware_profile": {},
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"http://127.0.0.1:{port}/v1/nodes/register",
|
||||||
|
data=body,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
urllib.request.urlopen(req).read()
|
||||||
|
|
||||||
|
registered = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
|
||||||
|
registered_event = next(
|
||||||
|
event for event in registered["events"]
|
||||||
|
if event["message"] == "node registered"
|
||||||
|
)
|
||||||
|
assert registered_event["fields"]["model_health"]["served_model_copies"] == 0.5
|
||||||
|
assert registered_event["fields"]["model_health"]["coverage_percentage"] == 50.0
|
||||||
|
|
||||||
|
time.sleep(0.06)
|
||||||
|
urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/network/map").read()
|
||||||
|
expired = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
|
||||||
|
expired_event = next(
|
||||||
|
event for event in expired["events"]
|
||||||
|
if event["message"] == "node expired"
|
||||||
|
)
|
||||||
|
assert expired_event["fields"]["model_health"]["served_model_copies"] == 0.0
|
||||||
|
assert expired_event["fields"]["model_health"]["coverage_percentage"] == 0.0
|
||||||
|
finally:
|
||||||
|
tracker.stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user