Repository navigation
Two-rank watchdog deadlines are asymmetric: a legal >300 s prefill kills both ranks (follower waits a fixed TF_TP_TIMEOUT_S while the leader's allowance scales with prompt length) #665
Description
Two-rank watchdog: the follower waits a fixed 300 s while the leader's prefill allowance scales with prompt length — a legal long prefill kills both ranks
Summary
On a 2-rank CUDA serve (two DGX Sparks), a legal, merely slow batched long-context prefill takes the
whole service down: rank 1 declares the peer lost and _exit(3)s, and rank 0 exits with it.
The two deadlines in Watch (in zig-flashnext/patches/0006-two-ranks.patch) are asymmetric:
| side | deadline | source |
|---|---|---|
| follower (rank 1) waiting for rank 0's next control record | fixed base_s = TF_TP_TIMEOUT_S, default 300 s |
`if (r.watch) |
| leader (rank 0) inside one prefill call | max(seconds, base_s) where seconds = prompt_len / 400 (single) or sum(prompt_len) / 400 (batched) |
self.arm(@max(seconds, w.base_s)) |
So a single 308k-token prompt grants rank 0 ≈ 770 s (a prompt's chunks: 300 s, or 2.5 ms a token past that),
and a batch of 6–7 long prompts grants it well over a thousand seconds — while rank 1 gives up after 300 s.
Any prefill that legitimately needs > 300 s of silence therefore reads as "the peer is dead".
Environment
- two DGX Spark (GB10), driver 580.173.02, CUDA 13.3 forward-compat container
- TensorFold build
db281878ddb8, patchesf3e1300f4f62(recipe: MiaAI-Lab Qwen3.8-Flash-Dual-DGX-Sparks-TensorFold) tensorfold-native serve … --tp 2 --context 1048576 --parallel 16 --max-tokens 32768 --kv-dtype fp8 --thinking --vision
Evidence (two occurrences, same signature, same day)
Rank logs (docker logs --timestamps) around each exit:
rank 1: error: rank 1: a call passed its deadline (TF_TP_TIMEOUT_S): the other rank stalled; exiting
rank 0: error: rank 0: release not sent to rank 1 (FollowerGone) (a handful, all on long prompts)
rank 0: error: rank 0: the other rank closed the control link (it exited, or the link is down); exiting
| # | rank 0 started | longest request that failed | its prompt | that request started | rank 1 deadline fired | container life |
|---|---|---|---|---|---|---|
| A | 11:41:58Z | after=300.39s (LinkWriteFailed) |
307953 | 11:56:32Z | 12:01:32.676Z | 1175 s |
| B | 12:25:48Z | after=300.40s (LinkWriteFailed) |
307965 | 12:32:39Z | 12:37:39.874Z | 711 s |
Both times the deadline fired ~300 s after a ~308k-token prompt arrived, and the failing prompt sizes differ
by single digits between runs (the same long agent sessions were replayed after each crash), i.e. this is
reproducible, not a flake. ExitCode=3, OOMKilled=false (the exit is abortAndExit's std.c._exit(3)).
Ruled out on the hardware side: clocks 2197 MHz at P0 with nvidia-smi -lgc effective (no low-power lock);
rank 0's log has no ≥5 s gap and keeps answering /health, /metrics, /v1/models with 200 until the last
second (the HTTP plane is fine — only the TP control plane goes silent); no OOM, no NVRM/Xid in either window.
Suggested fix
Make the follower's wait scale the same way the leader's allowance does — the follower can only know the size of
the call it is waiting on if the leader tells it (the prefill / prefill_many record already carries
prompt_len per stream), so either
- arm the follower's wait from the same
sum(prompt_len)/400value the leader uses, or - have the leader emit a periodic progress/heartbeat record during a long call (the follower's deadline resets on
every record, so a heartbeat alone fixes it without changing any timeout semantics).
The current coupling means the leader's allowed work and the follower's patience must be derived from the same
quantity; today they are not.
Workaround
TF_TP_TIMEOUT_S is read from the environment and reaches both ranks, so raising it (e.g. 900) lifts the
follower's wait and the leader's floor together. This is what we run now — it removes the failure but also widens
the window in which a genuinely dead peer is noticed.
Also noticed (minor, separate)
/healthlive.decode_tokens_per_second/live.prefill_tokens_per_secondread0.0for minutes at a time while
the GPU is busy (measured: 6 requests running,generation_tokens_running=2733, 25/25 one-second samples read
0.0), whilelive.connections/live.waitingupdate normally. Emitting the field only when it is known
(or a short smoothing window) would let monitoring distinguish "idle" from "no reading".README.md"API notes" lists a/statsendpoint; on this buildGET /statsreturns
404 {"error":{"message":"unknown path /stats"}}.- (Might be worth documenting)
start.sh's memory probe parsesfree, whose header is localized — under
LANG=zh_CN.UTF-8it reads an empty value and reportsERROR: only GiB memory available(note the missing
number) with exit 1. Reading/proc/meminfo(orLC_ALL=C free) would make it locale-independent; we worked
around it with anLC_ALL=Cwrapper.
Activity
ashhart commentedon Oct 11, 2026
Thank you, @Jenpo. The table shows the mismatch clearly. The follower gives up after a fixed 300 s, while the leader allows itself 2.5 ms a token past that, so any prefill that needs more than 300 s looks like a dead peer to the follower.
That
Watchcode isn't in TensorFold's tree. Your build is our public main atdb281878d(6 October) plus the recipe's patch series, andTF_TP_TIMEOUT_Sand both deadlines come from itspatches/0006-two-ranks.patch. Our main doesn't serve across two CUDA ranks yet (Flash Next on CUDA in Zig is #472, one GPU). @MiaAI-Lab, this one is for your recipe.When two-rank CUDA serving comes to our tree, the follower should wait as long as the leader allows itself. The simplest way is for the leader to send its allowance with each control record. A prefill past 300 s will be one of its tests.
ashhart commentedon Oct 15, 2026
Thank you, @Jenpo, for the clear table of the two deadlines.
They are in a recipe's patch series, not in this tree, so we are closing this here; @MiaAI-Lab has the details for that recipe. Two-rank CUDA serving comes to this tree through #853 and #855, and we have added your case to what they must pass: the follower waits as long as the leader allows, so a legal prefill past 300 s keeps both ranks up.
Posted by Ash's AI Assistant Synthena