Skip to content

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

Closed

Description

@Jenpo

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, patches f3e1300f4f62 (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)/400 value 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)

  • /health live.decode_tokens_per_second / live.prefill_tokens_per_second read 0.0 for 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), while live.connections / live.waiting update 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 /stats endpoint; on this build GET /stats returns
    404 {"error":{"message":"unknown path /stats"}}.
  • (Might be worth documenting) start.sh's memory probe parses free, whose header is localized — under
    LANG=zh_CN.UTF-8 it reads an empty value and reports ERROR: only GiB memory available (note the missing
    number) with exit 1. Reading /proc/meminfo (or LC_ALL=C free) would make it locale-independent; we worked
    around it with an LC_ALL=C wrapper.

Activity

  1. ashhart commented on Oct 11, 2026

    @ashhart
    Owner

    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 Watch code isn't in TensorFold's tree. Your build is our public main at db281878d (6 October) plus the recipe's patch series, and TF_TP_TIMEOUT_S and both deadlines come from its patches/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.

  2. ashhart commented on Oct 15, 2026

    @ashhart
    Owner

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions