dash - better model health
This commit is contained in:
@@ -84,6 +84,7 @@ const esc = s => String(s).replace(/[&<>"]/g,
|
|||||||
c => ({"&":"&","<":"<",">":">",'"':"""}[c]));
|
c => ({"&":"&","<":"<",">":">",'"':"""}[c]));
|
||||||
const usdt = v => (Math.round(v * 1e6) / 1e6).toFixed(6);
|
const usdt = v => (Math.round(v * 1e6) / 1e6).toFixed(6);
|
||||||
const tps = v => (v === null || v === undefined) ? "?" : (Math.round(v * 10) / 10).toFixed(1);
|
const tps = v => (v === null || v === undefined) ? "?" : (Math.round(v * 10) / 10).toFixed(1);
|
||||||
|
const copies = v => (v === null || v === undefined) ? "?" : Number(v).toFixed(2);
|
||||||
const short = (s, n=14) => { s = String(s); return s.length > n ? s.slice(0, 6) + "…" + s.slice(-5) : s; };
|
const short = (s, n=14) => { s = String(s); return s.length > n ? s.slice(0, 6) + "…" + s.slice(-5) : s; };
|
||||||
|
|
||||||
async function fetchJson(path) {
|
async function fetchJson(path) {
|
||||||
@@ -129,16 +130,17 @@ function renderNodes(map) {
|
|||||||
}
|
}
|
||||||
let html = "";
|
let html = "";
|
||||||
for (const [model, group] of Object.entries(byModel)) {
|
for (const [model, group] of Object.entries(byModel)) {
|
||||||
html += `<div><b>${esc(model)}</b> <span class="dim">(${group.length} node${group.length===1?"":"s"})</span></div>`;
|
const supply = group.find(n => n.model_supply && n.model_supply.served_model_copies !== undefined);
|
||||||
html += table(["node", "shard", "tps (1h)", "queue", "health"], group.map(n => {
|
const served = supply && supply.model_supply && supply.model_supply.served_model_copies;
|
||||||
|
html += `<div><b>${esc(model)}</b> <span class="dim">(${group.length} node${group.length===1?"":"s"} · ${esc(copies(served))} served)</span></div>`;
|
||||||
|
html += table(["node", "shard", "tps (1h)", "queue", "served"], group.map(n => {
|
||||||
const modelStats = (n.throughput && (n.throughput[n.hf_repo] || n.throughput[n.model])) || {};
|
const modelStats = (n.throughput && (n.throughput[n.hf_repo] || n.throughput[n.model])) || {};
|
||||||
return [
|
return [
|
||||||
esc(short(n.node_id || "?")),
|
esc(short(n.node_id || "?")),
|
||||||
esc(`${n.shard_start ?? "?"}-${n.shard_end ?? "?"}`),
|
esc(`${n.shard_start ?? "?"}-${n.shard_end ?? "?"}`),
|
||||||
`<span class="num">${esc(tps(modelStats.tokens_per_sec_last_hour))}</span>`,
|
`<span class="num">${esc(tps(modelStats.tokens_per_sec_last_hour))}</span>`,
|
||||||
esc(String((n.stats && n.stats.queue_depth) ?? 0)),
|
esc(String((n.stats && n.stats.queue_depth) ?? 0)),
|
||||||
(n.stats && (n.stats.alive === false || n.stats.healthy === false))
|
`<span class="num">${esc(copies(n.model_supply && n.model_supply.served_model_copies))}</span>`,
|
||||||
? '<span class="bad">down</span>' : '<span class="ok">up</span>',
|
|
||||||
]; }));
|
]; }));
|
||||||
}
|
}
|
||||||
$("nodes").innerHTML = html;
|
$("nodes").innerHTML = html;
|
||||||
|
|||||||
@@ -728,6 +728,33 @@ def _coverage_percentage(
|
|||||||
return round((covered / required_layers) * 100, 2)
|
return round((covered / required_layers) * 100, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def _served_model_copies(
|
||||||
|
nodes: list[_NodeEntry],
|
||||||
|
required_start: int,
|
||||||
|
required_end: int,
|
||||||
|
) -> float:
|
||||||
|
required_layers = required_end - required_start + 1
|
||||||
|
if required_layers <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
layer_counts = []
|
||||||
|
for layer in range(required_start, required_end + 1):
|
||||||
|
count = 0
|
||||||
|
for node in nodes:
|
||||||
|
if node.shard_start is None or node.shard_end is None:
|
||||||
|
continue
|
||||||
|
if node.shard_start <= layer <= node.shard_end:
|
||||||
|
count += 1
|
||||||
|
layer_counts.append(count)
|
||||||
|
|
||||||
|
if not layer_counts:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
complete_copies = min(layer_counts)
|
||||||
|
residual_layers = sum(1 for count in layer_counts if count > complete_copies)
|
||||||
|
return round(complete_copies + (residual_layers / required_layers), 2)
|
||||||
|
|
||||||
|
|
||||||
def _preset_layer_bounds(preset: dict) -> tuple[int, int]:
|
def _preset_layer_bounds(preset: dict) -> tuple[int, int]:
|
||||||
start = int(preset.get("layers_start", 0))
|
start = int(preset.get("layers_start", 0))
|
||||||
if "layers_end" in preset:
|
if "layers_end" in preset:
|
||||||
@@ -2143,6 +2170,40 @@ 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:
|
||||||
|
model = node.model or 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,
|
||||||
"pool": _pool_summary(nodes),
|
"pool": _pool_summary(nodes),
|
||||||
@@ -2173,6 +2234,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
"tracker_mode": node.tracker_mode,
|
"tracker_mode": node.tracker_mode,
|
||||||
"last_heartbeat": node.last_heartbeat,
|
"last_heartbeat": node.last_heartbeat,
|
||||||
"capacity": capacity_for(node),
|
"capacity": capacity_for(node),
|
||||||
|
"model_supply": model_supply_for(node),
|
||||||
"throughput": throughput_for(node),
|
"throughput": throughput_for(node),
|
||||||
"stats": _node_health(node, server.heartbeat_timeout),
|
"stats": _node_health(node, server.heartbeat_timeout),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,59 @@ def test_network_map_exposes_pool_size_and_speed_summary():
|
|||||||
assert pool["total_effective_throughput"] == 10.0
|
assert pool["total_effective_throughput"] == 10.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_network_map_exposes_served_model_copy_count():
|
||||||
|
tracker = TrackerServer()
|
||||||
|
port = tracker.start()
|
||||||
|
url = f"http://127.0.0.1:{port}"
|
||||||
|
try:
|
||||||
|
_post_json(
|
||||||
|
f"{url}/v1/nodes/register",
|
||||||
|
{
|
||||||
|
"endpoint": "http://127.0.0.1:7201",
|
||||||
|
"model": "copy-count-test",
|
||||||
|
"hf_repo": "example/copy-count-test",
|
||||||
|
"num_layers": 37,
|
||||||
|
"shard_start": 0,
|
||||||
|
"shard_end": 21,
|
||||||
|
"hardware_profile": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
network_map = _get_json(f"{url}/v1/network/map")
|
||||||
|
assert network_map["nodes"][0]["model_supply"]["served_model_copies"] == 0.59
|
||||||
|
|
||||||
|
_post_json(
|
||||||
|
f"{url}/v1/nodes/register",
|
||||||
|
{
|
||||||
|
"endpoint": "http://127.0.0.1:7202",
|
||||||
|
"model": "copy-count-test",
|
||||||
|
"hf_repo": "example/copy-count-test",
|
||||||
|
"num_layers": 37,
|
||||||
|
"shard_start": 22,
|
||||||
|
"shard_end": 36,
|
||||||
|
"hardware_profile": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
network_map = _get_json(f"{url}/v1/network/map")
|
||||||
|
assert network_map["nodes"][0]["model_supply"]["served_model_copies"] == 1.0
|
||||||
|
|
||||||
|
_post_json(
|
||||||
|
f"{url}/v1/nodes/register",
|
||||||
|
{
|
||||||
|
"endpoint": "http://127.0.0.1:7203",
|
||||||
|
"model": "copy-count-test",
|
||||||
|
"hf_repo": "example/copy-count-test",
|
||||||
|
"num_layers": 37,
|
||||||
|
"shard_start": 0,
|
||||||
|
"shard_end": 36,
|
||||||
|
"hardware_profile": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
network_map = _get_json(f"{url}/v1/network/map")
|
||||||
|
assert network_map["nodes"][0]["model_supply"]["served_model_copies"] == 2.0
|
||||||
|
finally:
|
||||||
|
tracker.stop()
|
||||||
|
|
||||||
|
|
||||||
def test_recommended_kimi_becomes_deployable_when_pool_is_large_enough():
|
def test_recommended_kimi_becomes_deployable_when_pool_is_large_enough():
|
||||||
tracker = TrackerServer()
|
tracker = TrackerServer()
|
||||||
port = tracker.start()
|
port = tracker.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user