Repository navigation
flashnext cuda: the opt-in --tp 2 decode graph (TF_FLASHNEXT_DECODE_GRAPH=1) serves again instead of failing its first plain round - #2980
Conversation
98c94ab to
dc9e410
Compare
dc9e410 to
2c3d7e4
Compare
|
Thanks @swbiggart, for the fix and for tracing #2974 to its cause. The fix is right:
Thanks for the rebase. One thing before it lands: it and #2977 overlap, and #2977 lands first (after #2976, which it's stacked on). Once #2977 is on main, please rebase onto it. Three spots will conflict:
This and #2978 both edit Thanks also for the honest notes on what you couldn't run.
|
|
Thanks. Once #2977 is on main I'll rebase this onto it:
The same goes for |
…e capture TF_FLASHNEXT_DECODE_GRAPH=1 at --tp 2 failed every first plain round with UnownedCaptureSource: Cache.run set only `prepared`, so Pass.chunk uploaded the round's token ids through the copy-source owner inside the capture, which refuses any host source there. The round now queues its ids, positions, lane table and n-gram rows on the stream with stageWindow, as the one-GPU shared windows do, then captures or replays kernels and collectives alone; an unstaged pass is refused before any capture. A replay stages every round's inputs afresh, the lane table included.
A graph that captured NCCL collectives holds persistent plans, and ncclCommAbort waits without a bound until every such graph is destroyed (commDestroySync polls localPersistentRefs). With the round graph held by the engine, a peer lost during a replay, a capture or between rounds would hang the surviving rank in the abort instead of returning RankDisconnected or RankTimeout. The cache that captures a communicator's collectives now registers a release that the abort runs first: it ends an open capture, frees its graph and destroys the executable, so NCCL's abort can drain and return.
… eager A round of taught and untaught streams scales its pocket rows through a buffer rowScales allocates anew every round, which the round graph's key does not cover, so a replay would read the freed one. Like the window graphs, the round graph now leaves pockets out: with any pocket loaded both ranks run their rounds eagerly (the mask is the checkpoint's, so the ranks agree).
…and a lost peer Each of 52 two-rank rounds (one lane, two lanes at different positions, the other owner order, the 512-key seam) must equal an eager one-row forward of a twin stream byte for byte; with TF_FLASHNEXT_DECODE_GRAPH=1 every round must take the graph and the one-lane rounds must replay the first one's executable. `leave` decodes until the peer is stopped from outside: after four rounds it must hold the graph exactly when it is on, and the leader must then stop with RankDisconnected or RankTimeout rather than wait (the run's own timeout bounds a hang). The context is 1024 so the rounds cross 512 keys.
cuda_round_graph.zig stages its round before capture, cuda_collective.zig runs the round graph's release before an abort, and cuda_prompt.zig leaves rounds with pockets eager; all three reach only the opt-in TF_FLASHNEXT_DECODE_GRAPH path, so test-gradient-serving follows the new hashes.
It also puts the pull request number on the notes of the three rows re-pinned in gradient-serving.tsv.
2c3d7e4 to
0ddafbc
Compare
|
Rebased onto main
One more test line: #2977's
|
|
Thanks @swbiggart. The rebase preserves This now waits on one final TP=2 graph-on/off run that we'll schedule on two GB10s, with eager-byte equality and bounded peer failure. No action needed from you. We'll keep the abort-release ordering when combining this with #2978 and #2982.
|
What this changes
Before: with the opt-in
TF_FLASHNEXT_DECODE_GRAPH=1(which needsTF_FLASHNEXT_SPLIT_DENSE=0) at--tp 2, every first plain round failed on both ranks withUnownedCaptureSourceand took the engine down (#2974).Cache.runset onlypass.prepared, so the round's token ids were uploaded inside the capture, where the copy-source owner refuses host sources (since f630f1a).After: a round stages its inputs on the stream with
stageWindow, as the one-GPU shared windows do, and the graph captures or replays only kernels and collectives. Also: the communicator's abort releases the round graph beforencclCommAbort, which waits without a bound on a graph holding NCCL plans, so a lost peer would hang the other rank; and a folder with learned pockets stays eager, becauserowScalesallocates a new buffer every round. The default path is unchanged: both sit behind a round-graph capture or the opt-in.--tp 2work).4cfd95573. Six commits: staging, abort release, pocket guard, device test,gradient-serving.tsvre-pins, and0ddafbc80, the changelog line (it also numbers the re-pinned rows' notes).atc overlap --json(ATC 0.9.6, 8 changed files,complete: false) on46eb7bb30over main93a2e93ec, conflicts checked withgit merge-treeagainst both:CHANGELOG.md: main now has## Unreleased, which that run saw this PR and llama on Metal: Qwen2 checkpoints, Bimo-7B-CyberAgent's bf16 release with its q, k and v biases and YaRN at HF's rounding points #2890 re-add; other PRs' lines there conflict.preparelines this PR deletes; the EXL3 stack (EXL3 Flash Next on CUDA: one rank, offloaded experts, sm_120 gate (1/8) #1441 to EXL3 Flash Next: draft lane guard for offloaded experts (8/8) #1446, EXL3 Flash Next: host-resident K/V on the tier (6/8) #1522) in four of this PR's files.gradient-serving.tsvrows and already conflict with main. flashnext: refuse a malformed TF_FLASHNEXT_* setting by name before device work #2585 (merged) changed how the setting is read, not the capture.atc overlap --jsonat 2026-11-12 14:09 UTC on head2c3d7e487; everyconflictsentry is named in this section, or adds no conflict beyond those it already has with main (1), or conflicts only inCHANGELOG.md's## Unreleasedlines, as this section describes (6); each checked withgit merge-tree.Checks on this head
0ddafbc80on base4cfd95573(main). Receipts were measured on46eb7bb30(served cells onfc6f58fbb, only tests differ) and stand on this head: this PR's own lines are unchanged since apart from thecuda_round_graph.zigpin row, re-pinned to the merged file; the rebase ontoeab305db0brought in flashnext: refuse a malformed TF_FLASHNEXT_* setting by name before device work #2585, which changes howTF_FLASHNEXT_*settings are read, with the same defaults, and the rebases onto4f8317dacand000b534a2changed only where theCHANGELOG.mdline sits. The rebase ontoab78aef90and4cfd95573brought in flashnext-rank: the failure modes start once both communicators are up, and timeout bounds only the collective #2976 and flashnext cuda: at --tp 2 a prompt pass reduce-scatters its routed experts' slots by rows and gathers their sums #2977: the device test keepsmode, theleavecheck andcontext = 1024and takes flashnext cuda: at --tp 2 a prompt pass reduce-scatters its routed experts' slots by rows and gathers their sums #2977'sprompt.chunkRows()andLOGITSline,zig/tests/cuda/main.zigkeeps both usage lines,cuda_collective.zigis re-pinned with both notes, and flashnext cuda: at --tp 2 a prompt pass reduce-scatters its routed experts' slots by rows and gathers their sums #2977's exchange test rig sets thereleasehook to null. The device test ran again ondd2089769, this code onab78aef90(below).python3 tools/zig/lean_check.py, on this head: 341 on base4cfd95573, 341 on head, 0 new.python3 tools/pr_check.py --base upstream/mainon this head: 11 changed files, 0 errors, 1 advisory warning ("pass --pr NUMBER").zig fmt --check build.zig zigandgit diff --check upstream/main...HEADon this head: exit 0.zig build test-cuda-hoston this head: exit 0 on Linux x86_64 (CUDA 13.3.1 image; run without--summary, so it prints no Build Summary line). On Linux,a406758da, this code onab78aef90before the exchange rig set its hook, crashedcuda_moe_exchange_test.zig's abort test the same way; that rig now sets it to null in the same commit. On Linux the head before,928b4e086, crashed three tests (two incuda_rank_media_test.zig, one incuda_lanes_restore_test.zig) inCollective.abort: they build aCollectivefromundefinedand left the newreleasehook unset, which macOS happened to read as null. Those tests now set it to null, folded into the commit that adds the hook; at46eb7bb30, 379/379 steps, 6184/6223 passed, 39 skipped. It runs the newcuda_round_graph_test.zig: staging, capture, replay and recapture; an upload inside a capture fails cleanly; a peer lost after a replay or during a capture leaves no graph atncclCommAbort.Cache.launch,Collective.releasemissing). At46eb7bb30, withif (c.release) |r| r.run(r.ctx);planted as_ = c.release;inCollective.abort: 377/379 steps, 6182/6223 passed, 39 skipped, 2 failed (the two lost-peer tests).zig build test test-golden -Dcpu=apple_m1onf8c8046bd, this code oneab305db0before the changelog-only rebases onto4f8317dac,000b534a2and3885dceb2: exit 1,837/839 steps succeeded (1 failed); 7595/7669 tests passed (73 skipped, 1 failed). The one failure,dashboard_test("dashboard page polls the public stats snapshot without private details", untouched here), is inherited: maineab305db0fails the same test on the same Mac (837/839 steps succeeded (1 failed); 13041/13152 tests passed (110 skipped, 1 failed)).zig build test-gradient-servingon this head: pass.tf-cuda-test flashnext-model-rankondd2089769, this code onab78aef90, 2 x RTX PRO 6000,TF_FLASHNEXT_DECODE_GRAPH=1 TF_FLASHNEXT_SPLIT_DENSE=0 TF_FLASHNEXT_CHUNK=32: graph on,PASS 52 rounds equal eager rows byte for byte (round graph true): replays, two lanes, owner order, 512 seam;leavewith rank 1 SIGKILLed afterLEAVE-READY,PASS the leader stopped after 10 rounds when its peer left: RankDisconnected, rank 0 out 1.0 s after the kill.46eb7bb30:zig build,zig build compile-cuda-tests -Dtarget=aarch64-linux-gnu, the Flash Next tests forx86_64-windows-gnu(the POSIX-only host test is skipped there), and a CUDA build for sm_121.NCCL_COMM_BLOCKING=1: passed on an earlier head with the same code under test.bench_concurrent.py --levels 1,4 --alone --serialwith the graph: the device test covers two lanes.TF_FLASHNEXT_ATTN_LANES=1with the graph, and a checkpoint with pockets: read only (the checkpoint has none).--tp 1, Metal, HIP, XPU: unreachable; the round graph needs a CUDA collective.Receipt
Main against branch: cells and numbers, or why they can't be run. On main the graph cannot serve (release 1.2.6, RTX pair, both communicators, logs in Flash Next on CUDA: TF_FLASHNEXT_DECODE_GRAPH=1 at --tp 2 fails the first plain round #2974).
2 x GB10 (200G RoCE), served,
bench_concurrent.pystreamclient viatp2_bench.py, 256 tokens, greedy, median of 3, one start each, tok/s:fc6f58fbb, split 0, graph onfc6f58fbb, split 0, graph off (main's eager path)fb8fdf3d8, its defaults (dense split on)2 x RTX PRO 6000 Blackwell Max-Q,
TF_FLASHNEXT_DECODE_GRAPH=1 TF_FLASHNEXT_SPLIT_DENSE=0 TF_FLASHNEXT_CHUNK=32 tf-cuda-test flashnext-model-rank MODEL RANK PORT ids.txt [leave]:fb8fdf3d846eb7bb30FAIL flashnext-model-rank: UnownedCaptureSourcePASS 52 rounds equal eager rows byte for byte (round graph true): replays, two lanes, owner order, 512 seamleave, rank 1 SIGKILLed afterLEAVE-READYPASS the leader stopped after 11 rounds when its peer left: RankDisconnectedWith rank 0 killed instead, rank 1 exits with
RankDisconnected. Raw results, logs and the comparison tool (python3 cell_sum.py . main-base rgc-graph-off rgc-graph-on): https://gist.github.com/swbiggart/8f680962b86c8db6201950f15eed53deA fix, not a speed-up: split 0 costs more than the graph gains.
Boxes it reaches: Flash Next on CUDA at
--tp 2withTF_FLASHNEXT_DECODE_GRAPH=1, plain decode rounds without media or pockets.New switch? No;
TF_FLASHNEXT_DECODE_GRAPHexists, off by default.Environment, with the TensorFold commit, Zig and backend toolchain, machine and GPU, checkpoint and revision: commits as above; Zig 0.17.0, nvcc 13.3.73, NCCL 2.32.3; RTX sm_120, driver 595.99.02; GB10 sm_121, rank 0 driver 580.178.04; Linux;
nvidia/Qwen3.8-Flash-Next-NVFP4atfc694b54fb0174e0913e6adf86691ef85a4ead47with its MTP drafter.Paths and platforms the change reaches, which I ran and which I could not:
Engine.round's graph (sm_120, sm_121);Collective.aborton every abort path (host tests, sm_120leave); the pocket guard, read only. Not run: other SMs.Exactness, from
tools/bench_concurrent.py --alone --serialortoken_shaagainst"draft": false:token_shais code4e759dbd859c, chat764da77fbd3fin every GB10 cell, drafted and plain. Capture cannot change the collectives' bits (two-operand FP32 sum, BF16/FP32 gathers).Main against this branch, same machine and checkpoint, posted before asking for a run on our machines (or why you can't): the tables above.
Decode speed before and after, from
tools/bench_openai.py: the GB10 table. Default decode is unchanged by construction.Prompt speed before and after: n/a; the graph runs only on one-row decode rounds.
Licence of any new weights or drafter, or "n/a": n/a.
Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. Resumed and concurrent were not served.--drafter, no slower than without it, greedy and sampled. The opt-in is slower than main's defaults on GB10.