routing and dashboard fixes
This commit is contained in:
@@ -139,22 +139,33 @@ def _hardware_label(device: str, gpu_name: str | None = None) -> str:
|
||||
return "CPU"
|
||||
|
||||
|
||||
def _max_assignable_layers(memory_mb: int, total_layers: int | None) -> int:
|
||||
def _max_assignable_layers(
|
||||
memory_mb: int,
|
||||
total_layers: int | None,
|
||||
bytes_per_layer: int | None = None,
|
||||
) -> int:
|
||||
if total_layers is None or total_layers <= 0 or memory_mb <= 0:
|
||||
return 0
|
||||
budget_bytes = memory_mb * 1024 * 1024
|
||||
return min(total_layers, int((budget_bytes * 0.8) // _DEFAULT_BYTES_PER_LAYER))
|
||||
layer_bytes = bytes_per_layer or _DEFAULT_BYTES_PER_LAYER
|
||||
return min(total_layers, int((budget_bytes * 0.8) // layer_bytes))
|
||||
|
||||
|
||||
def _shard_budget_line(memory_mb: int, memory_source: str, total_layers: int | None, quantization: str) -> str:
|
||||
def _shard_budget_line(
|
||||
memory_mb: int,
|
||||
memory_source: str,
|
||||
total_layers: int | None,
|
||||
quantization: str,
|
||||
bytes_per_layer: int | None = None,
|
||||
) -> str:
|
||||
memory_gb = memory_mb / 1024
|
||||
gb_str = f"{memory_gb:.1f} GB"
|
||||
budget_quantization = "bfloat16" if quantization == "auto" else quantization
|
||||
if total_layers is None or total_layers <= 0:
|
||||
return f"Memory budget: {gb_str} {memory_source}; shard budget: unknown model layer count"
|
||||
max_layers = _max_assignable_layers(memory_mb, total_layers)
|
||||
max_layers = _max_assignable_layers(memory_mb, total_layers, bytes_per_layer=bytes_per_layer)
|
||||
# Remaining capacity after one full model load (rough estimate)
|
||||
shard_bytes = max_layers * _DEFAULT_BYTES_PER_LAYER
|
||||
shard_bytes = max_layers * (bytes_per_layer or _DEFAULT_BYTES_PER_LAYER)
|
||||
remaining_gb = (memory_mb * 1024 * 1024 - shard_bytes) / (1024 ** 3)
|
||||
remaining_str = f"; {remaining_gb:.1f} GB remaining after full load" if remaining_gb > 1 else ""
|
||||
return (
|
||||
@@ -164,6 +175,23 @@ def _shard_budget_line(memory_mb: int, memory_source: str, total_layers: int | N
|
||||
)
|
||||
|
||||
|
||||
def _assignment_bytes_per_layer(assignment: dict, quantization: str) -> int | None:
|
||||
bytes_per_layer = assignment.get("bytes_per_layer")
|
||||
if isinstance(bytes_per_layer, int) and bytes_per_layer > 0:
|
||||
return bytes_per_layer
|
||||
if not isinstance(bytes_per_layer, dict):
|
||||
return None
|
||||
keys = [quantization, "bfloat16", "bf16", "int8", "nf4"]
|
||||
for key in keys:
|
||||
value = bytes_per_layer.get(key)
|
||||
if isinstance(value, int) and value > 0:
|
||||
return value
|
||||
for value in bytes_per_layer.values():
|
||||
if isinstance(value, int) and value > 0:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _post_json(url: str, payload: dict, timeout: float = 10.0) -> dict:
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
<section><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
|
||||
<section><h2>Node throughput</h2><div id="throughput" class="empty">loading…</div></section>
|
||||
<section class="wide"><h2>Console output</h2><div id="console" class="console empty">loading…</div></section>
|
||||
<section class="wide"><h2>Inference history</h2><div id="inference-history" class="empty">loading...</div></section>
|
||||
</main>
|
||||
<script>
|
||||
"use strict";
|
||||
@@ -209,9 +210,9 @@ function renderStats(stats) {
|
||||
}
|
||||
|
||||
function renderThroughput(stats) {
|
||||
const nodes = (stats && stats.nodes) || {};
|
||||
const rows = [];
|
||||
for (const [nodeId, nodeStats] of Object.entries(nodes)) {
|
||||
const nodes = (stats && stats.nodes) || {};
|
||||
const rows = [];
|
||||
for (const [nodeId, nodeStats] of Object.entries(nodes)) {
|
||||
for (const [model, s] of Object.entries((nodeStats && nodeStats.models) || {})) {
|
||||
rows.push([
|
||||
esc(short(nodeId)),
|
||||
@@ -220,10 +221,52 @@ function renderThroughput(stats) {
|
||||
`<span class="num">${esc(String(s.sample_count_last_hour ?? 0))}</span>`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$("throughput").innerHTML = table(["node", "model", "tps (1h)", "samples"], rows);
|
||||
}
|
||||
|
||||
function renderInferenceHistory(data) {
|
||||
const events = (data && data.events) || [];
|
||||
const started = new Map();
|
||||
const completed = [];
|
||||
for (const e of events) {
|
||||
const f = e.fields || {};
|
||||
const id = f.request_id;
|
||||
if (!id) continue;
|
||||
if (e.message === "proxy route selected") {
|
||||
started.set(id, e);
|
||||
} else if (e.message === "proxy complete" || e.message === "proxy failed" || e.message === "direct proxy failed after relay") {
|
||||
completed.push(e);
|
||||
started.delete(id);
|
||||
}
|
||||
}
|
||||
const activeByModel = {};
|
||||
for (const e of started.values()) {
|
||||
const f = e.fields || {};
|
||||
const model = f.model || f.route_model || "?";
|
||||
activeByModel[model] = (activeByModel[model] || 0) + 1;
|
||||
}
|
||||
const active = Object.entries(activeByModel)
|
||||
.map(([model, count]) => `${esc(model)}: <span class="num">${count}</span> active`)
|
||||
.join(" · ");
|
||||
const rows = completed.slice(-20).reverse().map(e => {
|
||||
const f = e.fields || {};
|
||||
return [
|
||||
new Date((e.ts || 0) * 1000).toLocaleTimeString(),
|
||||
esc(short(f.model || f.route_model || "?", 28)),
|
||||
esc(short(f.request_id || "?", 18)),
|
||||
`<span class="num">${esc(tps(f.tokens_per_sec))}</span>`,
|
||||
`<span class="num">${esc(String(f.tokens ?? "?"))}</span>`,
|
||||
`<span class="num">${esc(String(f.elapsed_seconds ?? "?"))}</span>`,
|
||||
f.stream ? "stream" : "json",
|
||||
];
|
||||
});
|
||||
$("inference-history").innerHTML =
|
||||
`<div class="dim" style="margin-bottom:6px">${active || "no active requests"}</div>` +
|
||||
(rows.length ? table(["time", "model", "request", "tps", "tokens", "sec", "mode"], rows)
|
||||
: '<div class="empty">no completed inference requests</div>');
|
||||
}
|
||||
|
||||
function renderConsole(data) {
|
||||
const events = (data && data.events) || [];
|
||||
if (!events.length) {
|
||||
@@ -406,9 +449,10 @@ async function refresh() {
|
||||
renderNodes(map);
|
||||
renderBilling(summary);
|
||||
renderSettlements(settlements);
|
||||
renderFraud(wallets, summary);
|
||||
renderFraud(wallets, summary);
|
||||
renderStats(stats);
|
||||
renderThroughput(stats);
|
||||
renderInferenceHistory(consoleData);
|
||||
renderConsole(consoleData);
|
||||
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
@@ -636,7 +636,7 @@ def _reputation_multiplier(node: "_NodeEntry", contracts: Any | None) -> float:
|
||||
return 0.5 + 0.5 * reputation
|
||||
|
||||
|
||||
def _select_route(
|
||||
def _select_route(
|
||||
nodes: list[_NodeEntry],
|
||||
required_start: int,
|
||||
required_end: int,
|
||||
@@ -678,10 +678,25 @@ def _select_route(
|
||||
covered_up_to = best.shard_end
|
||||
candidates = [n for n in candidates if n is not best]
|
||||
|
||||
return route, ""
|
||||
|
||||
|
||||
def _coverage_percentage(
|
||||
return route, ""
|
||||
|
||||
|
||||
def _node_route_summary(nodes: list["_NodeEntry"]) -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"node_id": node.node_id,
|
||||
"model": node.model,
|
||||
"hf_repo": node.hf_repo,
|
||||
"endpoint": node.endpoint,
|
||||
"shard": f"{node.shard_start}-{node.shard_end}",
|
||||
"num_layers": node.num_layers,
|
||||
"queue_depth": node.queue_depth,
|
||||
}
|
||||
for node in nodes
|
||||
]
|
||||
|
||||
|
||||
def _coverage_percentage(
|
||||
nodes: list[_NodeEntry],
|
||||
required_start: int,
|
||||
required_end: int,
|
||||
@@ -746,14 +761,14 @@ def _node_memory_budget_bytes(node: _NodeEntry) -> tuple[int, str]:
|
||||
return DEFAULT_RAM_BYTES, "ram-default"
|
||||
|
||||
|
||||
def _node_layer_capacity(node: _NodeEntry, preset: dict) -> int:
|
||||
bytes_per_layer = _preset_bytes_per_layer(preset)
|
||||
quantization = _node_quantization(node, preset)
|
||||
layer_bytes = bytes_per_layer[quantization]
|
||||
if layer_bytes <= 0:
|
||||
return 0
|
||||
memory_budget_bytes, _ = _node_memory_budget_bytes(node)
|
||||
return int((memory_budget_bytes * 0.8) // layer_bytes)
|
||||
def _node_layer_capacity(node: _NodeEntry, preset: dict) -> int:
|
||||
bytes_per_layer = _preset_bytes_per_layer(preset)
|
||||
quantization = _node_quantization(node, preset)
|
||||
layer_bytes = bytes_per_layer[quantization]
|
||||
if layer_bytes <= 0:
|
||||
return 0
|
||||
memory_budget_bytes, _ = _node_memory_budget_bytes(node)
|
||||
return int((memory_budget_bytes * 0.8) // layer_bytes)
|
||||
|
||||
|
||||
def _node_capacity_summary(node: _NodeEntry, preset: dict | None = None) -> dict:
|
||||
@@ -817,16 +832,17 @@ def _deployment_summary(nodes: list[_NodeEntry], preset: dict | None) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _max_layers_for_memory(memory_mb: int, total_layers: int, preset: dict | None = None) -> int:
|
||||
if total_layers <= 0:
|
||||
return 0
|
||||
if memory_mb <= 0:
|
||||
return max(1, total_layers // 2)
|
||||
bytes_per_layer = next(iter(_preset_bytes_per_layer(preset).values())) if preset is not None else 30 * 1024 * 1024
|
||||
return min(
|
||||
total_layers,
|
||||
max(1, int(((memory_mb * 1024 * 1024) * 0.8) // bytes_per_layer)),
|
||||
)
|
||||
def _max_layers_for_memory(memory_mb: int, total_layers: int, preset: dict | None = None) -> int:
|
||||
if total_layers <= 0:
|
||||
return 0
|
||||
if memory_mb <= 0:
|
||||
return max(1, total_layers // 2)
|
||||
memory_bytes = memory_mb * 1024 * 1024
|
||||
bytes_per_layer = next(iter(_preset_bytes_per_layer(preset).values())) if preset is not None else 30 * 1024 * 1024
|
||||
return min(
|
||||
total_layers,
|
||||
max(1, int((memory_bytes * 0.8) // bytes_per_layer)),
|
||||
)
|
||||
|
||||
|
||||
def _model_metadata_from_nodes(nodes: list[_NodeEntry]) -> dict:
|
||||
@@ -2330,17 +2346,24 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
# Pre-resolve the downstream route so the first-shard node skips its own
|
||||
# tracker query. We already hold the full registry picture — no need for
|
||||
# a second round-trip.
|
||||
route_model = node.hf_repo or node.model or model
|
||||
with server.lock:
|
||||
if route_model in server.model_presets:
|
||||
preset = server.model_presets[route_model]
|
||||
rs, re = _preset_layer_bounds(preset)
|
||||
all_nodes: list = [n for n in server.registry.values() if n.model == route_model]
|
||||
else:
|
||||
all_nodes = [
|
||||
n for n in server.registry.values()
|
||||
if _node_matches_model(n, route_model)
|
||||
and n.shard_start is not None and n.num_layers is not None
|
||||
route_model = node.hf_repo or node.model or model
|
||||
with server.lock:
|
||||
resolved_route_model, route_preset = _resolve_model_preset(server.model_presets, route_model)
|
||||
if route_preset is not None:
|
||||
route_model = resolved_route_model or route_model
|
||||
preset = route_preset
|
||||
rs, re = _preset_layer_bounds(preset)
|
||||
all_nodes: list = [
|
||||
n for n in server.registry.values()
|
||||
if _node_matches_preset(n, route_model, preset)
|
||||
and n.shard_start is not None
|
||||
and n.shard_end is not None
|
||||
]
|
||||
else:
|
||||
all_nodes = [
|
||||
n for n in server.registry.values()
|
||||
if _node_matches_model(n, route_model)
|
||||
and n.shard_start is not None and n.num_layers is not None
|
||||
]
|
||||
rs, re = 0, (max((n.num_layers for n in all_nodes), default=1) - 1)
|
||||
if pinned_nodes is not None:
|
||||
@@ -2356,6 +2379,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
route_model=route_model,
|
||||
error=route_error,
|
||||
candidate_count=len(all_nodes),
|
||||
candidates=_node_route_summary(all_nodes),
|
||||
)
|
||||
self._send_json(503, {"error": {
|
||||
"message": route_error,
|
||||
@@ -2393,6 +2417,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
head_endpoint=node.endpoint,
|
||||
downstream=downstream_urls,
|
||||
route=route_debug or "<empty>",
|
||||
nodes=_node_route_summary(route_nodes),
|
||||
)
|
||||
|
||||
req = urllib.request.Request(
|
||||
@@ -2450,12 +2475,25 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
in_tokens, out_tokens = _billable_non_stream_split(json.loads(body_text), body)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
in_tokens, out_tokens = 0, 0
|
||||
tokens = in_tokens + out_tokens
|
||||
self._record_observed_throughput(model, route_model, tokens, elapsed, route_nodes)
|
||||
self._bill_completed(
|
||||
api_key, model, tokens, node_work,
|
||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||
)
|
||||
tokens = in_tokens + out_tokens
|
||||
self._record_observed_throughput(model, route_model, tokens, elapsed, route_nodes)
|
||||
_tracker_log(
|
||||
server,
|
||||
"info",
|
||||
"proxy complete",
|
||||
request_id=request_id,
|
||||
model=model,
|
||||
route_model=route_model,
|
||||
status=int(first.get("status", 503)),
|
||||
tokens=tokens,
|
||||
elapsed_seconds=round(elapsed, 4),
|
||||
tokens_per_sec=round(tokens / elapsed, 4) if elapsed > 0 else 0.0,
|
||||
route=_node_route_summary(route_nodes),
|
||||
)
|
||||
self._bill_completed(
|
||||
api_key, model, tokens, node_work,
|
||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||
)
|
||||
return
|
||||
_tracker_log(
|
||||
server,
|
||||
@@ -2539,35 +2577,57 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
in_tokens, out_tokens = _stream_billable_split(
|
||||
observed_stream_tokens, stream_usage, body
|
||||
)
|
||||
self._record_observed_throughput(
|
||||
model, route_model, in_tokens + out_tokens, elapsed, route_nodes
|
||||
)
|
||||
self._bill_completed(
|
||||
api_key, model, in_tokens + out_tokens, node_work,
|
||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||
)
|
||||
else:
|
||||
# Non-streaming: buffer and relay
|
||||
resp_body = upstream.read()
|
||||
elapsed = time.monotonic() - started
|
||||
self._record_observed_throughput(
|
||||
model, route_model, in_tokens + out_tokens, elapsed, route_nodes
|
||||
)
|
||||
tokens = in_tokens + out_tokens
|
||||
_tracker_log(
|
||||
server,
|
||||
"info",
|
||||
"proxy complete",
|
||||
request_id=request_id,
|
||||
target_url=target_url,
|
||||
bytes=len(resp_body),
|
||||
model=model,
|
||||
route_model=route_model,
|
||||
status=200,
|
||||
stream=True,
|
||||
tokens=tokens,
|
||||
elapsed_seconds=round(elapsed, 4),
|
||||
tokens_per_sec=round(tokens / elapsed, 4) if elapsed > 0 else 0.0,
|
||||
route=_node_route_summary(route_nodes),
|
||||
)
|
||||
observed_output = ""
|
||||
try:
|
||||
response_payload = json.loads(resp_body)
|
||||
in_tokens, out_tokens = _billable_non_stream_split(response_payload, body)
|
||||
observed_output = _observed_output_from_non_stream_payload(response_payload)
|
||||
except json.JSONDecodeError:
|
||||
in_tokens, out_tokens = 0, 0
|
||||
tokens = in_tokens + out_tokens
|
||||
self._record_observed_throughput(model, route_model, tokens, elapsed, route_nodes)
|
||||
self._record_validation_event(request_id, model, body, observed_output, route_nodes)
|
||||
self._bill_completed(
|
||||
api_key, model, tokens, node_work,
|
||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||
)
|
||||
else:
|
||||
# Non-streaming: buffer and relay
|
||||
resp_body = upstream.read()
|
||||
elapsed = time.monotonic() - started
|
||||
observed_output = ""
|
||||
try:
|
||||
response_payload = json.loads(resp_body)
|
||||
in_tokens, out_tokens = _billable_non_stream_split(response_payload, body)
|
||||
observed_output = _observed_output_from_non_stream_payload(response_payload)
|
||||
except json.JSONDecodeError:
|
||||
in_tokens, out_tokens = 0, 0
|
||||
tokens = in_tokens + out_tokens
|
||||
self._record_observed_throughput(model, route_model, tokens, elapsed, route_nodes)
|
||||
_tracker_log(
|
||||
server,
|
||||
"info",
|
||||
"proxy complete",
|
||||
request_id=request_id,
|
||||
model=model,
|
||||
route_model=route_model,
|
||||
target_url=target_url,
|
||||
status=200,
|
||||
bytes=len(resp_body),
|
||||
tokens=tokens,
|
||||
elapsed_seconds=round(elapsed, 4),
|
||||
tokens_per_sec=round(tokens / elapsed, 4) if elapsed > 0 else 0.0,
|
||||
route=_node_route_summary(route_nodes),
|
||||
)
|
||||
self._record_validation_event(request_id, model, body, observed_output, route_nodes)
|
||||
self._bill_completed(
|
||||
api_key, model, tokens, node_work,
|
||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||
@@ -2740,13 +2800,30 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
in_tokens, out_tokens = _stream_billable_split(
|
||||
observed_stream_tokens, stream_usage, request_body
|
||||
)
|
||||
self._record_observed_throughput(
|
||||
model, route_model, in_tokens + out_tokens, elapsed, route_nodes
|
||||
)
|
||||
self._bill_completed(
|
||||
api_key, model, in_tokens + out_tokens, node_work,
|
||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||
)
|
||||
self._record_observed_throughput(
|
||||
model, route_model, in_tokens + out_tokens, elapsed, route_nodes
|
||||
)
|
||||
tokens = in_tokens + out_tokens
|
||||
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
||||
_tracker_log(
|
||||
server,
|
||||
"info",
|
||||
"proxy complete",
|
||||
request_id=session_id,
|
||||
model=model,
|
||||
route_model=route_model,
|
||||
status=200,
|
||||
stream=True,
|
||||
relay=True,
|
||||
tokens=tokens,
|
||||
elapsed_seconds=round(elapsed, 4),
|
||||
tokens_per_sec=round(tokens / elapsed, 4) if elapsed > 0 else 0.0,
|
||||
route=_node_route_summary(route_nodes),
|
||||
)
|
||||
self._bill_completed(
|
||||
api_key, model, tokens, node_work,
|
||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||
)
|
||||
|
||||
def _send_relayed_response(self, response: dict) -> None:
|
||||
status = int(response.get("status", 503))
|
||||
@@ -4140,11 +4217,12 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
"shard_end": shard_end,
|
||||
"num_layers": total_l,
|
||||
"gap_found": True,
|
||||
"price_per_token": 0.0,
|
||||
"deployment": _deployment_summary(pool_with_caller, preset),
|
||||
"model_sources": self._model_sources(
|
||||
str(resolved_name),
|
||||
preset,
|
||||
"price_per_token": 0.0,
|
||||
"bytes_per_layer": _preset_bytes_per_layer(preset),
|
||||
"deployment": _deployment_summary(pool_with_caller, preset),
|
||||
"model_sources": self._model_sources(
|
||||
str(resolved_name),
|
||||
preset,
|
||||
shard_start,
|
||||
shard_end,
|
||||
),
|
||||
@@ -4236,12 +4314,13 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
"shard_start": shard_start,
|
||||
"shard_end": shard_end,
|
||||
"num_layers": total_l,
|
||||
"gap_found": gap_found,
|
||||
"price_per_token": 0.0,
|
||||
"model_sources": self._model_sources(
|
||||
str(resolved_name or best_repo),
|
||||
best_preset or {"hf_repo": best_repo},
|
||||
shard_start,
|
||||
"gap_found": gap_found,
|
||||
"price_per_token": 0.0,
|
||||
"bytes_per_layer": _preset_bytes_per_layer(best_preset) if best_preset is not None else None,
|
||||
"model_sources": self._model_sources(
|
||||
str(resolved_name or best_repo),
|
||||
best_preset or {"hf_repo": best_repo},
|
||||
shard_start,
|
||||
shard_end,
|
||||
),
|
||||
})
|
||||
@@ -4287,10 +4366,20 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
if not node.wallet_address or not server.contracts.registry.get_wallet(node.wallet_address).banned
|
||||
]
|
||||
|
||||
route, error = _select_route(alive, required_start, required_end, contracts=server.contracts)
|
||||
if error:
|
||||
self._send_json(503, {"error": error})
|
||||
return
|
||||
route, error = _select_route(alive, required_start, required_end, contracts=server.contracts)
|
||||
if error:
|
||||
_tracker_log(
|
||||
server,
|
||||
"warn",
|
||||
"route unavailable",
|
||||
model=model,
|
||||
route_model=resolved_name or model,
|
||||
error=error,
|
||||
candidate_count=len(alive),
|
||||
candidates=_node_route_summary(alive),
|
||||
)
|
||||
self._send_json(503, {"error": error})
|
||||
return
|
||||
|
||||
covered_up_to = required_start - 1
|
||||
route_with_start: list[tuple] = []
|
||||
|
||||
Reference in New Issue
Block a user