Merge branch 'master' of https://git.d-popov.com/popov/neuron-tai
This commit is contained in:
@@ -42,3 +42,4 @@ Historical handoff note: `/mnt/c/Users/popov/Downloads/neuron-tai-alpha-handoff-
|
|||||||
- Verification: downloader/startup targeted subset passes (`pytest tests/test_node_startup.py -k "download_shard or same_shard"`). Full `tests/test_node_startup.py` has 46 passed and 4 unrelated Windows chmod/path separator failures.
|
- Verification: downloader/startup targeted subset passes (`pytest tests/test_node_startup.py -k "download_shard or same_shard"`). Full `tests/test_node_startup.py` has 46 passed and 4 unrelated Windows chmod/path separator failures.
|
||||||
- Live Windows confirmation: `meshnet-node start --tracker http://192.168.0.179:8080 --model Qwen3.6-35B-A3B` reuses `F:\_STORAGE\models\qwen3.6-35b-a3b`, prints `Cached at`, registers, and reaches ready as node `5gMLrmyB-26b1f8a4204a`.
|
- Live Windows confirmation: `meshnet-node start --tracker http://192.168.0.179:8080 --model Qwen3.6-35B-A3B` reuses `F:\_STORAGE\models\qwen3.6-35b-a3b`, prints `Cached at`, registers, and reaches ready as node `5gMLrmyB-26b1f8a4204a`.
|
||||||
- Follow-up fix: preset-model startup now starts the heartbeat thread after registration; without this, the node appeared briefly on the dashboard and was purged on first inference/route after heartbeat expiry. Tracker dashboard now has a "Console output" panel backed by `/v1/console` for node register/expiry, routing failures, and proxy events.
|
- Follow-up fix: preset-model startup now starts the heartbeat thread after registration; without this, the node appeared briefly on the dashboard and was purged on first inference/route after heartbeat expiry. Tracker dashboard now has a "Console output" panel backed by `/v1/console` for node register/expiry, routing failures, and proxy events.
|
||||||
|
- Qwen3.6-35B-A3B reserve-based split is expected: an 79 GB CPU node may be assigned layers 0-36, and a second node fills 37-39. Do not "fix" this by bypassing the 20% assignment reserve unless the shard-planning policy changes.
|
||||||
|
|||||||
@@ -193,22 +193,33 @@ def _configure_torch_threads(
|
|||||||
return active
|
return active
|
||||||
|
|
||||||
|
|
||||||
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:
|
if total_layers is None or total_layers <= 0 or memory_mb <= 0:
|
||||||
return 0
|
return 0
|
||||||
budget_bytes = memory_mb * 1024 * 1024
|
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
|
memory_gb = memory_mb / 1024
|
||||||
gb_str = f"{memory_gb:.1f} GB"
|
gb_str = f"{memory_gb:.1f} GB"
|
||||||
budget_quantization = "bfloat16" if quantization == "auto" else quantization
|
budget_quantization = "bfloat16" if quantization == "auto" else quantization
|
||||||
if total_layers is None or total_layers <= 0:
|
if total_layers is None or total_layers <= 0:
|
||||||
return f"Memory budget: {gb_str} {memory_source}; shard budget: unknown model layer count"
|
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)
|
# 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_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 ""
|
remaining_str = f"; {remaining_gb:.1f} GB remaining after full load" if remaining_gb > 1 else ""
|
||||||
return (
|
return (
|
||||||
@@ -218,6 +229,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:
|
def _post_json(url: str, payload: dict, timeout: float = 10.0) -> dict:
|
||||||
data = json.dumps(payload).encode()
|
data = json.dumps(payload).encode()
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
@@ -869,6 +897,7 @@ def run_startup(
|
|||||||
hf_repo: str | None = assignment.get("hf_repo")
|
hf_repo: str | None = assignment.get("hf_repo")
|
||||||
peers: list[dict] = assignment.get("peers", [])
|
peers: list[dict] = assignment.get("peers", [])
|
||||||
model_sources: list[dict] = [] if tracker_source_disabled else assignment.get("model_sources", [])
|
model_sources: list[dict] = [] if tracker_source_disabled else assignment.get("model_sources", [])
|
||||||
|
assignment_bytes_per_layer = _assignment_bytes_per_layer(assignment, quantization)
|
||||||
print(f" Shard: layers {shard_start}-{shard_end} of {assigned_model}", flush=True)
|
print(f" Shard: layers {shard_start}-{shard_end} of {assigned_model}", flush=True)
|
||||||
|
|
||||||
# 4. Download shard
|
# 4. Download shard
|
||||||
@@ -955,7 +984,7 @@ def run_startup(
|
|||||||
f"meshnet-node ready\n"
|
f"meshnet-node ready\n"
|
||||||
f" Wallet: {address}\n"
|
f" Wallet: {address}\n"
|
||||||
f" Shard: layers {shard_start}-{shard_end} ({assigned_model})\n"
|
f" Shard: layers {shard_start}-{shard_end} ({assigned_model})\n"
|
||||||
f" {_shard_budget_line(memory_budget_mb, memory_budget_source, assignment.get('model_layers_end', shard_end) + 1, quantization)}\n"
|
f" {_shard_budget_line(memory_budget_mb, memory_budget_source, assignment.get('model_layers_end', shard_end) + 1, quantization, bytes_per_layer=assignment_bytes_per_layer)}\n"
|
||||||
f" Endpoint: {endpoint}\n"
|
f" Endpoint: {endpoint}\n"
|
||||||
f" Node ID: {node_id}\n"
|
f" Node ID: {node_id}\n"
|
||||||
f" Hardware: {hw_str}\n"
|
f" Hardware: {hw_str}\n"
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
<section><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
|
<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><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>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>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -224,6 +225,48 @@ function renderThroughput(stats) {
|
|||||||
$("throughput").innerHTML = table(["node", "model", "tps (1h)", "samples"], rows);
|
$("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) {
|
function renderConsole(data) {
|
||||||
const events = (data && data.events) || [];
|
const events = (data && data.events) || [];
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
@@ -409,6 +452,7 @@ async function refresh() {
|
|||||||
renderFraud(wallets, summary);
|
renderFraud(wallets, summary);
|
||||||
renderStats(stats);
|
renderStats(stats);
|
||||||
renderThroughput(stats);
|
renderThroughput(stats);
|
||||||
|
renderInferenceHistory(consoleData);
|
||||||
renderConsole(consoleData);
|
renderConsole(consoleData);
|
||||||
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
|
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -681,6 +681,21 @@ def _select_route(
|
|||||||
return route, ""
|
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(
|
def _coverage_percentage(
|
||||||
nodes: list[_NodeEntry],
|
nodes: list[_NodeEntry],
|
||||||
required_start: int,
|
required_start: int,
|
||||||
@@ -822,10 +837,11 @@ def _max_layers_for_memory(memory_mb: int, total_layers: int, preset: dict | Non
|
|||||||
return 0
|
return 0
|
||||||
if memory_mb <= 0:
|
if memory_mb <= 0:
|
||||||
return max(1, total_layers // 2)
|
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
|
bytes_per_layer = next(iter(_preset_bytes_per_layer(preset).values())) if preset is not None else 30 * 1024 * 1024
|
||||||
return min(
|
return min(
|
||||||
total_layers,
|
total_layers,
|
||||||
max(1, int(((memory_mb * 1024 * 1024) * 0.8) // bytes_per_layer)),
|
max(1, int((memory_bytes * 0.8) // bytes_per_layer)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2332,10 +2348,17 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
# a second round-trip.
|
# a second round-trip.
|
||||||
route_model = node.hf_repo or node.model or model
|
route_model = node.hf_repo or node.model or model
|
||||||
with server.lock:
|
with server.lock:
|
||||||
if route_model in server.model_presets:
|
resolved_route_model, route_preset = _resolve_model_preset(server.model_presets, route_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)
|
rs, re = _preset_layer_bounds(preset)
|
||||||
all_nodes: list = [n for n in server.registry.values() if n.model == route_model]
|
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:
|
else:
|
||||||
all_nodes = [
|
all_nodes = [
|
||||||
n for n in server.registry.values()
|
n for n in server.registry.values()
|
||||||
@@ -2356,6 +2379,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
route_model=route_model,
|
route_model=route_model,
|
||||||
error=route_error,
|
error=route_error,
|
||||||
candidate_count=len(all_nodes),
|
candidate_count=len(all_nodes),
|
||||||
|
candidates=_node_route_summary(all_nodes),
|
||||||
)
|
)
|
||||||
self._send_json(503, {"error": {
|
self._send_json(503, {"error": {
|
||||||
"message": route_error,
|
"message": route_error,
|
||||||
@@ -2393,6 +2417,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
head_endpoint=node.endpoint,
|
head_endpoint=node.endpoint,
|
||||||
downstream=downstream_urls,
|
downstream=downstream_urls,
|
||||||
route=route_debug or "<empty>",
|
route=route_debug or "<empty>",
|
||||||
|
nodes=_node_route_summary(route_nodes),
|
||||||
)
|
)
|
||||||
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
@@ -2452,6 +2477,19 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
in_tokens, out_tokens = 0, 0
|
in_tokens, out_tokens = 0, 0
|
||||||
tokens = in_tokens + out_tokens
|
tokens = in_tokens + out_tokens
|
||||||
self._record_observed_throughput(model, route_model, tokens, elapsed, route_nodes)
|
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(
|
self._bill_completed(
|
||||||
api_key, model, tokens, node_work,
|
api_key, model, tokens, node_work,
|
||||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||||
@@ -2542,22 +2580,29 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
self._record_observed_throughput(
|
self._record_observed_throughput(
|
||||||
model, route_model, in_tokens + out_tokens, elapsed, route_nodes
|
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,
|
||||||
|
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),
|
||||||
|
)
|
||||||
self._bill_completed(
|
self._bill_completed(
|
||||||
api_key, model, in_tokens + out_tokens, node_work,
|
api_key, model, tokens, node_work,
|
||||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Non-streaming: buffer and relay
|
# Non-streaming: buffer and relay
|
||||||
resp_body = upstream.read()
|
resp_body = upstream.read()
|
||||||
elapsed = time.monotonic() - started
|
elapsed = time.monotonic() - started
|
||||||
_tracker_log(
|
|
||||||
server,
|
|
||||||
"info",
|
|
||||||
"proxy complete",
|
|
||||||
request_id=request_id,
|
|
||||||
target_url=target_url,
|
|
||||||
bytes=len(resp_body),
|
|
||||||
)
|
|
||||||
observed_output = ""
|
observed_output = ""
|
||||||
try:
|
try:
|
||||||
response_payload = json.loads(resp_body)
|
response_payload = json.loads(resp_body)
|
||||||
@@ -2567,6 +2612,21 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
in_tokens, out_tokens = 0, 0
|
in_tokens, out_tokens = 0, 0
|
||||||
tokens = in_tokens + out_tokens
|
tokens = in_tokens + out_tokens
|
||||||
self._record_observed_throughput(model, route_model, tokens, elapsed, route_nodes)
|
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._record_validation_event(request_id, model, body, observed_output, route_nodes)
|
||||||
self._bill_completed(
|
self._bill_completed(
|
||||||
api_key, model, tokens, node_work,
|
api_key, model, tokens, node_work,
|
||||||
@@ -2743,8 +2803,25 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
self._record_observed_throughput(
|
self._record_observed_throughput(
|
||||||
model, route_model, in_tokens + out_tokens, elapsed, route_nodes
|
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(
|
self._bill_completed(
|
||||||
api_key, model, in_tokens + out_tokens, node_work,
|
api_key, model, tokens, node_work,
|
||||||
input_tokens=in_tokens, output_tokens=out_tokens,
|
input_tokens=in_tokens, output_tokens=out_tokens,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4141,6 +4218,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
"num_layers": total_l,
|
"num_layers": total_l,
|
||||||
"gap_found": True,
|
"gap_found": True,
|
||||||
"price_per_token": 0.0,
|
"price_per_token": 0.0,
|
||||||
|
"bytes_per_layer": _preset_bytes_per_layer(preset),
|
||||||
"deployment": _deployment_summary(pool_with_caller, preset),
|
"deployment": _deployment_summary(pool_with_caller, preset),
|
||||||
"model_sources": self._model_sources(
|
"model_sources": self._model_sources(
|
||||||
str(resolved_name),
|
str(resolved_name),
|
||||||
@@ -4238,6 +4316,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
"num_layers": total_l,
|
"num_layers": total_l,
|
||||||
"gap_found": gap_found,
|
"gap_found": gap_found,
|
||||||
"price_per_token": 0.0,
|
"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(
|
"model_sources": self._model_sources(
|
||||||
str(resolved_name or best_repo),
|
str(resolved_name or best_repo),
|
||||||
best_preset or {"hf_repo": best_repo},
|
best_preset or {"hf_repo": best_repo},
|
||||||
@@ -4289,6 +4368,16 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
route, error = _select_route(alive, required_start, required_end, contracts=server.contracts)
|
route, error = _select_route(alive, required_start, required_end, contracts=server.contracts)
|
||||||
if error:
|
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})
|
self._send_json(503, {"error": error})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -355,6 +355,134 @@ def test_tracker_proxy_accepts_hf_model_alias_from_quickstart():
|
|||||||
assert response["choices"][0]["message"]["content"] == "56"
|
assert response["choices"][0]["message"]["content"] == "56"
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracker_proxy_routes_split_preset_nodes_by_alias():
|
||||||
|
"""The chat proxy must build routes from alias-matched preset nodes."""
|
||||||
|
|
||||||
|
class ChatHandler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, fmt, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path != "/v1/chat/completions":
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
self.rfile.read(length)
|
||||||
|
route_header = self.headers.get("X-Meshnet-Route", "[]")
|
||||||
|
body = json.dumps({
|
||||||
|
"choices": [{"message": {"content": route_header}}],
|
||||||
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
|
||||||
|
}).encode()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
head = http.server.HTTPServer(("127.0.0.1", 0), ChatHandler)
|
||||||
|
head_thread = threading.Thread(target=head.serve_forever, daemon=True)
|
||||||
|
head_thread.start()
|
||||||
|
tracker = TrackerServer(model_presets={
|
||||||
|
"qwen3.6-35b-a3b": {
|
||||||
|
"layers_start": 0,
|
||||||
|
"layers_end": 39,
|
||||||
|
"hf_repo": "unsloth/Qwen3.6-35B-A3B",
|
||||||
|
"aliases": ["Qwen3.6-35B-A3B"],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tracker_port = tracker.start()
|
||||||
|
try:
|
||||||
|
_post_json(
|
||||||
|
f"http://127.0.0.1:{tracker_port}/v1/nodes/register",
|
||||||
|
{"endpoint": f"http://127.0.0.1:{head.server_address[1]}",
|
||||||
|
"model": "qwen3.6-35b-a3b",
|
||||||
|
"hf_repo": "unsloth/Qwen3.6-35B-A3B",
|
||||||
|
"num_layers": 40,
|
||||||
|
"shard_start": 0,
|
||||||
|
"shard_end": 21,
|
||||||
|
"tracker_mode": True,
|
||||||
|
"hardware_profile": {},
|
||||||
|
"score": 1.0},
|
||||||
|
)
|
||||||
|
_post_json(
|
||||||
|
f"http://127.0.0.1:{tracker_port}/v1/nodes/register",
|
||||||
|
{"endpoint": "http://127.0.0.1:9199",
|
||||||
|
"model": "Qwen3.6-35B-A3B",
|
||||||
|
"hf_repo": "unsloth/Qwen3.6-35B-A3B",
|
||||||
|
"num_layers": 40,
|
||||||
|
"shard_start": 22,
|
||||||
|
"shard_end": 39,
|
||||||
|
"tracker_mode": True,
|
||||||
|
"hardware_profile": {},
|
||||||
|
"score": 1.0},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = _post_json(
|
||||||
|
f"http://127.0.0.1:{tracker_port}/v1/chat/completions",
|
||||||
|
{"model": "Qwen3.6-35B-A3B",
|
||||||
|
"messages": [{"role": "user", "content": "hi"}]},
|
||||||
|
)
|
||||||
|
console = _get_json(f"http://127.0.0.1:{tracker_port}/v1/console")
|
||||||
|
finally:
|
||||||
|
tracker.stop()
|
||||||
|
head.shutdown()
|
||||||
|
head.server_close()
|
||||||
|
head_thread.join(timeout=1.0)
|
||||||
|
|
||||||
|
route = json.loads(response["choices"][0]["message"]["content"])
|
||||||
|
assert route == [{"endpoint": "http://127.0.0.1:9199", "start_layer": 22}]
|
||||||
|
complete = [event for event in console["events"] if event["message"] == "proxy complete"][-1]
|
||||||
|
assert complete["fields"]["tokens"] == 2
|
||||||
|
assert complete["fields"]["tokens_per_sec"] >= 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracker_route_endpoint_routes_split_preset_nodes_by_alias():
|
||||||
|
tracker = TrackerServer(model_presets={
|
||||||
|
"qwen3.6-35b-a3b": {
|
||||||
|
"layers_start": 0,
|
||||||
|
"layers_end": 39,
|
||||||
|
"hf_repo": "unsloth/Qwen3.6-35B-A3B",
|
||||||
|
"aliases": ["Qwen3.6-35B-A3B"],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tracker_port = tracker.start()
|
||||||
|
try:
|
||||||
|
_post_json(
|
||||||
|
f"http://127.0.0.1:{tracker_port}/v1/nodes/register",
|
||||||
|
{"endpoint": "http://127.0.0.1:9101",
|
||||||
|
"model": "qwen3.6-35b-a3b",
|
||||||
|
"hf_repo": "unsloth/Qwen3.6-35B-A3B",
|
||||||
|
"num_layers": 40,
|
||||||
|
"shard_start": 0,
|
||||||
|
"shard_end": 21,
|
||||||
|
"tracker_mode": True,
|
||||||
|
"hardware_profile": {},
|
||||||
|
"score": 1.0},
|
||||||
|
)
|
||||||
|
_post_json(
|
||||||
|
f"http://127.0.0.1:{tracker_port}/v1/nodes/register",
|
||||||
|
{"endpoint": "http://127.0.0.1:9102",
|
||||||
|
"model": "Qwen3.6-35B-A3B",
|
||||||
|
"hf_repo": "unsloth/Qwen3.6-35B-A3B",
|
||||||
|
"num_layers": 40,
|
||||||
|
"shard_start": 22,
|
||||||
|
"shard_end": 39,
|
||||||
|
"tracker_mode": True,
|
||||||
|
"hardware_profile": {},
|
||||||
|
"score": 1.0},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = _get_json(
|
||||||
|
f"http://127.0.0.1:{tracker_port}/v1/route?model=Qwen3.6-35B-A3B"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
tracker.stop()
|
||||||
|
|
||||||
|
assert response["route"] == ["http://127.0.0.1:9101", "http://127.0.0.1:9102"]
|
||||||
|
assert [node["start_layer"] for node in response["nodes"]] == [0, 22]
|
||||||
|
|
||||||
|
|
||||||
def test_tracker_registration_node_id_includes_wallet_prefix_and_stable_suffix():
|
def test_tracker_registration_node_id_includes_wallet_prefix_and_stable_suffix():
|
||||||
tracker = TrackerServer()
|
tracker = TrackerServer()
|
||||||
tracker_port = tracker.start()
|
tracker_port = tracker.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user