Skip to content

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
localai-bot merged 13 commits into
mainfrom
row/ENG-CUDAGRAPH-BREAK-W5
Aug 19, 2026
Merged

Conversation

@localai-bot

@localai-bot localai-bot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

W5 is the LAST migration stage of ENG-CUDAGRAPH-BREAK. It moves the three
SINGLE-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/ 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 stated rather than a
cleaner-sounding sentence.

Gone with them: three raw void* handles, two hand-written destructors that
released them, and the DFlash driver's private try/drain.

The three drivers

Each Step opens a vt::GraphCaptureScope over its own vt::BreakableGraph in
kFull and replays through BreakableGraph::Replay.

  • DFlash draft graph (qwen3_dflash.cpp) — the (1+k) paged draft step of
    the speculator.
  • DeepSeek V4 decode graph (deepseek_v4.cpp) — the T=1 resident decode step.
  • Laguna decode graph (laguna.cpp) — whose own note at :2116-2119 asked
    for 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_NVFP4 CUDA code, not the capture.

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 decode_mode()
(:65-66) returns the full half. 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.

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 for
exactly one driver. What changes for a user is that VLLM_CPP_CUDAGRAPH reaches
all three for the first time
— before this, none of them read it, so no single
setting turned capture off everywhere. docs/USAGE.md says so.

A FAILED capture now propagates in all three instead of returning its buffer.
Under stream capture nothing between BeginCapture and a throwing
EndCaptureGraph executed — the kernels were RECORDED, not run — so those
buffers 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

GraphCaptureScope owns the set of side queues forked since the current segment
opened and joins every outstanding one before Backend::EndCaptureGraph — the
port of _end_current_segment (breakable_cuda_graph.py:353-361 @ SGLang pin
f63458b5be) plus the wait_stream hook (:101-153) whose only purpose is to
populate 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 segment
CLOSE 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 only
fork inside a captured region by construction, and that driver is the rule's
production caller
through vt::GraphNoteFork and vt::GraphNoteJoin. No
monkey-patch is needed, because our fork is an explicit RecordEvent /
QueueWaitEvent pair rather than an implicit torch call — the model tells the
scope.

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::GraphBreakStats gains forks_tracked and
forks_auto_joined, and the claim is an ORDER — closing a capture with an
unjoined fork FAILS at cudaStreamEndCapture, so "the join happened BEFORE the
close" 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 model
joins first and the scope must do nothing, kFull, the inert scope, and
re-registering one queue. Two mutations, both compiled clean, both with the
diff stat recorded
: deleting JoinOutstandingForks() reds ONLY the new case (5
assertions, exit 1, 29 of 30 cases still passing), and making NoteJoin fail to
retire the entry reds it on 8 — so neither the rule nor its control is
decoration. test_breakable_graph 30 cases / 265 assertions / exit 0.

tests/vllm/models/test_qwen3_dflash_decode_graph_seam.cpp, on the shared
harness. 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 Replay with the pre-W5 raw pair (compiled clean, 79
insertions / 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 last
one 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 rc lease on thor:gpu0 — NVIDIA Thor, sm_110, driver
595.78, nvcc 13.0.88, -DVLLM_CPP_CUDA=ON -DVLLM_CPP_CUDA_ARCHITECTURES=110, 32
.cu.o objects, 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 seam
changed 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_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.

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 "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.
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. Then 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.

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_CUDAGRAPH itself — so the driver's admission predicate gained a state
it did not know about. With that switch set to 0 on a capture-capable backend
the 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_captured at 0, so
no 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::Alloc read
control=11, switch-off=0, and would have read those 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. Copies are per-operation work the pool cannot absorb.
Red-first on the unfixed driver: DIFFERENT-LANE, control copies=4 against
switch-off copies=5, exit 1 — and the same run reports the logits IDENTICAL.

What is OWED, stated per driver because the obstacles differ

  • DFlash owes only G1, for the reason every CPU-gated driver in this row owes
    it: a CPU "replay" recomputes nothing.
  • DeepSeek V4 owes G1 AND G2. CanRunResidentDecode refuses a CPU queue
    outright and refuses again without the four V4 kernel families registered under
    kCUDA. The 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. This migration is covered by CONSTRUCTION, which is
    an argument and not a gate.
  • Laguna owes both for a BUILD reason: its capture class is inside
    #ifdef VT_MARLIN_NVFP4, so on a host with no nvcc the migrated region is in
    no 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 devices lists dgx:gpu0,
orin:gpu0 and thor:gpu0, all NVIDIA — and no ordering of the remaining work
produces 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-DEDUP closed its benefit question negative after measurement.
kFull keeps 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 is
neither a GB10 nor sm_121a, so the sentence 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 criterion ran on orin:gpu0, G1 and the unit suite on thor:gpu0 at sm_110.

sm_121a on GB10 is OWED and now has its own entry, carrying the reason it
cannot be closed cheaply: re-running G1 on dgx:gpu0 would not discharge it,
because every migrated driver opens kFull and so nothing in this tree re-begins
a 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:1341 off pure_decode and unblocks the PIECEWISE arm, and the row
still 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]

mudler added 13 commits August 19, 2026 11:27
…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]
@localai-bot
localai-bot merged commit 601b576 into main Aug 19, 2026
1 check failed
@localai-bot
localai-bot deleted the row/ENG-CUDAGRAPH-BREAK-W5 branch August 19, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants