feat(ENG-CUDAGRAPH-BREAK): migrate the three single-shape drivers onto the seam, and join an outstanding aux-stream fork before a segment closes (#1335, #1163) - #1350
Merged
Conversation
…before a segment closes (#1335, #1163) D10 of `.agents/specs/eng-cudagraph-break.md`, and test 15 of its `## Tests to port`. `vt::GraphCaptureScope` now owns the set of side queues forked since the current segment opened and joins every outstanding one before it calls `Backend::EndCaptureGraph`. This is the port of upstream's auto-join inside `_end_current_segment` (`breakable_cuda_graph.py:353-361` @ SGLang pin `f63458b5be`) together with the `wait_stream` hook (`:101-153`, installed `:310`, removed `:332`) whose entire purpose is to know WHICH streams are outstanding. WHY IT IS CHEAPER FOR US THAN FOR SGLANG. Upstream monkey-patches `torch.cuda.Stream.wait_stream` because a fork there is an implicit torch call it cannot otherwise observe. Ours is an explicit `Backend::RecordEvent` plus `Backend::QueueWaitEvent` pair on a seam we control, so the model TELLS the scope: `vt::GraphNoteFork(aux, join_event)` registers and `vt::GraphNoteJoin(aux)` retires the registration when the model joined the queue itself. Both are no-ops outside an active scope and make ZERO backend calls there, which is the same pass-through guarantee `vt::GraphBreak` gives. WHY IT COULD NOT LAND EARLIER, said plainly rather than implied. W1 registered its break point on a model that forks no auxiliary queue. W2, W3 and W4 all migrated drivers that open `kFull`, which has exactly ONE segment and therefore no segment CLOSE inside a fork window for the rule to govern; landing the machinery then would have landed it unexercised. W5 owns the first driver whose fork is inside the captured region by construction. THE GATE, AND THE ASSUMPTION THAT IT IS VACUOUS UNTIL A MUTATION SAYS OTHERWISE. W3 measured a guard that could not fail: flipping `kFull` to `kPiecewise` left a whole driver gate green at 226/226 because the mode was unobservable from outside the driver. So the fork set is a COUNTER, not an inference: `vt::GraphBreakStats` gains `forks_tracked` and `forks_auto_joined`, and the second is the load-bearing one — it is 0 for a model that joins its own fork inside the segment and non-zero exactly when the seam did the work. The rule is an ORDER, not an event: closing a capture with an unjoined fork FAILS at `cudaStreamEndCapture`, so "the join happened" is not the claim, "the join happened BEFORE the close" is. Both ends are asserted out of ONE backend trace, for the same reason W1 had to move break markers into the backend's own log — two independently asserted sequences are satisfied by an implementation that interleaves nothing. `tests/vt/recording_capture_backend.h` therefore logs `RecordEvent` and `QueueWaitEvent` and reports `SupportsAuxStream()` true, which is what the CUDA backend reports (`src/vt/cuda/cuda_backend.cu:200`). Five arms, and three of them exist to stop the gate from being a flag that is always set: the rule itself (an unjoined fork is joined, trace `Begin RecordEvent QueueWaitEvent EndCaptureGraph Begin EndCaptureGraph`); the CONTROL where the model joins first and the scope must then do NOTHING; `kFull`, which is the arm every migrated driver actually takes; the inert scope, where both hooks move no counter; and re-registering one queue, which must leave ONE entry because two would make a single `GraphNoteJoin` leave a joined queue looking outstanding forever. Red-first and mutation-proven, both compiled clean and both with the diff stat recorded. Deleting the `JoinOutstandingForks()` call from `EndSegment` — the rule itself — reds ONLY the new case, 5 assertions, exit 1, at 29 of 30 cases still passing. Making it OVER-fire, by stopping `NoteJoin` from retiring the entry, reds the same case on 8 assertions, which is what proves the control arm is not decoration. Green after: `test_breakable_graph` 30 cases, 265 assertions, exit 0. NO SPEED IS CLAIMED AND NONE WAS MEASURED. This row has never carried a throughput case and this stage does not give it one. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…am (#1335, #1163) The first of W5's three single-shape drivers. The `(1+k)` paged draft step of the DFlash speculator captures through `vt::GraphCaptureScope` over a `vt::BreakableGraph` in `kFull` and replays through `BreakableGraph::Replay`, so the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle, the `Backend*` the store kept alive only so its destructor could call `DestroyGraph`, that destructor, and the driver's own `try`/drain are gone. Seven of the nine drivers `## Our baseline` enumerates are now on the seam. `VT_DFLASH_GRAPH` STAYS. It is an A/B lever for exactly this driver, not a copy of the shared kill switch; what the migration adds is that `VLLM_CPP_CUDAGRAPH` now reaches this driver too, through `vt::GraphCaptureEnabled()` inside the scope, where before it did not reach it at all. kFULL, INHERITED FROM W2 AND NOT RE-ARGUED. vLLM's v1 default `FULL_AND_PIECEWISE` (`vllm/config/compilation.py:63` @ pin `5559679229`) is documented at `:630-632` as a FULL graph for DECODE batches and a piecewise one for prefill and mixed batches, and `decode_mode()` (`:65-66`) returns the full half. A draft step is a decode batch, so its capture is ONE segment with the attention calls INSIDE it — byte-identical in shape to the region this replaces. W2 measured what the alternative costs: a `kPiecewise` scope over a decode driver does not merely slow the step down, it FAULTS on the first replay. A FAILED CAPTURE NOW PROPAGATES INSTEAD OF DOWNLOADING ITS BUFFER, and the distinction is the one W2 learned the hard way. `~GraphCaptureScope` must swallow a throwing `EndCaptureGraph` — a destructor that propagates terminates — so a FAILED capture leaves the container reporting exactly what an INERT scope reports. Those states mean opposite things: an inert scope ran the forward EAGERLY and its buffer is real, while under stream capture NOTHING between `BeginCapture` and the throw executed, so the failed capture's logits buffer holds whatever the pool last left there. Handing that to the speculator is invisible to a token gate, because a draft the target rejects is indistinguishable from a bad draft. `capture_failed()` and `capture_error()` separate the two and the driver rethrows the runtime's own exception. ## Why this driver is the one W5 can gate on this box The other two single-shape drivers refuse a CPU queue BEFORE they reach their capture: DeepSeek V4's `CanRunResidentDecode` returns false for `device.type == kCPU` and again unless the four CUDA-registered V4 kernel families are present, and Laguna's whole capture class compiles only under `VT_MARLIN_NVFP4`, which needs a CUDA build on a marlin-nvfp4 architecture. The DFlash draft graph's admission predicate names neither a device type nor a kernel registry, so the shared harness's two swapped registries reach it. ## The gate, red first `tests/vllm/models/test_qwen3_dflash_decode_graph_seam.cpp`, on the shared `decode_graph_seam_harness.h`. It exists because nothing else can see the difference: a driver that kept its raw pair produces identical logits, an identical backend log and an identical private replay count, so `segments_captured`, `full_scopes` and `replays` are the ONLY observables that separate "captured a graph" from "captured a graph THROUGH THE SEAM". RED FIRST against the unmigrated driver: 3 cases, 0 passed, 16 assertions, 7 failed, exit 1, on exactly those counters. GREEN after: 3 cases, 18 assertions, exit 0. G2, the reachability mutation, compiled clean at 79 insertions and 29 deletions: replacing the scope and `Replay` with the pre-W5 raw pair reds ONLY this file, 7 assertions, while `test_breakable_graph` (265), `test_qwen3_decode_graph_seam` (231), `test_qwen3_moe_decode_graph_seam` (228), `test_voxtral_decode_graph_seam` (230), `test_deepseek_v2_decode_graph_seam` (230), `test_qwen3_5_decode_graph_seam` (129) and `test_dflash_propose` (31) all stay GREEN. That last one is the point of the whole file: the driver's own bit-exactness suite cannot see the difference at all. G4 holds in the same file: the capture step's logits are bit-identical to the driver's own eager paged arm, 24 values, 0 differing. G1 — that a REPLAYED segment reproduces the eager forward — needs a real device and is recorded as owed, because a CPU "replay" recomputes nothing. NO SPEED IS CLAIMED AND NONE WAS MEASURED. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…the seam (#1335, #1163) The second of W5's three single-shape drivers, and the sibling `laguna.cpp`'s own note named as the one that moves with it. `V4Graph::Step` captures through `vt::GraphCaptureScope` over a `vt::BreakableGraph` in `kFull` and replays through `BreakableGraph::Replay`, so the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle and the hand-written destructor that released it are gone. `gstate` STAYS, and it is not a duplicate of `captured()`. It is this driver's cold/warm/captured ladder, and the seam has no notion of the eager warm-run that grows the per-stream keep-quant GEMM scratch so the capture that follows can allocate nothing. `VT_V4_DECODE_GRAPH` stays for the same reason `VT_DFLASH_GRAPH` does: it is a same-binary A/B lever for exactly this driver. What changes is that `VLLM_CPP_CUDAGRAPH` now reaches this driver at all, through `vt::GraphCaptureEnabled()` inside the scope. kFULL, INHERITED FROM W2 AND NOT RE-ARGUED — vLLM's v1 default `FULL_AND_PIECEWISE` (`vllm/config/compilation.py:63` @ pin `5559679229`) is a FULL graph for DECODE batches (`:630-632`), and `decode_mode()` (`:65-66`) returns the full half. This is the T=1 resident decode step, so the capture is ONE segment with the attention calls inside it. A FAILED CAPTURE PROPAGATES. Under stream capture nothing between `BeginCapture` and a throwing `EndCaptureGraph` executed — the kernels were RECORDED, not run — so the driver's persistent `logits` hold whatever the pool last left there, and returning them would make this step's output uncomputed device memory with no fault and nothing a token gate could see. The inert arm is the opposite state and is handled as such: `RunChain` ran EAGERLY, the buffers are real, and the driver stays in `gstate == 1` running eager every step rather than pretending to hold a graph. ## G1 and G2 are OWED for this driver, on hardware, and the reason is not effort `CanRunResidentDecode` (`deepseek_v4.cpp:1481-1487`) refuses a CPU queue outright and refuses again unless `V4DeviceKernelsAvailable()` — the four V4 kernel families registered under `kCUDA` by `cuda_deepseek_v4.cu`. The shared CPU harness swaps the CPU backend and platform, which is enough for a driver whose admission predicate names capture capability; it cannot manufacture a device type, a CUDA op registry and a whole vt kernel layer. So this migration is covered by CONSTRUCTION — it is the same seam, the same mode and the same call shape as the six drivers that are gated — and NOT by measurement. That is an argument, not a gate, and the spec's `## Owed` records it as owed with the resource it needs rather than implying this change is held by something. Verified to COMPILE in the CPU build, which compiles `V4Graph` (it is not behind a build flag), and the model's own suites stay green: `test_deepseek_v4_gguf_load` 15 cases / 931 assertions, `test_deepseek_v4_forward` 6 / 34, `test_deepseek_v4_scaffold` 5 / 62, `test_deepseek_v4_mtp` 5 / 37, all exit 0. NO SPEED IS CLAIMED AND NONE WAS MEASURED. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…eam, and give D10 its production caller (#1335, #1163) The third of W5's single-shape drivers, and the one whose own note asked for this seam by name. `laguna.cpp:2116-2119` said the capture class was "irreducibly device-coupled" with a "REPAIR OWED: a portable `vt` capture/replay seam (the same one deepseek_v4.cpp's V4Graph would move behind)". That repair has landed, both graphs moved onto it in this stage, and the note now says what is actually left device-coupled: the captured CHAIN is `VT_MARLIN_NVFP4` CUDA code, not the capture. `LagunaGraph::Step` captures through `vt::GraphCaptureScope` over a `vt::BreakableGraph` in `kFull` and replays through `BreakableGraph::Replay`; the raw `void*` handle and its hand-written release are gone. NINE OF NINE DRIVERS ARE NOW ON THE SEAM. ## This is where D10 stops being machinery and starts being reached `LagunaGraph` owns its own auxiliary queue and event pair and forks the fp4 shared expert onto it INSIDE the captured region by construction, because the class is the capture driver: fork at `:2572-2576`, join at `:2612`. It is the only such site in the tree — Qwen3.5's MoE overlap forks outside its driver's scope. So the fork now registers with the scope (`vt::GraphNoteFork(aux_q, aux_done)`) and the model's own join retires it (`vt::GraphNoteJoin(aux_q)`), which is what makes the auto-join a capability something reaches rather than a class that works. The retirement call is not politeness. Without it the scope would issue a REDUNDANT second join before every segment close, which is exactly the arm the unit gate's CONTROL case pins at `forks_auto_joined == 0`. And both calls are no-ops outside an active scope making zero backend calls, so the eager arm is byte-identical. The queue and its two events stay owned by `LagunaGraph`, and its destructor still releases them: the seam TRACKS a fork and never owns one. `aux_done` therefore outlives every scope, which is the lifetime rule the registration needs. kFULL, INHERITED FROM W2 AND NOT RE-ARGUED, and a failed capture PROPAGATES for the reason the sibling commits state — under stream capture the abandoned region's kernels were RECORDED, not run, so `logits` and the on-device `argmax_id` hold pool-recycled memory and returning them would make this step's token uncomputed device memory with no fault. ## G1 and G2 are OWED for this driver, on hardware, and the reason is the build The whole capture class sits behind `#ifdef VT_MARLIN_NVFP4`, which CMake enables only for a CUDA build on a marlin-nvfp4 architecture (`sm_12xa` and, since the Thor bring-up, `sm_110`). This box has no NVIDIA GPU and no `nvcc`, so the migrated code is not compiled by any build reachable from here, and no CPU harness can reach a class that does not exist in the binary. `laguna.cpp` builds clean in the CPU configuration and `test_laguna_scaffold` (8 cases, 167 assertions) and `test_laguna_nvfp4_loader` (4 cases, 63 assertions) stay green, but that verifies the file, NOT the migrated region. The spec's `## Owed` records what this owes and the resource it needs, rather than letting a green CPU suite imply a coverage it cannot have. NO SPEED IS CLAIMED AND NONE WAS MEASURED. This driver's graph is a measured decode win that already shipped; nothing here changes what it captures. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…e drivers, and D10 is discharged and reached (#1335, #1163) The record edits W5's three driver commits and its D10 commit made stale, riding in the same pull request per AGENTS.md "a record edit rides in the pull request whose change made the record stale". `.agents/specs/eng-cudagraph-break.md` — `## Work breakdown` W5 becomes DONE and states what it delivered and what it did not; `## Now` moves to NINE OF NINE drivers on the seam with only W6 left; `## Gates` G1 is re-counted from "five of six" to "five of NINE", because W5 added three migrated drivers and NO G1 case and the count must not read as coverage it does not have; `## Owed` retires D10 and files three new per-driver entries. ## What the Owed entries say, and why they are per driver The temptation is one line reading "G1 owed for W5's drivers", and it would hide that the three have DIFFERENT obstacles with different fixes. DFlash owes only G1, and for the reason every CPU-gated driver in this row owes it: a CPU "replay" recomputes nothing. DeepSeek V4 owes G1 AND G2, because `CanRunResidentDecode` refuses a CPU queue outright and refuses again without the four V4 kernel families registered under `kCUDA`. The shared harness swaps a backend and a platform; it cannot manufacture a device type, a CUDA op registry and a whole vt kernel layer, and a stub that did would measure the stub. The migration is covered by CONSTRUCTION, which is an argument and not a gate, and the entry exists so nobody reads it as one. Laguna owes both for a BUILD reason rather than a predicate one: its capture class is inside `#ifdef VT_MARLIN_NVFP4`, so on a host with no `nvcc` the migrated region is in no reachable binary. `laguna.cpp` compiling clean in the CPU configuration verifies the FILE, not the region, and its two green suites say nothing about the capture — which is exactly the shape of a green that means less than it looks like. G5's ROCm and Tenstorrent arms move from W5 to the ROW. This is the second stage to inherit the item and hit the identical wall — `rc devices` lists `dgx:gpu0`, `orin:gpu0` and `thor:gpu0`, all NVIDIA — and that repetition is the signal that it is not a stage-sized problem: no ordering of the remaining work produces the hardware. D10's retirement carries a RESIDUAL rather than a clean close: the seam joins a fork the model REGISTERED, so a model that forks without calling `vt::GraphNoteFork` is exactly as exposed as before, and no checker can see that because a fork is an ordinary pair of backend calls. ## The public documents, and one that was genuinely wrong `docs/USAGE.md` said "Production steps now open a capture scope, six of them as of W4" and enumerated six drivers. That is now false, and the paragraph carries its own note that it had asserted a stale claim once before. It now names all nine, and it states the part that is a USER-VISIBLE change rather than an internal one: the three single-shape drivers never read `VLLM_CPP_CUDAGRAPH` at all — each invented its own name — so before W5 no single setting turned capture off everywhere. There is one now, and the three per-driver names stay as per-driver A/B levers. It also states why turning capture off is safe while a FAILED capture is not, because that distinction is invisible to a token gate. `docs/ENVIRONMENT.md` gains the same reach note on `VT_DFLASH_GRAPH`, the only one of the three that has a row there. `docs/STATUS.md` gains three paragraphs, all under the 700-character cap: the migration completing, the auxiliary-stream rule and why W5 is the first stage that could exercise it, and what remains owed with the per-driver reasons. No paragraph grew. `.agents/engine-matrix.md` and `.agents/issue-index.md` (append-only, one new row for #1335) follow, and `.agents/claims/CLAIM-ENG-CUDAGRAPH-BREAK-W5.md` records the scope this claim owns and excludes. The claim that nine of nine are migrated is a MEASUREMENT and is stated as the command that produced it: a call-shaped grep over `src/vllm/` for `BeginCapture`, `EndCaptureGraph`, `ReplayGraph` and `DestroyGraph`, with comment lines excluded, returns nothing. The unqualified word-grep still matches 23 lines, every one of them prose about what was removed, which is why the qualifier is in the record instead of a cleaner-sounding sentence. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
… into eight columns (#1335) The #1335 row quoted the exact grep that proves nine of nine drivers are on the seam, and that regex contains three alternation pipes. Markdown reads every one as a cell boundary, so `scripts/check-agent-record.py` counted 8 pipes where the schema wants 5. The same text was pasted into `.agents/engine-matrix.md`, where it broke that row from 11 fields to 14 and was caught before the commit. This is the second occurrence of one mistake, which is the argument for the fix being a RULE rather than an escape: a table cell states the grep in words, and the prose in `.agents/specs/eng-cudagraph-break.md` — which is not a table — keeps the literal command a reader can run. The row is unlanded and is this branch's own append, so correcting it is not an edit to an existing index row. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
`origin/main` advanced to `edbc47ce0` while W5 was in flight. Three of the four files that auto-merged are KEYED RECORDS, so the merge was verified rather than accepted: `.agents/engine-matrix.md` differs from `origin/main` on exactly ONE key, `ENG-CUDAGRAPH-BREAK`, with all 290 lines and every other row byte-identical; `.agents/issue-index.md` holds 412 rows with ZERO duplicate issue ids and exactly one row added against `origin/main`, which is this branch's #1335 append; and `docs/STATUS.md` carries `origin/main`'s edit intact, since diffing the merged tree against `origin/main` removes no line of it. `.agents/specs/record-anchor-ratchet.md` is `origin/main`'s version whole. This branch never touched it. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…d of the eager one (#1335, #1163) Found and CAUSED while migrating the DFlash draft graph onto the capture seam, and fixed in the same flow. `ForwardBlockLogitsWithDeviceKV`'s paged predicate was complete before W5, because the driver's capture was its own `BeginCapture` pair and nothing outside the file could turn it off. W5 routes that capture through `vt::GraphCaptureScope`, which reads `VLLM_CPP_CUDAGRAPH` itself — so the switch gained a state the predicate did not know about. With `VLLM_CPP_CUDAGRAPH=0` on a capture-capable backend the driver still chose the CAPTURE lane: it ran its eager warm pass, opened an INERT scope, and ran the whole `ForwardPagedBody` a SECOND time inside it. Two full draft forwards per propose, forever, because it could never reach `g_state == 2`. Correct output, pure waste, on a non-default diagnostic lane. `vt::GraphCaptureEnabled()` is now the third conjunct, so the switch selects this driver's existing single-forward eager path — what it means everywhere else. ## The gate is unusual because the defect is invisible to every ordinary one Both versions produce IDENTICAL logits and both leave `segments_captured` at 0. A token gate cannot see this and neither can the seam's counters. What differs is which CODE PATH ran, so the assertion is LANE IDENTITY against a live control: a backend that CANNOT capture takes the eager path by construction, and a capture-capable backend with the switch OFF must then do exactly the same work, because it must be running exactly the same code. An equality against a control rather than a magic number, so it cannot go stale when the driver's allocation pattern changes. It runs in a CHILD PROCESS for the reason `test_breakable_graph` T4 does: the switch is read once per process into a function-local static. **THE FIRST INSTRUMENT WAS WRONG AND THE HARNESS NOW SAYS WHY.** Counting `Backend::Alloc` read `control=11, switch-off=0` — and would have read the same two numbers whichever lane was measured second, because the `DevicePool` serves the second of two identical forwards out of its free list without calling `Alloc` at all. That instrument reported the ORDER of the runs, not their shape, and it would have failed the correct program. Copies are per-operation work the pool cannot absorb, so `CaptureCapableCpuBackend` counts those; the comment at the counter records the trap rather than leaving the next reader to re-find it. The harness also gains a `supports_capture` flag so `StaticGraphCpu(false)` produces the control lane. That is a flag rather than a subclass because `CaptureCapableCpuBackend` is `final`, and unsealing a shared test class to add one boolean is the larger change. Red-first: the new case FAILS on the unfixed driver, `DIFFERENT-LANE` with `control copies=4, switch-off copies=5`, exit 1, one assertion, compiled clean at 22 insertions and 3 deletions — and the same run reports the logits `IDENTICAL`, which is the measurement that shows why nothing else could catch it. Green after: `test_qwen3_dflash_decode_graph_seam` 4 cases, 23 assertions, exit 0. Every other seam gate is unchanged and green: `test_breakable_graph` 265, `test_qwen3_decode_graph_seam` 231, `test_qwen3_moe_decode_graph_seam` 228, `test_voxtral_decode_graph_seam` 230, `test_deepseek_v2_decode_graph_seam` 230, `test_qwen3_5_decode_graph_seam` 129, `test_dflash_propose` 31. Closes #1352. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…roving both ifdef'd regions are really compiled (#1335, #1163, #1352) Two GPU results and the #1352 index row, riding in the pull request whose change made the record stale. ## G1 was RE-RUN, and it is a re-run rather than a new case D10 put a `JoinOutstandingForks()` call on the path of EVERY segment close, so the seam changed UNDERNEATH the five drivers W3 and W4 measured: each migrated driver's capture now executes seam code that run did not. A change to the shared close path is exactly what a CPU harness cannot clear, and carrying forward an older number would have been asserting a measurement of different code. On `thor:gpu0` through an `rc` lease, same provenance as W3 and W4 — NVIDIA Thor, sm_110, driver 595.78, nvcc 13.0.88, `-DVLLM_CPP_CUDA=ON -DVLLM_CPP_CUDA_ARCHITECTURES=110`, 32 `.cu.o` objects — at source `79dc6b5bd`: `test_decode_graph_seam_g1_cuda` ran 5 cases, 2066 assertions, 0 failed, exit 0, every driver again reading `0 differing, 4 replays`, and `test_breakable_graph` ran 265 assertions on the same device. ## The measurement a green build could NOT have produced Laguna's capture class is inside `#ifdef VT_MARLIN_NVFP4`. So "the CUDA build compiled `laguna.cpp`" is satisfied just as well by a build that compiled the migrated region OUT — success and failure are the SAME OBSERVATION, which is the shape this project has been burned by before. Two facts settle it, and the second is the load-bearing one. CMake reported `CUDA feature marlin-nvfp4: ENABLED for [110]` and `-DVT_MARLIN_NVFP4=1` appears on `laguna.cpp`'s OWN compile command in `compile_commands.json` — not on the project's, on the file's. And the region itself was MUTATED: an undeclared identifier inserted immediately after `vt::GraphCaptureScope scope(b, q, graph, kFull)` failed the object build under `-Werror` at `laguna.cpp:2735`, against a baseline object build of rc 0, with the tree restoring to an empty `git diff`. The identical mutation on DeepSeek V4 failed at `deepseek_v4.cpp:1921`. Both migrated regions are COMPILED. `## Owed` now splits what that closes from what it does not: "the code might not even be built" is gone, while G2 (the routing reaches the seam) and G1 (a REPLAYED segment reproduces the eager forward) remain, and both need each model's own device kernels rather than only a compiler. ## #1352 Appended to the index. The row records the defect, the fix, and the part worth carrying forward: the FIRST instrument was wrong in a way that would have failed the correct program. Counting `Backend::Alloc` read `control=11, switch-off=0`, and would have read those same two numbers whichever lane ran second, because the `DevicePool` serves the second of two identical forwards out of its free list without calling `Alloc` at all. It reported the ORDER of the runs, not their shape. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…er ran on (#1335, #1163) Found by W5 while reading `## Owed` to record what this stage did and did not discharge. Record-only; no product behaviour is involved. `.agents/specs/eng-cudagraph-break.md` closed its G5 entry with "the seam's CUDA arm now runs on TWO architectures rather than one, sm_110 here and sm_121a on GB10 for the W1 exit criterion". The SAME FILE records that criterion, under `## Work breakdown` W1, as "measured on `orin:gpu0` through an `rc` lease, driver `12060`". A Jetson AGX Orin is neither a GB10 nor `sm_121a`, so the sentence was wrong in both halves: it named a device the measurement did not run on, and an architecture nothing in this row has measured the criterion against. The two-architecture claim SURVIVES and only the attribution was wrong. The exit criterion — `cudaStreamEndCapture` followed by `cudaStreamBeginCapture` mid-forward with eager work between — ran on `orin:gpu0`; G1 and the unit suite ran on `thor:gpu0` at sm_110 for W3, W4 and W5. ## The part that is not a typo `sm_121a` on GB10 is OWED, and it now has its own `## Owed` entry rather than a clause inside another one. It carries the reason it cannot be closed cheaply: **re-running G1 on `dgx:gpu0` would not discharge it.** Every migrated driver opens `kFull`, so nothing in this tree re-begins a capture mid-forward, and G1 exercises capture and replay rather than the re-begin the criterion is about. Discharging it needs the W1 probe itself run on GB10, or the first PIECEWISE production driver, which is W6. W5 could not take it either way: `dgx:gpu0` was held by another session for this stage's whole window. This is the shape where a number quoted often starts being treated as measured, so it is filed and corrected rather than quietly reworded. `docs/STATUS.md` was checked and is correct — it names `orin:gpu0` — so no public document changes. Closes #1361. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…are green at the final head (#1335, #1163) Completes the GPU evidence for W5 from the same `rc` job on `thor:gpu0`, at the head this branch ends on (`79dc6b5bd`). The full CUDA library built clean at 742 targets, `tests/vt/test_breakable_graph.cpp` ran 30 cases and 265 assertions, and `tests/vllm/models/test_qwen3_dflash_decode_graph_seam.cpp` ran 4 cases and 23 assertions — so W5's own driver gate, the #1352 child-process arm included, is green on a CUDA build and not only on this box's CPU one. That last number is recorded for what it is and no more. It is a BUILD and ROUTING result, not a replay one: the DFlash case still drives the CPU harness inside that binary, so it says the gate compiles and passes where the real backend exists, and says nothing about a replayed segment reproducing the eager forward. G1 for that driver stays owed, and `## Owed` still says so. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
`origin/main` advanced again, to `d0598a255`, while W5 was in flight. Verified rather than accepted, because three of the files that auto-merged are KEYED RECORDS. `.agents/engine-matrix.md`: 291 lines on both sides, differing on exactly ONE key, `ENG-CUDAGRAPH-BREAK`, with every other row byte-identical. `.agents/issue-index.md`: 418 rows, ZERO duplicate issue ids, exactly THREE rows added against `origin/main` — this branch's #1335, #1352 and #1361 appends. `docs/USAGE.md` needs a word, because the mechanical check reports nine lines "removed" against `origin/main` and that is CORRECT rather than a lost merge: they are the stale paragraph claiming "Production steps now open a capture scope, six of them as of W4", which this branch replaces with the one naming all nine. A merge check that flagged only additions would have passed a branch that silently dropped someone else's edit, so the removals were read rather than counted. The squashed shape was re-materialized against this same `d0598a255` in a detached scratch worktree and every applicable checker is green there, including `check-doc-checkpoint --commit HEAD`, which is the single-commit form a squash-merge actually lands. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
Takes main so the committed range gates report against an ancestor base and the squashed shape does not revert main's newer commits. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
W5 is the LAST migration stage of
ENG-CUDAGRAPH-BREAK. It moves the threeSINGLE-SHAPE drivers onto the break-point capture seam and discharges D10, the
auxiliary-stream fork/join, which every earlier stage was structurally unable to
exercise.
NINE OF NINE DRIVERS ARE NOW ON THE SEAM AND THE MIGRATION IS COMPLETE. A
call-shaped grep over
src/vllm/forBeginCapture,EndCaptureGraph,ReplayGraphandDestroyGraph, with comment lines excluded, returns NOTHING.The unqualified word-grep still matches 23 lines, every one of them prose about
what was removed — which is why the qualifier is stated rather than a
cleaner-sounding sentence.
Gone with them: three raw
void*handles, two hand-written destructors thatreleased them, and the DFlash driver's private
try/drain.The three drivers
Each
Stepopens avt::GraphCaptureScopeover its ownvt::BreakableGraphinkFulland replays throughBreakableGraph::Replay.qwen3_dflash.cpp) — the(1+k)paged draft step ofthe speculator.
deepseek_v4.cpp) — the T=1 resident decode step.laguna.cpp) — whose own note at:2116-2119askedfor this seam by name and named V4's driver as the sibling that moves with it.
Both moved here, and that note now says what is actually left device-coupled:
the captured CHAIN is
VT_MARLIN_NVFP4CUDA code, not the capture.kFULL, inherited from W2 and not re-argued: vLLM's v1 defaultFULL_AND_PIECEWISE(vllm/config/compilation.py:63@ pin5559679229) isdocumented at
:630-632as a FULL graph for DECODE batches, anddecode_mode()(
:65-66) returns the full half. W2 measured what the alternative costs — akPiecewisescope over a decode driver does not merely slow the step down, itFAULTS on the first replay.
The three per-model rollback switches STAY (
VT_V4_DECODE_GRAPH,VT_DFLASH_GRAPH,VT_LAGUNA_DECODE_GRAPH): each is a same-binary A/B lever forexactly one driver. What changes for a user is that
VLLM_CPP_CUDAGRAPHreachesall three for the first time — before this, none of them read it, so no single
setting turned capture off everywhere.
docs/USAGE.mdsays so.A FAILED capture now propagates in all three instead of returning its buffer.
Under stream capture nothing between
BeginCaptureand a throwingEndCaptureGraphexecuted — the kernels were RECORDED, not run — so thosebuffers hold whatever the allocator last left there. On the DFlash driver that is
especially invisible, because a draft the target rejects looks exactly like a bad
draft.
D10, and why W5 is the first stage that could land it
GraphCaptureScopeowns the set of side queues forked since the current segmentopened and joins every outstanding one before
Backend::EndCaptureGraph— theport of
_end_current_segment(breakable_cuda_graph.py:353-361@ SGLang pinf63458b5be) plus thewait_streamhook (:101-153) whose only purpose is topopulate that set.
W1 registered its break point on a model that forks no auxiliary queue. W2, W3
and W4 all opened
kFull, which has exactly ONE segment and therefore no segmentCLOSE inside a fork window for the rule to govern; landing the machinery then
would have landed it unexercised. W5 owns
laguna.cpp:2572-2576,2612, the onlyfork inside a captured region by construction, and that driver is the rule's
production caller through
vt::GraphNoteForkandvt::GraphNoteJoin. Nomonkey-patch is needed, because our fork is an explicit
RecordEvent/QueueWaitEventpair rather than an implicit torch call — the model tells thescope.
The rule is gated as a COUNTER and an ORDER, because W3 proved prose is not a
gate: it measured a mode guard that stayed green at 226/226 under the exact
mutation it named. So
vt::GraphBreakStatsgainsforks_trackedandforks_auto_joined, and the claim is an ORDER — closing a capture with anunjoined fork FAILS at
cudaStreamEndCapture, so "the join happened BEFORE theclose" is the whole thing, asserted out of ONE backend trace
(
Begin RecordEvent QueueWaitEvent EndCaptureGraph Begin EndCaptureGraph).Gates
Test 15 of
## Tests to port, five arms — the rule, the CONTROL where the modeljoins first and the scope must do nothing,
kFull, the inert scope, andre-registering one queue. Two mutations, both compiled clean, both with the
diff stat recorded: deleting
JoinOutstandingForks()reds ONLY the new case (5assertions, exit 1, 29 of 30 cases still passing), and making
NoteJoinfail toretire the entry reds it on 8 — so neither the rule nor its control is
decoration.
test_breakable_graph30 cases / 265 assertions / exit 0.tests/vllm/models/test_qwen3_dflash_decode_graph_seam.cpp, on the sharedharness. RED FIRST against the unmigrated driver: 3 cases, 0 passed, 16
assertions, 7 failed, exit 1. GREEN after: 3 cases, 18 assertions, exit 0. G2:
replacing the scope and
Replaywith the pre-W5 raw pair (compiled clean, 79insertions / 29 deletions) reds ONLY that file, while
test_breakable_graph(265),
test_qwen3_decode_graph_seam(231),test_qwen3_moe_decode_graph_seam(228),
test_voxtral_decode_graph_seam(230),test_deepseek_v2_decode_graph_seam(230),test_qwen3_5_decode_graph_seam(129) and the driver's OWN
test_dflash_propose(31) all stay GREEN. That lastone is the argument restated as a measurement: a driver's bit-exactness suite
cannot see which capture machinery ran. G4 holds in the same file at 24
values, 0 differing.
What was measured on a GPU
Two results from one
rclease onthor:gpu0— NVIDIA Thor, sm_110, driver595.78, nvcc 13.0.88,
-DVLLM_CPP_CUDA=ON -DVLLM_CPP_CUDA_ARCHITECTURES=110, 32.cu.oobjects, the same provenance W3 and W4 used.G1 was RE-RUN, and it is a re-run rather than a new case. D10 puts a
JoinOutstandingForks()call on the path of EVERY segment close, so the seamchanged UNDERNEATH the five drivers W3 and W4 measured; carrying an older number
forward would have been asserting a measurement of different code.
test_decode_graph_seam_g1_cudaran 5 cases, 2066 assertions, 0 failed, exit 0,every driver again reading
0 differing, 4 replays, andtest_breakable_graphran 265 assertions on the same device.
And the one thing a green build could NOT have told us was measured
separately. Laguna's capture class is inside
#ifdef VT_MARLIN_NVFP4, so "theCUDA build compiled
laguna.cpp" is satisfied just as well by a build thatcompiled the migrated region OUT — success and failure are the SAME OBSERVATION.
CMake reported
CUDA feature marlin-nvfp4: ENABLED for [110]and-DVT_MARLIN_NVFP4=1appears onlaguna.cpp's OWN compile command incompile_commands.json. Then the region itself was MUTATED: an undeclaredidentifier inserted immediately after
vt::GraphCaptureScope scope(b, q, graph, kFull)failed the object build under-Werroratlaguna.cpp:2735, against a baseline object build of rc 0, with thetree restoring to an empty
git diff. The identical mutation on DeepSeek V4failed at
deepseek_v4.cpp:1921. Both migrated regions are compiled.A defect this migration CAUSED, found and fixed in the same flow
#1352. W5 routes the DFlash capture through the seam, and the seam reads
VLLM_CPP_CUDAGRAPHitself — so the driver's admission predicate gained a stateit did not know about. With that switch set to
0on a capture-capable backendthe driver still chose the CAPTURE lane, ran its eager warm pass, opened an INERT
scope, and ran the whole forward a SECOND time inside it: two full draft forwards
per propose, forever. Correct output, pure waste, on a non-default lane — which
is exactly why nothing would have found it later.
Both versions emit IDENTICAL logits and both leave
segments_capturedat 0, sono ordinary gate separates them. The assertion is LANE IDENTITY against a live
control: a backend that CANNOT capture takes the eager path by construction, so a
capture-capable backend with the switch OFF must do exactly the same work. Run in
a child process, because the switch is read once per process.
The first instrument was WRONG in the direction that fails the CORRECT
program, and the harness records why. Counting
Backend::Allocreadcontrol=11, switch-off=0, and would have read those two numbers whichever laneran second, because the
DevicePoolserves the second of two identical forwardsout of its free list without calling
Allocat all. It reported the ORDER of theruns, not their shape. Copies are per-operation work the pool cannot absorb.
Red-first on the unfixed driver:
DIFFERENT-LANE,control copies=4againstswitch-off copies=5, exit 1 — and the same run reports the logitsIDENTICAL.What is OWED, stated per driver because the obstacles differ
it: a CPU "replay" recomputes nothing.
CanRunResidentDecoderefuses a CPU queueoutright and refuses again without the four V4 kernel families registered under
kCUDA. The harness swaps a backend and a platform; it cannot manufacture adevice type, a CUDA op registry and a whole vt kernel layer, and a stub that
did would measure the stub. This migration is covered by CONSTRUCTION, which is
an argument and not a gate.
#ifdef VT_MARLIN_NVFP4, so on a host with nonvccthe migrated region is inno reachable binary. Its two green CPU suites verify the FILE, not the region.
One half of 2 and 3 is now CLOSED: the mutation above proves both regions are
really compiled, so "the code might not even be built" is gone. What remains is
behavioural — that the routing reaches the seam, and that a REPLAYED segment
reproduces the eager forward — and both need each model's own device kernels
rather than only a compiler.
G5's ROCm and Tenstorrent arms move from W5 to the ROW. This is the second stage
to inherit the item and hit the identical wall —
rc deviceslistsdgx:gpu0,orin:gpu0andthor:gpu0, all NVIDIA — and no ordering of the remaining workproduces the hardware.
Framing
Coverage and correctness, never speed. No throughput is claimed and none was
measured. Prefill has 3.8% host idle at above 96% GPU-busy, the 27B prefill gap
is 92.5% non-GEMM glue, decode is already captured, and the sibling row
ENG-CUDAGRAPH-DEDUPclosed its benefit question negative after measurement.kFullkeeps every migrated step's shape the one it already had.One more record defect, found and corrected in flow
#1361. The spec's G5 entry credited the W1 exit criterion to "sm_121a on GB10",
while the SAME FILE records it as measured on
orin:gpu0. A Jetson AGX Orin isneither a GB10 nor
sm_121a, so the sentence named a device the measurement didnot run on and an architecture nothing in this row has measured the criterion
against. The two-architecture claim survives and only the attribution was wrong:
the criterion ran on
orin:gpu0, G1 and the unit suite onthor:gpu0at sm_110.sm_121aon GB10 is OWED and now has its own entry, carrying the reason itcannot be closed cheaply: re-running G1 on
dgx:gpu0would not discharge it,because every migrated driver opens
kFulland so nothing in this tree re-beginsa capture mid-forward. It needs the W1 probe itself, or the first PIECEWISE
driver, which is W6.
Closes #1335. Closes #1352. Closes #1361. Parent #1163 stays open: W6 moves the eligibility predicate at
runner.cpp:1341offpure_decodeand unblocks the PIECEWISE arm, and the rowstill owns the async decline (#1179, #323), #1305, and G5's non-CUDA arm.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]