85 lines
4.5 KiB
Markdown
85 lines
4.5 KiB
Markdown
# US-047 — Tracker-first model downloads: visibility, sane timeouts, RAM-based sizing
|
||
|
||
Status: in progress
|
||
Priority: High (follow-up to US-044/US-046; blocks usable LAN downloads)
|
||
|
||
## Context
|
||
|
||
Reported 2026-07-06 (Windows CPU node, 79.2 GB RAM, `--tracker
|
||
http://192.168.0.179:8080 --model Qwen3.6-35B-A3B`):
|
||
|
||
1. Startup prints `(auto-join unavailable: HTTP Error 503)` even though the
|
||
user explicitly named a model. The auto-join query (`/v1/network/assign`)
|
||
never sends the requested model, so a fresh tracker + a caller too small
|
||
for the *recommended* preset 503s (expected per US-046) — but the whole
|
||
auto-join step is pointless when the user already picked a model: the
|
||
`/v1/nodes/assign?model=…` call right after it succeeds (assigned layers
|
||
0–2 with tracker `model_sources`).
|
||
2. The tracker-vs-HuggingFace race then starts, but only HuggingFace shows
|
||
progress (hf tqdm bars). The tracker tar download prints nothing and
|
||
swallows every failure (`except Exception: return None`), so the node
|
||
*appears* to download only from slow HF; the user killed it. Tracker-side
|
||
log showed the tar stream reset mid-`archive.add` — with no way to tell
|
||
whether the client timed out or the user aborted.
|
||
3. `_download_model_source` inherits `peer_timeout` (2.0 s) as its urlopen
|
||
socket timeout. Any 2 s read stall during a multi-GB tar stream silently
|
||
kills the tracker source and leaves HF as the only contender.
|
||
4. Every client abort spams the tracker console with a full
|
||
`BrokenPipeError`/`ConnectionResetError` traceback from `socketserver`.
|
||
|
||
## Fix
|
||
|
||
1. `startup.py`: skip the network auto-join query entirely when a model was
|
||
explicitly requested (`model` set and not `"stub-model"`); path 3b
|
||
(`/v1/nodes/assign?model=…`) is the authoritative one there.
|
||
2. `downloader.py`: model-source downloads get their own timeout constant
|
||
(30 s socket timeout) instead of the 2 s peer-probe timeout. Peer shard
|
||
downloads keep 2 s — they run sequentially before the race, and a dead
|
||
peer must not hang startup for 30 s; the race is concurrent so a slow
|
||
source costs nothing.
|
||
3. `downloader.py`: progress + failure visibility for the race —
|
||
`_download_model_source` prints received bytes every 512 MB and prints
|
||
the exception when a source fails, so "downloads only from HF" can never
|
||
happen silently again.
|
||
4. Tracker `_handle_model_files_download`: catch
|
||
`BrokenPipeError`/`ConnectionResetError` around the tar stream and log a
|
||
single line instead of a traceback.
|
||
|
||
## Design revision (2026-07-06, after live retest)
|
||
|
||
The race is gone. User decision: **HuggingFace is used only when the model is
|
||
not available from a tracker/peer source, or when `--tracker-source-disabled`
|
||
is passed.** Sources are tried sequentially with progress + failure output;
|
||
HF (layer-filtered via the source file list, else the remote index) is the
|
||
fallback.
|
||
|
||
Second live finding: the node was assigned only layers 0–2 of 40 on a 79 GB
|
||
box. Cause: CPU-mode nodes still report the detected-but-unusable GPU's
|
||
`vram_mb` (RTX 4060 → 8192), and shard sizing used VRAM whenever it was > 0
|
||
(8 GB × 0.8 ≈ 6.5 GB ≈ 3 layers). Fixed on both sides: the node now sends
|
||
`assignment_vram_mb` (0 unless CUDA is actually usable) to `/v1/nodes/assign`,
|
||
and the tracker only trusts `vram_mb` when `device=cuda` (all three sizing
|
||
sites), falling back to `ram_mb`.
|
||
|
||
## Acceptance criteria
|
||
|
||
- [x] Node started with an explicit `--model` never queries
|
||
`/v1/network/assign` and never prints `auto-join unavailable`.
|
||
- [x] Tracker/peer model source is preferred outright; HF is contacted only
|
||
when no source is advertised, every source fails, or
|
||
`--tracker-source-disabled` is passed (flag on both CLI parsers, plumbed
|
||
through config and `run_startup`).
|
||
- [x] Tracker-source downloads print progress every 512 MB and print the
|
||
exception + URL on failure; nothing fails silently.
|
||
- [x] A ≥2 s read stall no longer aborts a tracker model-source download
|
||
(30 s socket timeout).
|
||
- [x] Client disconnect during `/v1/model-files/download` logs one line on
|
||
the tracker, no traceback.
|
||
- [x] CPU node with big RAM gets a RAM-sized shard: `/v1/nodes/assign` and
|
||
both `/v1/network/assign` sizing paths ignore VRAM unless `device=cuda`.
|
||
- [x] `pytest tests/test_node_startup.py tests/test_tracker_routing.py`
|
||
passes (139/140; the one failure is the pre-existing port-dependent
|
||
`test_mining_cli` case, present on clean master).
|
||
- [ ] Live two-machine retest: Windows node downloads only from tracker at
|
||
LAN speed and is assigned a RAM-sized shard.
|