md rework. new code
This commit is contained in:
@@ -42,7 +42,7 @@ 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.
|
- Qwen3.6-35B-A3B CPU runtime cap (2026-07-08): the old reserve-based split could assign an 79 GB CPU node layers 0-36, but real partial loading can exceed that budget and die without a Python traceback. Node startup now clips oversized CPU auto-assignments before loading, and tracker CPU assignment uses a stricter runtime headroom factor; do not revert this to the old 20% reserve-only policy.
|
||||||
- Route hardening: tracker chat proxy and `/v1/route` diagnostics now use alias-aware preset node matching for split Qwen3.6 routes; dashboard derives grouped inference history from proxy route/complete console events and shows observed TPS after completion.
|
- Route hardening: tracker chat proxy and `/v1/route` diagnostics now use alias-aware preset node matching for split Qwen3.6 routes; dashboard derives grouped inference history from proxy route/complete console events and shows observed TPS after completion.
|
||||||
- Live proxy hardening: model lookup trims outer whitespace before alias matching (`qwen3.6-35b-a3b ` resolves), and tracker route logs/dashboard queue depth combine heartbeat queue with tracker-local proxy in-flight counts so Postman-style bursts no longer show every selected route as queue `0`.
|
- Live proxy hardening: model lookup trims outer whitespace before alias matching (`qwen3.6-35b-a3b ` resolves), and tracker route logs/dashboard queue depth combine heartbeat queue with tracker-local proxy in-flight counts so Postman-style bursts no longer show every selected route as queue `0`.
|
||||||
- Split-shard streaming hardening: Qwen3.6-style distributed generation now emits SSE chunks token-by-token from the head node instead of buffering all generated text until completion. Tracker direct/relay stream proxy logs `proxy progress` with live tokens/TPS, dashboard Inference history shows currently processing requests with live TPS/tokens/queue, and relay stream completion no longer references an undefined `session_id`.
|
- Split-shard streaming hardening: Qwen3.6-style distributed generation now emits SSE chunks token-by-token from the head node instead of buffering all generated text until completion. Tracker direct/relay stream proxy logs `proxy progress` with live tokens/TPS, dashboard Inference history shows currently processing requests with live TPS/tokens/queue, and relay stream completion no longer references an undefined `session_id`.
|
||||||
|
|||||||
926
QUICKSTART.md
926
QUICKSTART.md
File diff suppressed because it is too large
Load Diff
@@ -197,12 +197,43 @@ def _max_assignable_layers(
|
|||||||
memory_mb: int,
|
memory_mb: int,
|
||||||
total_layers: int | None,
|
total_layers: int | None,
|
||||||
bytes_per_layer: int | None = None,
|
bytes_per_layer: int | None = None,
|
||||||
|
*,
|
||||||
|
safety_fraction: float = 0.8,
|
||||||
) -> int:
|
) -> 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
|
||||||
layer_bytes = bytes_per_layer or _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))
|
return min(total_layers, int((budget_bytes * safety_fraction) // layer_bytes))
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_shard_safety_fraction(device: str) -> float:
|
||||||
|
"""CPU partial loads need room for model skeletons, tokenizer, and allocator peaks."""
|
||||||
|
return 0.55 if device != "cuda" else 0.8
|
||||||
|
|
||||||
|
|
||||||
|
def _cap_auto_assigned_shard(
|
||||||
|
shard_start: int,
|
||||||
|
shard_end: int,
|
||||||
|
total_layers: int | None,
|
||||||
|
memory_mb: int,
|
||||||
|
bytes_per_layer: int | None,
|
||||||
|
device: str,
|
||||||
|
) -> tuple[int, bool, int]:
|
||||||
|
if bytes_per_layer is None or total_layers is None:
|
||||||
|
return shard_end, False, 0
|
||||||
|
max_layers = _max_assignable_layers(
|
||||||
|
memory_mb,
|
||||||
|
total_layers,
|
||||||
|
bytes_per_layer=bytes_per_layer,
|
||||||
|
safety_fraction=_runtime_shard_safety_fraction(device),
|
||||||
|
)
|
||||||
|
if max_layers <= 0:
|
||||||
|
return shard_end, False, max_layers
|
||||||
|
assigned_layers = shard_end - shard_start + 1
|
||||||
|
if assigned_layers <= max_layers:
|
||||||
|
return shard_end, False, max_layers
|
||||||
|
return min(total_layers - 1, shard_start + max_layers - 1), True, max_layers
|
||||||
|
|
||||||
|
|
||||||
def _format_shard_label(
|
def _format_shard_label(
|
||||||
@@ -226,13 +257,19 @@ def _shard_budget_line(
|
|||||||
total_layers: int | None,
|
total_layers: int | None,
|
||||||
quantization: str,
|
quantization: str,
|
||||||
bytes_per_layer: int | None = None,
|
bytes_per_layer: int | None = None,
|
||||||
|
safety_fraction: float = 0.8,
|
||||||
) -> str:
|
) -> 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, bytes_per_layer=bytes_per_layer)
|
max_layers = _max_assignable_layers(
|
||||||
|
memory_mb,
|
||||||
|
total_layers,
|
||||||
|
bytes_per_layer=bytes_per_layer,
|
||||||
|
safety_fraction=safety_fraction,
|
||||||
|
)
|
||||||
# Remaining capacity after one full model load (rough estimate)
|
# Remaining capacity after one full model load (rough estimate)
|
||||||
shard_bytes = max_layers * (bytes_per_layer or _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)
|
||||||
@@ -729,6 +766,25 @@ def run_startup(
|
|||||||
if net_asgn.get("hf_repo") == model_id and net_asgn.get("gap_found"):
|
if net_asgn.get("hf_repo") == model_id and net_asgn.get("gap_found"):
|
||||||
shard_start = net_asgn["shard_start"]
|
shard_start = net_asgn["shard_start"]
|
||||||
shard_end = net_asgn["shard_end"]
|
shard_end = net_asgn["shard_end"]
|
||||||
|
asgn_total_layers = int(net_asgn.get("num_layers") or detected)
|
||||||
|
asgn_bytes_per_layer = _assignment_bytes_per_layer(net_asgn, quantization)
|
||||||
|
capped_shard_end, was_capped, max_runtime_layers = _cap_auto_assigned_shard(
|
||||||
|
shard_start,
|
||||||
|
shard_end,
|
||||||
|
asgn_total_layers,
|
||||||
|
memory_budget_mb,
|
||||||
|
asgn_bytes_per_layer,
|
||||||
|
device,
|
||||||
|
)
|
||||||
|
if was_capped:
|
||||||
|
original_end = shard_end
|
||||||
|
shard_end = capped_shard_end
|
||||||
|
print(
|
||||||
|
f" WARNING: tracker assigned layers {shard_start}-{original_end}, "
|
||||||
|
f"but CPU-safe runtime budget fits {max_runtime_layers}/{asgn_total_layers} layers; "
|
||||||
|
f"loading layers {shard_start}-{shard_end} instead.",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
full_sources = (
|
full_sources = (
|
||||||
[] if tracker_source_disabled
|
[] if tracker_source_disabled
|
||||||
else _full_model_sources(net_asgn.get("model_sources", []))
|
else _full_model_sources(net_asgn.get("model_sources", []))
|
||||||
@@ -744,7 +800,7 @@ def run_startup(
|
|||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f" Tracker found uncovered shard: "
|
f" Tracker found uncovered shard: "
|
||||||
f"layers {shard_start}–{shard_end} (of {detected})",
|
f"layers {shard_start}-{shard_end} (of {detected})",
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -859,18 +915,36 @@ def run_startup(
|
|||||||
assigned_shard_start: int = net_assignment["shard_start"]
|
assigned_shard_start: int = net_assignment["shard_start"]
|
||||||
assigned_shard_end: int = net_assignment["shard_end"]
|
assigned_shard_end: int = net_assignment["shard_end"]
|
||||||
assigned_num_layers: int = net_assignment["num_layers"]
|
assigned_num_layers: int = net_assignment["num_layers"]
|
||||||
|
assigned_bytes_per_layer = _assignment_bytes_per_layer(net_assignment, quantization)
|
||||||
|
capped_shard_end, was_capped, max_runtime_layers = _cap_auto_assigned_shard(
|
||||||
|
assigned_shard_start,
|
||||||
|
assigned_shard_end,
|
||||||
|
assigned_num_layers,
|
||||||
|
memory_budget_mb,
|
||||||
|
assigned_bytes_per_layer,
|
||||||
|
device,
|
||||||
|
)
|
||||||
|
if was_capped:
|
||||||
|
original_end = assigned_shard_end
|
||||||
|
assigned_shard_end = capped_shard_end
|
||||||
|
print(
|
||||||
|
f" WARNING: tracker assigned layers {assigned_shard_start}-{original_end}, "
|
||||||
|
f"but CPU-safe runtime budget fits {max_runtime_layers}/{assigned_num_layers} layers; "
|
||||||
|
f"loading layers {assigned_shard_start}-{assigned_shard_end} instead.",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
assigned_model_sources: list[dict] = net_assignment.get("model_sources", [])
|
assigned_model_sources: list[dict] = net_assignment.get("model_sources", [])
|
||||||
if _gap_found:
|
if _gap_found:
|
||||||
print(
|
print(
|
||||||
f" Assigned gap: {assigned_hf_repo} "
|
f" Assigned gap: {assigned_hf_repo} "
|
||||||
f"layers {assigned_shard_start}–{assigned_shard_end} "
|
f"layers {assigned_shard_start}-{assigned_shard_end} "
|
||||||
f"(of {assigned_num_layers})",
|
f"(of {assigned_num_layers})",
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
f" Assigned redundant copy: {assigned_hf_repo} "
|
f" Assigned redundant copy: {assigned_hf_repo} "
|
||||||
f"layers {assigned_shard_start}–{assigned_shard_end} "
|
f"layers {assigned_shard_start}-{assigned_shard_end} "
|
||||||
f"(of {assigned_num_layers})",
|
f"(of {assigned_num_layers})",
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
@@ -956,7 +1030,7 @@ def run_startup(
|
|||||||
f" Wallet: {address}\n"
|
f" Wallet: {address}\n"
|
||||||
f" Model ID: {assigned_hf_repo}\n"
|
f" Model ID: {assigned_hf_repo}\n"
|
||||||
f" Shard: {shard_label}\n"
|
f" Shard: {shard_label}\n"
|
||||||
f" {_shard_budget_line(memory_budget_mb, memory_budget_source, assigned_num_layers, quantization)}\n"
|
f" {_shard_budget_line(memory_budget_mb, memory_budget_source, assigned_num_layers, quantization, bytes_per_layer=assigned_bytes_per_layer, safety_fraction=_runtime_shard_safety_fraction(device))}\n"
|
||||||
f" Quantization: {quantization}\n"
|
f" Quantization: {quantization}\n"
|
||||||
f" Endpoint: {endpoint}\n"
|
f" Endpoint: {endpoint}\n"
|
||||||
f" Node ID: {tracker_node_id or 'unregistered'}\n"
|
f" Node ID: {tracker_node_id or 'unregistered'}\n"
|
||||||
@@ -1022,6 +1096,7 @@ def run_startup(
|
|||||||
memory_budget_mb,
|
memory_budget_mb,
|
||||||
assigned_total_layers,
|
assigned_total_layers,
|
||||||
assignment_bytes_per_layer,
|
assignment_bytes_per_layer,
|
||||||
|
safety_fraction=_runtime_shard_safety_fraction(device),
|
||||||
)
|
)
|
||||||
if pinned_layers > max_layers:
|
if pinned_layers > max_layers:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -1115,7 +1190,7 @@ def run_startup(
|
|||||||
f" Wallet: {address}\n"
|
f" Wallet: {address}\n"
|
||||||
f" Model ID: {hf_repo}\n"
|
f" Model ID: {hf_repo}\n"
|
||||||
f" Shard: {shard_label}\n"
|
f" Shard: {shard_label}\n"
|
||||||
f" {_shard_budget_line(memory_budget_mb, memory_budget_source, total_layers, quantization, bytes_per_layer=assignment_bytes_per_layer)}\n"
|
f" {_shard_budget_line(memory_budget_mb, memory_budget_source, total_layers, quantization, bytes_per_layer=assignment_bytes_per_layer, safety_fraction=_runtime_shard_safety_fraction(device))}\n"
|
||||||
f" Quantization: {quantization}\n"
|
f" Quantization: {quantization}\n"
|
||||||
f" Endpoint: {endpoint}\n"
|
f" Endpoint: {endpoint}\n"
|
||||||
f" Node ID: {tracker_node_id or 'unregistered'}\n"
|
f" Node ID: {tracker_node_id or 'unregistered'}\n"
|
||||||
@@ -1192,7 +1267,7 @@ def run_startup(
|
|||||||
f"meshnet-node ready\n"
|
f"meshnet-node ready\n"
|
||||||
f" Wallet: {address}\n"
|
f" Wallet: {address}\n"
|
||||||
f" Shard: {shard_label}\n"
|
f" Shard: {shard_label}\n"
|
||||||
f" {_shard_budget_line(memory_budget_mb, memory_budget_source, assigned_total_layers, quantization, bytes_per_layer=assignment_bytes_per_layer)}\n"
|
f" {_shard_budget_line(memory_budget_mb, memory_budget_source, assigned_total_layers, quantization, bytes_per_layer=assignment_bytes_per_layer, safety_fraction=_runtime_shard_safety_fraction(device))}\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"
|
||||||
|
|||||||
@@ -1275,16 +1275,23 @@ 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:
|
def _max_layers_for_memory(
|
||||||
|
memory_mb: int,
|
||||||
|
total_layers: int,
|
||||||
|
preset: dict | None = None,
|
||||||
|
*,
|
||||||
|
device: str | None = None,
|
||||||
|
) -> int:
|
||||||
if total_layers <= 0:
|
if total_layers <= 0:
|
||||||
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
|
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
|
||||||
|
safety_fraction = 0.55 if device == "cpu" else 0.8
|
||||||
return min(
|
return min(
|
||||||
total_layers,
|
total_layers,
|
||||||
max(1, int((memory_bytes * 0.8) // bytes_per_layer)),
|
max(1, int((memory_bytes * safety_fraction) // bytes_per_layer)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -5273,7 +5280,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
required_start, required_end = _preset_layer_bounds(preset)
|
required_start, required_end = _preset_layer_bounds(preset)
|
||||||
total_l = required_end - required_start + 1
|
total_l = required_end - required_start + 1
|
||||||
memory_mb = vram_mb if (device == "cuda" and vram_mb > 0) else ram_mb
|
memory_mb = vram_mb if (device == "cuda" and vram_mb > 0) else ram_mb
|
||||||
max_layers = _max_layers_for_memory(memory_mb, total_l, preset)
|
max_layers = _max_layers_for_memory(memory_mb, total_l, preset, device=device)
|
||||||
shard_start = required_start
|
shard_start = required_start
|
||||||
shard_end = min(required_end, shard_start + max_layers - 1)
|
shard_end = min(required_end, shard_start + max_layers - 1)
|
||||||
self._send_json(200, {
|
self._send_json(200, {
|
||||||
@@ -5373,11 +5380,11 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
memory_mb = vram_mb if (device == "cuda" and vram_mb > 0) else ram_mb
|
memory_mb = vram_mb if (device == "cuda" and vram_mb > 0) else ram_mb
|
||||||
resolved_name, best_preset = _resolve_model_preset(server.model_presets, str(best_repo))
|
resolved_name, best_preset = _resolve_model_preset(server.model_presets, str(best_repo))
|
||||||
if memory_mb > 0:
|
if memory_mb > 0:
|
||||||
max_layers = _max_layers_for_memory(memory_mb, total_l, best_preset)
|
max_layers = _max_layers_for_memory(memory_mb, total_l, best_preset, device=device)
|
||||||
elif device == "cuda" and vram_mb >= 8192:
|
elif device == "cuda" and vram_mb >= 8192:
|
||||||
max_layers = total_l
|
max_layers = total_l
|
||||||
else:
|
else:
|
||||||
max_layers = _max_layers_for_memory(memory_mb, total_l, best_preset)
|
max_layers = _max_layers_for_memory(memory_mb, total_l, best_preset, device=device)
|
||||||
|
|
||||||
shard_start = best_gap_start
|
shard_start = best_gap_start
|
||||||
shard_end = min(total_l - 1, shard_start + max_layers - 1)
|
shard_end = min(total_l - 1, shard_start + max_layers - 1)
|
||||||
|
|||||||
@@ -1680,6 +1680,66 @@ def test_preset_startup_rejects_pinned_shard_above_memory_budget(tmp_path, monke
|
|||||||
tracker.stop()
|
tracker.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_network_auto_join_clips_oversized_cpu_assignment(tmp_path, monkeypatch, capsys):
|
||||||
|
"""Old trackers may assign too many CPU layers; node clips before model load."""
|
||||||
|
import meshnet_node.startup as startup_mod
|
||||||
|
|
||||||
|
torch_calls: list[dict] = []
|
||||||
|
registrations: list[dict] = []
|
||||||
|
|
||||||
|
class FakeBackend:
|
||||||
|
total_layers = 40
|
||||||
|
|
||||||
|
class FakeTorchNodeServer:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
torch_calls.append(kwargs)
|
||||||
|
self.backend = FakeBackend()
|
||||||
|
self.tracker_node_id = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
return 7000
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
oversized_assignment = {
|
||||||
|
"hf_repo": "unsloth/Qwen3.6-35B-A3B",
|
||||||
|
"model": "qwen3.6-35b-a3b",
|
||||||
|
"shard_start": 0,
|
||||||
|
"shard_end": 36,
|
||||||
|
"num_layers": 40,
|
||||||
|
"gap_found": False,
|
||||||
|
"bytes_per_layer": {"bfloat16": 1_797_594_419},
|
||||||
|
"model_sources": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
startup_mod,
|
||||||
|
"detect_hardware",
|
||||||
|
lambda: {"device": "cpu", "gpu_name": None, "vram_mb": 0, "ram_mb": 79 * 1024},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(startup_mod, "TorchNodeServer", FakeTorchNodeServer)
|
||||||
|
monkeypatch.setattr(startup_mod, "_get_json", lambda *_args, **_kwargs: oversized_assignment)
|
||||||
|
monkeypatch.setattr(startup_mod, "_post_json", lambda _url, payload: registrations.append(payload) or {"node_id": "n1"})
|
||||||
|
monkeypatch.setattr(startup_mod, "_start_heartbeat", lambda *_args, **_kwargs: None)
|
||||||
|
monkeypatch.setattr(startup_mod, "model_metadata_for", lambda *_args, **_kwargs: {"num_layers": 40})
|
||||||
|
|
||||||
|
node = run_startup(
|
||||||
|
tracker_url="http://127.0.0.1:8080",
|
||||||
|
wallet_path=tmp_path / "wallet.json",
|
||||||
|
tracker_source_disabled=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
assert torch_calls[0]["shard_start"] == 0
|
||||||
|
assert torch_calls[0]["shard_end"] == 24
|
||||||
|
assert registrations[0]["shard_end"] == 24
|
||||||
|
output = capsys.readouterr().out
|
||||||
|
assert "CPU-safe runtime budget fits 25/40 layers" in output
|
||||||
|
assert "layers 0-24" in output
|
||||||
|
finally:
|
||||||
|
node.stop()
|
||||||
|
|
||||||
|
|
||||||
def test_preset_model_with_hf_repo_loads_torch_backend(tmp_path, monkeypatch, capsys):
|
def test_preset_model_with_hf_repo_loads_torch_backend(tmp_path, monkeypatch, capsys):
|
||||||
"""Named presets that advertise hf_repo must load TorchNodeServer, not the stub server."""
|
"""Named presets that advertise hf_repo must load TorchNodeServer, not the stub server."""
|
||||||
import meshnet_node.startup as startup_mod
|
import meshnet_node.startup as startup_mod
|
||||||
@@ -1996,6 +2056,55 @@ def test_network_assign_gap_found_field():
|
|||||||
tracker.stop()
|
tracker.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_network_assign_uses_conservative_cpu_runtime_budget():
|
||||||
|
"""CPU assignments leave headroom for partial-load overhead, not just raw weights."""
|
||||||
|
import json as _json
|
||||||
|
import urllib.request as _ur
|
||||||
|
|
||||||
|
tracker = TrackerServer(model_presets={
|
||||||
|
"qwen3.6-35b-a3b": {
|
||||||
|
"hf_repo": "unsloth/Qwen3.6-35B-A3B",
|
||||||
|
"aliases": ["unsloth/Qwen3.6-35B-A3B"],
|
||||||
|
"layers_start": 0,
|
||||||
|
"layers_end": 39,
|
||||||
|
"recommended": True,
|
||||||
|
"bytes_per_layer": {"bfloat16": 1_797_594_419},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
port = tracker.start()
|
||||||
|
try:
|
||||||
|
data = _json.dumps({
|
||||||
|
"endpoint": "http://127.0.0.1:9200",
|
||||||
|
"model": "qwen3.6-35b-a3b",
|
||||||
|
"hf_repo": "unsloth/Qwen3.6-35B-A3B",
|
||||||
|
"num_layers": 40,
|
||||||
|
"shard_start": 0,
|
||||||
|
"shard_end": 39,
|
||||||
|
"hardware_profile": {},
|
||||||
|
"score": 1.0,
|
||||||
|
}).encode()
|
||||||
|
req = _ur.Request(
|
||||||
|
f"http://127.0.0.1:{port}/v1/nodes/register",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with _ur.urlopen(req) as r:
|
||||||
|
r.read()
|
||||||
|
|
||||||
|
resp = _get_json(
|
||||||
|
f"http://127.0.0.1:{port}/v1/network/assign"
|
||||||
|
"?device=cpu&vram_mb=0&ram_mb=80896"
|
||||||
|
"&hf_repo=unsloth/Qwen3.6-35B-A3B"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp["gap_found"] is False
|
||||||
|
assert resp["shard_start"] == 0
|
||||||
|
assert resp["shard_end"] == 24
|
||||||
|
finally:
|
||||||
|
tracker.stop()
|
||||||
|
|
||||||
|
|
||||||
def test_route_finds_hf_model_across_two_nodes():
|
def test_route_finds_hf_model_across_two_nodes():
|
||||||
"""Tracker /v1/route returns ordered route for HF model even without a preset."""
|
"""Tracker /v1/route returns ordered route for HF model even without a preset."""
|
||||||
import json as _json
|
import json as _json
|
||||||
|
|||||||
Reference in New Issue
Block a user