Skip to content

fix(ENG-CUDAGRAPH-BREAK): three registrations never read the async mirror's identifiers, on the graph path and the eager path alike (#1305) - #1391

Merged
localai-bot merged 14 commits into
mainfrom
row/ENG-CUDAGRAPH-DEVIDS
Aug 19, 2026
Merged

fix(ENG-CUDAGRAPH-BREAK): three registrations never read the async mirror's identifiers, on the graph path and the eager path alike (#1305)#1391
localai-bot merged 14 commits into
mainfrom
row/ENG-CUDAGRAPH-DEVIDS

Conversation

@localai-bot

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

Copy link
Copy Markdown
Collaborator

Three registrations routed an asynchronous step into a model that never read
ModelForwardInput::device_token_ids, so the decode graph AND both eager arms
embedded identifiers the runner deliberately leaves stale.

#1305 reads as a decode-graph hazard: qwen3_moe_registry.cpp,
deepseek_v2_registry.cpp and glm4_moe_lite_registry.cpp admit a pure-decode
step to a driver that replays against a host vector, while qwen3.cpp declines
for exactly that condition. Reading the tree found something larger. Those three
registrations never constructed a detail::DeviceTokenIdsScope, and neither
qwen3_moe.cpp's nor deepseek_v2.cpp's EmbedInto ever consulted one, so
device_token_ids reached NOTHING in either translation unit. On the
asynchronous serving path the runner's combine splices each decode row's sampled
token into the DEVICE identifiers on the main queue and leaves the host
token_ids stale by design — the synchronize that path exists to remove — and
the mirror arm is the default. Both models therefore generated from the previous
step's identifiers for every decode row, on the graph path and on the eager path
alike. No decline could have mitigated the eager half, which is why the fix here
is the consumption and not the refusal.

What lands

The three registries publish the scope, which is the mechanism qwen3.cpp,
qwen3_5.cpp, mistral_registry.cpp, internlm2_registry.cpp and
llama_registry.cpp already use, so every embed in both translation units
consumes it.

The two decode-graph drivers take the version of the fix the issue asks for
rather than a fifth private copy. Each padded size slot now owns a
vllm::StepTokenIds (include/vllm/model_executor/models/step_token_ids.h),
whose destination is a device buffer with an address that does not move and whose
refresh runs through vt::PersistentStepInput: the host arm for the padded
vector, then the DEVICE arm over the real prefix, both enqueued on the main queue
so the second is ordered after the combine instead of racing it. That gives
RefreshFromDevice its first production caller — W4 landed it with none and named
the gap under ## Owed.

last_source() is NOT given a reader, and an earlier draft of this body said it
was. Every caller of last_source() or StepInputSource is a test; what the
gate reads is the process-wide vt::GetStepInputStats(). Five of StepTokenIds'
six accessors have no caller in src/ or tests/, and the header now says so
rather than leaving a reader to assume the counters it exposes are the ones under
test.

The consumer side is written ONCE. Taking the scope and splicing it over an
embed's device buffer is four lines plus five, and qwen3.cpp and qwen3_5.cpp
each spelled both out; this change first added a third and a fourth copy, in a
row whose stated purpose is deleting them. detail::TakeDeviceTokenIds and
detail::ApplyDeviceTokenIds now sit in qwen3_5_internal.h beside the
DeviceTokenIdsScope that publishes what they read, and all four models call
them. The refusal messages keep their per-caller wording, so a shape disagreement
still names the model it came from.

What this deliberately does NOT do

qwen3.cpp's decline is untouched. W4 (#1307) measured that decline's recorded
CAUSE false, so its failure mode is unexplained, and a refactor does not retire a
mitigation whose mechanism nobody can name. Removing it still needs the two
battery runs the spec records.

The embed still sits OUTSIDE the captured region in every driver, because
vt::Embedding allocates a device flag and synchronizes the stream. The
identifiers are therefore read once per step from a stable device address, not
from inside the replay. StepTokenIds is the destination the inside-the-capture
change would need; it is not that change. Two production comments that stated the
older facts — persistent_step_input.h on RefreshFromDevice having no caller,
and qwen3.cpp's decline on the fix existing in no driver — are corrected here,
because a record edit rides in the pull request whose change made it stale.

Evidence

tests/vllm/models/test_moe_async_device_ids.cpp enters at
ModelRegistry::Forward over a synthetic safetensors checkpoint. A case that
drove the driver directly would measure the type rather than the registration.

Three runs per case, because two of them cannot separate the cases: right host
identifiers and no mirror as the reference; stale host identifiers and no mirror
as the CONTROL, which must differ; the same stale host vector with the truth
reaching the model ONLY through device_token_ids as the gate, which must be
bit-identical to the reference. Without the control, a model that ignored its
identifiers entirely would satisfy the gate.

RED before the change, for the intended reason: 2 cases, 65 assertions, 10
failed, exit 1, with 800 of 800 logit values differing over four steps on both
architectures and every seam counter at 0. GREEN after: 65/65, exit 0.

A fresh review then proved that gate covered half of what this change claims,
by mutation rather than by reading, and the repair is in this branch.
Deleting
the TakeDeviceTokenIds + d.b.Copy block from BOTH
EmbedInto(const std::vector<int32_t>&) overloads — restoring the pre-fix EAGER
behaviour, the half this body calls the reason the fix is a consumption — left
the gate green at 2/2 cases and 65/65 assertions. Deleting the two-line
DeviceTokenIdsScope from glm4_moe_lite_registry.cpp, the THIRD of the three
registries this body says publish a scope, did too; the original reachability
mutation had covered only two.

The gate is now 6 cases, 191 assertions, exit 0: both lanes for all three
registrations, through one A/B/C helper. The lane is chosen by the registry's OWN
predicate rather than by the test — a case that constructs StaticGraphCpu gets
the decode graph, a case that does not gets ForwardDevice — and through_seam
asserts the vt::PersistentStepInput counters BOTH ways, moving on the graph
lane and at zero on the eager one, so a case cannot drift onto the other lane and
stay green. GLM-4-MoE-Lite gets its own fixture rather than a claim of coverage:
it shares the driver, the model and the weights struct with DeepSeek-V2, so the
only thing it owns is its scope.

Four mutations, each with its compile status printed:

mutation compile exit cases what reds
delete both eager EmbedInto consumers rc=0 1 3 of 6 pass the 3 EAGER cases, on differing == 0
delete glm4_moe_lite_registry.cpp's scope rc=0 1 4 of 6 pass the 2 GLM cases only
delete StepTokenIds::Refresh's RefreshFromDevice rc=0 1 3 of 6 pass the 3 GRAPH cases, on device_refreshes AND differing == 0
shared ApplyDeviceTokenIds copies 0 bytes rc=0 1 3 of 6 pass the 3 EAGER cases — the hoisted body is reached

A fifth deleted that shared copy outright and FAILED TO BUILD under
-Wunused-parameter. Its verdict was DISCARDED, not read as a pass.

Neighbours green on the same binary after merging origin/main at 96ed8346f:
test_qwen3_moe_decode_graph_seam 228, test_deepseek_v2_decode_graph_seam 230,
test_qwen3_decode_graph_seam 231, test_persistent_step_input 66,
test_model_registry 924, test_qwen3_moe_forward 504,
test_deepseek_v2_forward 1052, test_glm4_moe_lite_load 15 — all 0 failed,
exit 0. test_qwen3_dense_async_serving exits 0 over 8 cases and 0 assertions,
which is a skip wearing a pass: it needs checkpoints this box does not have.
Component checkers green: check-agent-record,
check-issue-index-append-only, check-surface-coverage,
check-test-registration, check-symbol-anchors, check-public-doc-tables,
check-runner-routing-consistency, check-fusion-consistency,
check-device-leakage, check-now-current, check-doc-checkpoint,
check-gate-commands, check-role-discipline, check-prompt-contract,
check-commit-style, check-commit-trailers.

Continuous integration is REMOTE_UNVERIFIED: every check on this branch reads
pending and none starts (#1376).

What is still owed, and why #1305 does NOT close here

The issue splits, and only one half settles. The EAGER half is fixed and
gated on all three registrations and deserves to close. The GRAPH half does not,
and the reason is sharper than "the battery did not run": the mechanism these two
drivers now have is functionally what qwen3.cpp ALREADY HAD at 338cbbfd1^ — a
registry scope, consumed by EmbedInto, copying the mirror's identifiers over the
embed source OUTSIDE the capture — and W4 recorded at qwen3.cpp:1083-1095 that
the depth-2 graph-ON battery STILL FAILED with exactly that in place. A stable
device address buys nothing while the embed stays outside the capture, which this
body concedes two sections above.

The depth-2 four-concurrent battery against Qwen3-Coder and DeepSeek-V2-Lite on a
real device — #1305's own settlement condition — has NOT been run, and the reason
is a fleet state rather than an intention: at 2026-08-19 rc devices read
dgx:gpu0 busy, the only box whose HuggingFace cache carries the checkpoint,
while thor:gpu0 and orin:gpu0 were ready and carry none. So #1305 stays
open
, with the ENG-CUDAGRAPH-BREAK row as owner and the residual recorded in
the spec's ## Owed.

The DEVICE half of the refresh contract is untested on any device, and the gate's
own opening comment used to say the opposite. On CPU vt::Backend::Alloc returns
HOST-addressable memory, so both refresh arms reduce to the same memcpy from the
same address; swapping the device arm for the host arm leaves every logit
bit-identical at 0 of 800 differing and reds only the
device_refreshes/host_refreshes counters. Those counters are a legitimate
stand-in for which arm ran and they are what the file asserts, but they gate the
INSTRUMENT, not the behaviour. Owed with the battery, same window, same owner.

Found on main while doing this, and not caused by it

test_qwen3_5_decode_graph_seam exits 139 at 5f68e60df, which was origin/main
exactly, while its assertion line reads 135 of 135 passed and 0 failed. Only the
exit status and a CRASHED: SIGSEGV line say otherwise. Re-measured on this
branch: exit 139 at the SAME crash case and site
(test_qwen3_5_decode_graph_seam.cpp:800, W6: two spec shapes of EQUAL S and different q get two graphs) both with and without this branch's working-tree
changes, and its printed counts are not reproducible — three consecutive runs of
ONE unchanged baseline binary gave 6 passed with 2 failed and 141 assertions,
then no summary at all, then no summary at all. The exit code is the only stable
observation there. Filed as #1390 with ENG-CUDAGRAPH-BREAK as the owner and
recorded under ## Owed; that issue stays open.

Records

.agents/specs/eng-cudagraph-break.md ## Owed, ## Now and ## Outcome,
.agents/engine-matrix.md, and one appended .agents/issue-index.md row for
#1390. No lifecycle state moved, so docs/STATUS.md and docs/BENCHMARKS.md
owe nothing.

An earlier draft of this section said the engine-matrix issue cell "listed #1305
twice" at the base. It did not. The cell carries #1305 ONCE at the merge base and
once at origin/main; the duplicate was this branch's own, introduced by
831f8d3ce and removed by 91087e670. That sentence attributed a self-inflicted
transient to the base, and the net state is one mention, unchanged.

Repaired after a fresh re-review

The re-review returned PASS_WITH_FINDINGS on the code — all six production call
sites proved individually, the four-way hoist behaviour-identical to every
original, baseline 6 cases / 191 assertions / exit 0 — plus one blocking record
defect. It is repaired here, and no source line moves.

The blocking one. ## Owed said last_source() and StepInputSource "gain
their reader with it". They do not, and this change already says so in three
other places: both headers state it, and this body retracts it below.
Re-derived rather than assumed — grep -rn 'last_source()' src/ returns NOTHING,
the only readers of a value are six CHECKs in
tests/vt/test_persistent_step_input.cpp, and step_token_ids.h:122 forwards to
cell_.last_source() but is itself never called. What gains a production caller
is RefreshFromDevice; the arm observable stays unread. A record asserting a
reader exists is precisely the drift the two header repairs in this branch were
made to end, which is why it blocked.

Four smaller ones. include/vt/persistent_step_input.h said "every caller of
either is a test", false by exactly one — that forwarding wrapper — and now says
every caller that reads a VALUE is a test, naming the wrapper. The #1390
measurement was recorded three ways: the spec presented 8 cases, 7 passed, 1 failed, 135 of 135 as the measurement while this body and the engine-matrix row
already carried the correction that those printed counts are not reproducible.
The spec now agrees with them and states the general rule plainly: on a
crashing suite no assertion count means anything, because the process dies before
the harness totals it — only the exit code carries a verdict.

.agents/issue-index.md is append-only and cannot be edited, so it still shows
the original numbers; the spec and the issue are named as the authority over that
row. And a stray .; in .agents/engine-matrix.md field 7, where the appended
#1305 block was concatenated onto the #1380 sentence.

Two are RECORDED as bounded residuals in ## Outcome rather than repaired.
The shape-refusal hoist moves __FILE__/__LINE__ to qwen3_5.cpp:562 for all
four refusals, so three of the four callers now name the wrong file; no test
asserts those strings (grepped for the message and for each of the four what
values), the what prefix still carries caller identity, and the audience is
somebody reading one refusal out of a log. The two rewritten call sites in files
this branch was not repairing (qwen3.cpp:213, qwen3_5.cpp:7829) are covered
only by checkpoint-gated skips reporting assertions: 0, and
test_qwen3_decode_graph_seam.cpp:341-349 gates the DECLINE rather than the
consumption; the shared body they call IS gated, so the ungated surface is two
argument lists, the gap pre-dates this branch, and net the hoist IMPROVES
coverage, because a defect in the shared body now reds
test_moe_async_device_ids.

Advances #1305, which stays open on the battery above.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]

mudler added 8 commits August 19, 2026 18:02
…ector and never read the device mirror (#1305)

`qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and
`glm4_moe_lite_registry.cpp` route a step into a model that ignored
`ModelForwardInput::device_token_ids` entirely. On the asynchronous serving path
the runner's combine splices each decode row's sampled token into the DEVICE
identifiers on the main queue and leaves the host `token_ids` deliberately stale
(`src/vllm/v1/worker/gpu/runner.cpp`, the mirror arm, which is default ON), so
those three models embedded the previous step's identifiers for every decode row
— on the decode-graph arm AND on both eager arms.

The three registries now publish `detail::DeviceTokenIdsScope`, the same
mechanism `qwen3.cpp`, `qwen3_5.cpp`, `mistral_registry.cpp`,
`internlm2_registry.cpp` and `llama_registry.cpp` already use, so every embed in
the two model translation units consumes it.

The decode-graph drivers take the version of that fix `#1305` asks for rather
than a fifth private copy: each padded size slot now owns a
`vllm::StepTokenIds`, whose destination is a device buffer with a stable address
and whose refresh runs through `vt::PersistentStepInput` — the host arm for the
padded vector, then the DEVICE arm over the real prefix, both enqueued on the
main queue so the second is ordered after the combine instead of racing it. That
gives the capability its first production caller of `RefreshFromDevice`, which
W4 landed with none, and it gives a step that re-read the mirror an observable
that separates it from one that uploaded a stale vector: no token gate and no
segment count can tell those two apart.

WHAT THIS DOES NOT DO. It does not remove `qwen3.cpp`'s decline. W4 (#1307)
measured that decline's recorded CAUSE false, so its failure mode is unexplained
and a refactor does not retire it. The embed still sits OUTSIDE the captured
region in every driver, because `vt::Embedding` allocates a device flag and
synchronizes the stream, so the identifiers are read once per step from a stable
device address rather than from inside the replay; `StepTokenIds` is the
destination that future change needs, not that change.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
… ModelRegistry::Forward (#1305)

The defect #1305 names is SILENTLY WRONG TOKENS at concurrency, not a fault, so
the gate asserts the identifiers themselves rather than a class that constructs.
It enters at `ModelRegistry::Forward` over a synthetic safetensors checkpoint,
which is the production entry point a user arrives through; a case that drove
the driver directly would measure the type and not the registration.

Three runs per architecture, because two of them cannot separate the cases: the
reference has the right host identifiers and no mirror; the CONTROL has stale
host identifiers and no mirror and must DIFFER; the gate has the same stale host
vector with the right identifiers reaching the model ONLY through
`ModelForwardInput::device_token_ids` and must be bit-identical to the
reference. Without the control, a model that ignored its identifiers entirely
would satisfy the gate.

`vt::StepInputStats::device_refreshes` carries the other half. It moves only
inside `vt::PersistentStepInput::RefreshFromDevice`, so a driver that hand-rolled
the same copy would produce identical logits and leave it at zero — and a step
that re-read the mirror and one that uploaded a stale host vector leave the same
bytes-shaped destination, which no token gate can separate.

RED before the fix, for the intended reason: 2 cases, 59 assertions, 12 failed,
exit 1, with 200 of 200 logit values differing per step on both architectures and
every counter at 0. GREEN after: 59/59, exit 0.

Bounded honestly. A CPU "replay" recomputes nothing
(`decode_graph_seam_harness.h`), so only the cold and capture steps carry
information and only those two are compared. The depth-2 four-concurrent battery
on a real device is a different gate; it needs a GPU and a checkpoint, and the
spec records it as owed.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…ain gate nobody had read (#1305, #1390)

The spec's `## Owed` carried #1305 as "three registrations admit an asynchronous
step with NO decline", owned by a stage that would get a `dgx` window. Reading
the tree found a larger defect than the issue described and a fix that needs no
decline at all, so the entry records the resolution, what the fix actually was,
and the one thing still owed rather than being struck.

The `RefreshFromDevice` entry is retired: it landed with no production caller and
now has one, in both migrated drivers, reached from `ModelRegistry::Forward`.

A new entry, and it is a red `main` gate rather than this row's work:
`test_qwen3_5_decode_graph_seam` exits 139 at `5f68e60df`, which is `origin/main`
exactly, while its assertion line reads 135 of 135 passed. The number a reader
greps says the suite is green. It is order-dependent — the case passes alone —
and `gdb` puts the fault inside the CPU paged-attention kernel on a threadpool
worker. Filed as #1390 with `ENG-CUDAGRAPH-BREAK` as the owner, not fixed in
flow: a segmentation fault in another stage's newly landed code, mechanism
unlocated, in a file under concurrent edit for #1380.

No lifecycle state moved, so `docs/STATUS.md` and `docs/BENCHMARKS.md` owe
nothing.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…buffer from the backend (#1305)

Two changes that cost nothing on CPU and are what make this file the device gate
the moment it runs on one.

The mirror's identifiers now live in a real `vt::Backend::Alloc` block rather
than at the host vector's address. On CPU the two are the same thing. On a device
they are not, and a host address there is the wrong kind of pointer for a field
the runner's combine writes.

Every step is compared instead of the first two. On CPU a "replay" recomputes
nothing, so steps 2 and 3 hold what step 1 produced and the comparison is true
for that reason; on a device a replay recomputes, and those two steps become the
assertion the reported defect is actually about — that a REPLAY does not generate
from stale identifiers.

Re-measured, and the records carry the new numbers: RED at 2 cases / 65
assertions / 10 failed / exit 1 with 800 of 800 values differing on both
architectures, GREEN at 65/65 exit 0. Deleting the registry's scope line reds 4
assertions and puts all 800 values back; swapping the seam's DEVICE arm for its
HOST arm leaves the logits bit identical at 0 of 800 and reds only the counters.

The spec's `## Owed` now names why the device battery did not run as a fleet
state rather than as an intention: `dgx:gpu0`, the only box carrying the
checkpoint, read busy, and the two ready devices carry none.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…ch nothing gated (#1305)

The gate this file shipped with covered two decode-graph drivers and left the
half of #1305 the pull request called its most important finding completely
untested. A fresh review deleted the `TakeDeviceTokenIds()` + `d.b.Copy` block
from BOTH `EmbedInto(const std::vector<int32_t>&)` overloads -- restoring the
pre-fix eager behaviour in `qwen3_moe.cpp` and `deepseek_v2.cpp` -- and the
binary stayed green at 2/2 cases and 65/65 assertions. It also deleted the
two-line `DeviceTokenIdsScope` from `glm4_moe_lite_registry.cpp`, the third of
the three registries the body claims to change, and that stayed green too.

So this adds four cases and routes all six through one A/B/C helper.

The EAGER lane is entered by NOT constructing `StaticGraphCpu`: a plain CPU
platform answers `support_static_graph_mode()` false, so the registry's own
predicate falls through to `ForwardDevice` and `ForwardBody` embeds from the
host vector. That is the lane every non-CUDA and every non-pure-decode step
takes, and it is the lane no graph refusal could ever have mitigated, which is
why the fix for it had to be the consumption rather than a decline.

The THIRD registration gets its own fixture rather than a claim. GLM-4-MoE-Lite
shares `DeepseekV2DecodeGraph`, `DeepseekV2Model` and the weights struct with
DeepSeek-V2 down to the loader, so the only thing it owns is its own scope --
and deleting that one scope leaves both DeepSeek cases green. `DsConfigJson`
therefore takes the architecture, and the same geometry serves both.

`through_seam` is the lane-identity assertion, not decoration. The graph driver
refreshes a `vllm::StepTokenIds` and moves `vt::PersistentStepInput`'s
process-wide counters once per step; the eager arms copy the override straight
over their own per-step `DBuf` and must never touch that seam. Asserting the
counters BOTH ways means a change that quietly moved a case onto the other lane
could not keep it green.

The file's opening comment also said the device contract is "directly testable
here" because a host pointer is device-addressable on CPU. That is the wrong way
round: both refresh arms reduce to the same memcpy from the same address there,
which is exactly why the DEVICE half is NOT testable on this backend. The
counters stand in for which arm ran, and they gate the instrument rather than
the behaviour. Corrected, with the residual named.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…and two comments this change had made false (#1305)

Three repairs a fresh review asked for, none of which changes behaviour.

FOUR PRIVATE COPIES BECOME ONE. Taking the scoped override and splicing it over
an embed's device buffer is four lines plus five, and `qwen3.cpp` and
`qwen3_5.cpp` each spelled both out. #1305 then added a third and a fourth, in
`qwen3_moe.cpp` and `deepseek_v2.cpp` -- in a row whose stated purpose is
deleting hand-rolled copies. `detail::TakeDeviceTokenIds` and
`detail::ApplyDeviceTokenIds` now sit in `qwen3_5_internal.h` beside the
`DeviceTokenIdsScope` that publishes what they read, defined in `qwen3_5.cpp`
beside `DeviceTokenIdsOverride()`. All four models call them. The refusal
messages keep their per-caller wording through `what`, so a shape disagreement
still names the model it came from.

TWO PRODUCTION COMMENTS WERE LEFT FACTUALLY FALSE by the change that landed,
and a record edit rides in the pull request whose change made it stale.
`persistent_step_input.h` still told the reader that `RefreshFromDevice` lands
with NO production caller and that grepping for it returns its definition and
nothing else. `step_token_ids.h:102` is that caller, reached by three shipped
registrations. And `qwen3.cpp`'s decline still said the fix it names "DOES NOT
EXIST IN ANY DRIVER" and that every batched driver embeds from the host vector;
two of the nine now hold their identifiers in a `vllm::StepTokenIds`.

Both are corrected to what is now true, and both keep naming what is still
owed, because the half that matters did not change: the refresh runs OUTSIDE
the capture in every driver that has it, so reading the identifiers at REPLAY
time -- the decline's own wording for the fix -- exists nowhere. The decline in
`qwen3.cpp` is untouched. W4 measured its recorded cause false, so its mechanism
is unexplained, and a refactor that plausibly addresses an explanation nobody has
confirmed does not retire it.

`StepTokenIds` also now says which of its accessors nothing reads, rather than
leaving a reader to assume the counters it exposes are the ones under test. Five
of its six have no caller; what the gate reads is the process-wide
`vt::GetStepInputStats()`.

Evidence, all on the CPU gate lane:
  green            6/6 cases, 191/191 assertions, exit 0
  M4b (delete both `ApplyDeviceTokenIds` call sites, the post-hoist form of the
       review's M4)      compile_rc=0, exit=1, 3/6 cases, 3 EAGER cases red on
                         `differing == 0`
  MHELPER (shared body copies 0 bytes)
                         compile_rc=0, exit=1, 3/6 cases, the same three red
An earlier MHELPER that deleted the copy outright FAILED TO BUILD on
-Wunused-parameter and its verdict was discarded, not read as a pass.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
… before the final gate (#1305)

`origin/main` moved to `96ed8346f` while this branch was under fresh-implementer
repair: `edfa62b3c` (SPEC-DFLASH2) and `96ed8346f` (ENV-LEASE-CLOCK-PINNING).
Neither touches the device-token-id path, but merge-tree clean is not merge-tree
builds, so the branch takes the merge before its gate run rather than after.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…half does not (#1305)

The records said #1305 CLOSED. It should not, and the reason is sharper than
"the battery did not run".

The issue SPLITS. The EAGER half -- the half no graph refusal could ever have
mitigated, and the half the landing change called its most important finding --
is fixed on all three registrations and is now gated on both lanes. That half
deserves to close.

The GRAPH half does not. The mechanism `Qwen3MoeDecodeGraph` and
`DeepseekV2DecodeGraph` now have is functionally what `qwen3.cpp` ALREADY HAD at
`338cbbfd1^`: a registry scope, consumed by `EmbedInto`, copying the mirror's
identifiers over the embed source OUTSIDE the capture. W4 recorded at
`qwen3.cpp:1083-1095` that the depth-2 graph-ON battery STILL FAILED with
exactly that in place. A stable device address buys nothing while the embed
stays outside the capture, which the change itself concedes. So landing it is
not evidence that the degeneration is gone, and #1305's own settlement condition
is that battery, against Qwen3-Coder and DeepSeek-V2-Lite, which did not run.

#1305 stays OPEN with the `ENG-CUDAGRAPH-BREAK` row as owner, the pull request
references it without a closing keyword, and `qwen3.cpp`'s decline stands.

Also recorded, because a fresh review measured them and nothing in the tree
said so:

  * The gate that landed covered half of what the change claims. Deleting the
    consumer from BOTH eager `EmbedInto` overloads left it green at 2/2 and
    65/65; deleting the third registry's scope did too. Repaired to 6 cases /
    191 assertions / exit 0, with the three detecting mutations tabulated.
  * The DEVICE half of the refresh contract is untested on any device. On CPU
    `Backend::Alloc` returns host-addressable memory, so both refresh arms are
    the same memcpy from the same address; swapping the device arm for the host
    arm leaves the logits bit-identical and reds only the counters. Those
    counters gate the instrument, not the behaviour, and the entry now says so
    instead of implying coverage.
  * `test_qwen3_5_decode_graph_seam` (#1390) re-measured on this branch: exit
    139 at the same crash case and site with and without this branch's changes,
    and its printed counts are not reproducible across three runs of ONE
    unchanged binary. Only the exit code carries a verdict there.

And one duplicate removed: the row's issue cell listed #1305 twice.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
mudler added 2 commits August 19, 2026 21:51
…RAPH-DEVIDS (#1305)

`origin/main` moved again while this branch's records were being written:
`5f4eb356e` (ENG-EXPERT-STREAM-DEVICE W0b-W0d). It touches the expert-streaming
slot arm and the platform surface, not the device-token-id path, but a clean
merge is not a build, so the branch takes it before the gate re-runs.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…RAPH-DEVIDS (#1305)

The third move of `origin/main` during this repair: `7265cea08`
(SPEC-PROMPT-TOKEN-DIVERGENCE), which is spec text and touches no file under
`src/` or `include/`. Taken so the branch's gate ran against the head it will
land on rather than one behind it.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
mudler added 3 commits August 19, 2026 22:44
…nge does not give (#1305, #1390)

A fresh re-review returned PASS_WITH_FINDINGS on the code and one blocking
record defect. This repairs the records only; no source line moves.

The blocking one: `## Owed` said `last_source()` and `StepInputSource` "gain
their reader with it". They do not, and this same change says so in three other
places — both headers state it, and the pull-request body explicitly retracts an
earlier draft that claimed it. The `## Owed` entry had kept the retracted
wording. Re-derived rather than assumed: `grep -rn 'last_source()' src/` returns
NOTHING, the only readers of a value are six `CHECK`s in
`tests/vt/test_persistent_step_input.cpp`, and `step_token_ids.h:122` forwards to
`cell_.last_source()` but is itself never called. A record asserting a reader
exists is precisely the drift the two header repairs in this branch were made to
end, which is why it blocks rather than rides along.

Four smaller ones. `persistent_step_input.h` said "every caller of either is a
test", false by exactly one — that forwarding wrapper — so it now says every
caller that reads a VALUE is a test and names the wrapper. The #1390 measurement
was recorded three ways: the spec presented `8 cases, 7 passed, 1 failed, 135 of
135` as the measurement while the matrix and the pull-request body already
carried the correction that those printed counts are NOT reproducible on one
unchanged binary. The spec now agrees with them and states the general rule
plainly: on a crashing suite no assertion count means anything, because the
process dies before the harness totals it, and only the exit code carries a
verdict. `.agents/issue-index.md` is append-only and still shows the original
numbers, so the spec and the issue are named as the authority over that row. And
a stray `.;` where the appended #1305 block was concatenated onto the #1380
sentence in `engine-matrix.md` field 7.

Two findings are RECORDED as bounded residuals in `## Outcome` rather than
repaired, each with what bounds it. The shape-refusal hoist moves
`__FILE__`/`__LINE__` to `qwen3_5.cpp:562` for all four refusals, so three of the
four callers now report the wrong file; no test asserts those strings (grepped
for the message and for each of the four `what` values), the `what` prefix still
carries caller identity, and the audience is somebody reading one refusal from a
log. The two rewritten call sites in files this branch was not repairing
(`qwen3.cpp:213`, `qwen3_5.cpp:7829`) are covered only by checkpoint-gated skips
reporting `assertions: 0`; the shared body they call IS gated, so the ungated
surface is two argument lists, the gap pre-dates this branch, and net the hoist
improves coverage because a defect in the shared body now reds
`test_moe_async_device_ids`.

#1305 stays OPEN, owned by `ENG-CUDAGRAPH-BREAK`, pending the depth-2
four-concurrent battery on a `dgx` window with checkpoints.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
…RAPH-DEVIDS (#1305)

Keeps the branch current before the records repair is pushed. The incoming
commit is #1418's SGLANG-ORACLE-LEASE-WHEEL spec, which touches five files this
branch does not: `.agents/environment.md`, `.agents/oracles/sglang.md`,
`.agents/sglang-matrix.md` and the two `sglang-wheel-in-lease` spec files. The
two file sets are disjoint, so nothing is re-applied and no keyed record is
three-way merged.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
… left ragged (#1305)

Prose only, in the two `## Owed` passages the previous commit edited. The
`last_source()` correction had left `through` alone on its own line, and the
#1390 correction had run one line to 127 columns and another to 140 by joining
new text onto the sentence that followed it. The ORDER-DEPENDENT observation is
now its own paragraph, which is what it always was. No claim, anchor or number
changes.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
localai-bot added a commit that referenced this pull request Aug 19, 2026
… STEP needs, not what one tensor is (#1380) (#1393)

A speculative decode-graph capture did a `cudaMalloc` inside the
captured region
on `thor:gpu0` (sm_110) and threw, so a replay was unreachable on a
default-ON
path (`VT_SPEC_DECODE_GRAPH`) and the queue was poisoned afterwards.

## The call site, named rather than inferred

`cudaMalloc: operation not permitted when stream is capturing` names the
API and
no call site, so a backtrace was taken AT the failing allocation with
the CUDA
backend's own `Alloc` instrumented and the binary built `-g`. It
resolves to
`DBuf` construction inside `GdnBlockPaged` -- `dconv`, the GDN
causal-conv output
in `src/vllm/model_executor/models/qwen3_5.cpp` -- reached through
`RunDenseLayerPaged` and `DenseForwardLayers` from the
`GraphCaptureScope` in
`Qwen3_5DenseDecodeGraph::Step`. The request was **960 bytes**, which at
the
gate's synthetic shape is BOTH `[T=6, conv_dim=80]` bf16 and `[S=6,
vocab=40]`
f32.

That coincidence is the defect. `DevicePool` hands out blocks by SIZE
CLASS and
knows nothing about tensors. The driver pre-grew by allocating and
freeing ONE
`[S, vocab]` f32 block, reasoning that the capture retains its logits
while the
other ring slot still holds its own. The retention half is right; the
"one block
of that shape" half is not. A `VT_POOL_TRACE_CLASS` trace on the device
measured
the forward holding TWO blocks of that class live at once -- the GDN
projection
output and the causal-conv output it feeds -- inside a region where a
miss aborts
the capture.

**And the pre-grow supplied nothing at all**, which is sharper than "one
block".
An alloc-and-free only reaches the driver when that class's free list is
EMPTY.
At the second ring slot's capture the class had ONE block free, because
the first
slot had retained the other, so the pre-grow took that block, returned
it, and
grew the pool by zero against a demand of two.

## The fix

`DevicePool` now measures, per size class, the PEAK number of blocks a
step holds
live above the baseline it started from -- the transient demand of the
FORWARD,
not of whatever was resident when it ran -- and `PreGrowForCapture`
makes the
free list able to serve that profile before `BeginCapture`, where a
`cudaMalloc`
is legal. Both Qwen3.5 decode drivers record the profile at the end of
their COLD
step, per SLOT, because two shapes interleave through one pool and
last-step pool
state would answer for whichever step ran most recently. It is
idempotent and a
no-op once the free list is deep enough, which is the steady state on a
warm
server.

## Measured on `thor:gpu0` (sm_110, driver 595.78, nvcc 13.0.88,
`-DVLLM_CPP_CUDA=ON -DVLLM_CPP_CUDA_ARCHITECTURES=110
-DVLLM_CPP_TRITON=OFF`)

RED at `9c27fb9f5`, the case alone in a fresh process -- 752 assertions,
2
failed, exit 1:

```
spec step 2: captured=1 threw=''
spec step 3: captured=1 threw='vt cuda: cudaMalloc: operation not permitted when stream is capturing'
spec step 4: captured=1 threw='vt cuda: embedding: operation failed due to a previous error during capture'
```

GREEN at `ffeea2172`, the whole G1 file -- **6 cases, 3306 assertions, 0
failed**, the five pre-existing drivers unchanged at `0 differing, 4
replays`
each and the new case reading `5 steps x 240 logits, 0 differing, 3
replays`.

RE-RUN at the merged head `15906f0a4`, same box, four gates:

| Gate | Result |
|---|---|
| `test_decode_graph_seam_g1_cuda` | 6 cases, **3306 assertions**, 0
failed; the spec case `0 differing, 3 replays` |
| `test_device_pool` | 10 cases, 37 assertions, 0 failed |
| `test_qwen3_5_decode_graph_seam` | 8 cases, 138 assertions, 0 failed
-- identical to `origin/main` on the same build |
| `test_breakable_graph` | 30 cases, 265 assertions, 0 failed |

`b316afc05` and `47143457f` sit on top of that head and are comment
corrections
in `src/vt/cpu/cpu_paged_attn.cpp` and
`src/vllm/model_executor/models/qwen3_5.cpp`
with no code in either.

## The merge, and what a reviewer should check first

GitHub reported this CONFLICTING and it is not. `git merge-tree
--write-tree
origin/main HEAD` returns rc 0, no CONFLICT line, clean tree
`4eec7dd20`; the
forge ignores this repository's `merge=union` driver and reads an append
to
`.agents/issue-index.md` as a conflict. Merged locally at `96ed8346f`,
never
rebased. **The union merge is verified rather than assumed**, because
two
relocations can merge cleanly into a duplicate: against `origin/main`'s
index the
merged file is 441 rows plus exactly ONE added line and zero removed,
`diff`
reports a single hunk, and each of #1380 and #1394 has exactly one row —
re-checked on the built tree inside the lease.

**One thing this PR does NOT gate, stated plainly.** The pre-grow
profile comes
from the driver's COLD step, so the fix rests on the captured forward
demanding
no more blocks of any size class than the eager forward at that shape
did. That
is a reading of the code and **no test asserts it**. A capture-only
allocation
with no eager counterpart would reopen #1380 and nothing here would say
so until
a capture threw on a device. The spec's `## Owed` names the two things
that
would settle it and which is worth building.

**An earlier revision of this body gave the WRONG reason for believing
it, and
the belief survives on a different one.** It said the two arms "run the
same
`DenseForwardLayers`". They call the same function and take DIFFERENT
branches:
the cold arm passes 13 arguments and lets `persistent_sdi` default to
`nullptr`
(`qwen3_5.cpp:11153`), the capture arm passes `dbuf ? s.dev.get() :
nullptr` as a
14th (`:11084`), and the MoE driver carries the same asymmetry (`:10595`
against
`:10529`). What actually holds the containment is what those branches
DO, at
`:8858-8890`: the `nullptr` arm calls `BuildStepDevInputs` into the main
pool and
then `MaybeBuildAttnCosSin`, while the non-null arm allocates NOTHING
there and
only calls `FillAttnCosSin` into buffers `s.dev` already owns — built
pre-capture
under `ActivePoolScope
persistent_scope(&PersistentDecodeInputPool(d.b))`
(`:11043`, `:10493`), a different pool from the one `PreGrowForCapture`
grows. So
at the one point the arms diverge, the captured region's main-pool
demand is a
strict SUBSET, and the asymmetry runs in the safe direction. Both `warm
= false`
paths (`:10902`, `:10952`; `:10355`, `:10406`) reset the graph and force
a fresh
cold step, so no slot pre-grows from a stale profile. Sameness was never
the
argument, and asserting it invited a reviewer to check the wrong thing.

Two mutations, each compiled clean, each restoring to an empty diff:

| Mutation | `test_decode_graph_seam_g1_cuda` | `test_device_pool` |
|---|---|---|
| 1, driver half: restore the single-block pre-grow | RED, 752
assertions / 2 failed, the original symptom | green |
| 2, pool half: `PreGrowForCapture` grows nothing | RED, 752 assertions
/ 2 failed | RED |

## It reproduces on sm_121a, so the architecture question is measured

#1380 said the first thing a fix had to establish was whether this
reproduces on
`dgx:gpu0`, because SPEC-DSPARK W8 recorded a WORKING speculative
capture on
GB10. **It reproduces.** NVIDIA GB10, capability 12.1, driver
580.173.02, nvcc
13.0.88, `-DVLLM_CPP_CUDA_ARCHITECTURES=121a -DVLLM_CPP_TRITON=OFF`,
binary
resolving `libcudart.so.13` and `libcublasLt.so.13`:

| sha | `dgx:gpu0` (GB10, sm_121a) | `thor:gpu0` (sm_110) |
|---|---|---|
| red `eae8cc3f0` | `cudaMalloc: operation not permitted when stream is
capturing`, then the queue poisoned; 507 assertions / 8 failed / exit 1
| same message, same per-step shape, same counts |
| fixed `15906f0a4` | 6 cases, **3306 assertions**, 0 failed, exit 0; `0
differing, 3 replays` | identical |

Two architectures, both arms, identical readings. That supersedes the
argument
this rested on — that a host-side `DevicePool` size-class deficit cannot
be
architecture-specific — by measuring it. The argument is still worth
keeping,
because `tests/vllm/models/test_device_pool.cpp` reproduces the same
deficit with
fake host backends, **no GPU and no CUDA at all**.

**W8 is explained rather than contradicted.** Whether two tensors
collide in a
size class is arithmetic over the MODEL's dimensions, not the device's:
at the
real 35B, `[S, vocab]` f32 with `vocab = 151936` shares a class with
nothing the
GDN block allocates, so W8's capture had no competitor for its class. A
larger
pool never "hid" the defect so much as never met it at that shape —
which is why
a pool-size-dependent hiding was never going to be the answer.

## The gate the case had to become, and why it could not have been
written the obvious way

The case W6 landed pinned steps 0 through 2 and printed the rest, so it
recorded
the blocker but could not fail while the blocker was live. Turning it
into the
gate took two corrections that are worth more than the diff:

- **`max_num_reqs == 0` does not select an eager arm on a speculative
shape.**
`S = spec_step ? B : PadToCaptureSize(B)` never consults it, so that arm
  captures too, and the "eager" reference was a second capturing driver
competing for the same pool -- measured, the refusal moved from the
second ring
slot to the first. The eager arm is `Qwen3_5DenseModel::ForwardDevice`,
the body
  the driver's own disabled path runs.
- **An interleaved reference HIDES the defect.** An eager forward
between the
graph arm's steps deepens the shared free list on the graph arm's
behalf. The
interleaved shape of this case passed 1240 assertions at the un-fixed
head
while the driver stepped alone threw. The eager loop now runs to
completion
first; each arm owns its own KV and GDN state, so it is the same
computation.

The spec carried the opposite advice on both points and now carries the
measurement.

## A second defect this uncovered, fixed in the same flow

The `DevicePool` change moves which pooled block holds a tensor and
therefore
what follows it, and that turned a SILENT out-of-bounds read into a
SIGSEGV in
`tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp`. `gdb` puts it at
`src/vt/cpu/cpu_paged_attn.cpp:224`, one frame under
`FullAttnBlockPaged`,
reached from the eager fallback of `Qwen3_5DecodeGraph::Step`.

`PagedAttentionKernel` reads `btab[r * bt_row + (j / block_size) *
bt_col]` for
every `j < seq_lens[r]` and never checked that the block table has that
many
columns. The seam case's `SpecAttnMeta` hardcoded one column, and its
shape C
sits at `pos = 20` with `q = 4`, so `seq_lens = 24` against `block_size
= 16`.

**This closes a live crash on `main`, not a latent one, and an earlier
revision
of this body said otherwise.** Two readings of that file at `5f68e60df`
exactly:
one build gives 8 cases / 138 assertions / exit 0, and the operator's
gives a
deterministic **SIGSEGV, exit 139, three runs of three**. Both are real.
The read
is out of bounds at `main` unconditionally; whether it FAULTS is the
allocator's
business rather than the caller's. "Passes at main" was a statement
about one
build presented as a statement about the tree, and it is withdrawn.

What settles the presence of the defect independently of any heap layout
is
measured: `origin/main` built with ONLY the kernel refusal and WITHOUT
the
test-data fix -- **the refusal fired**. And the assertion counts never
disagreed;
a SIGSEGV truncates doctest's count, so 135 is the crashed-partway run
of the
same 8 cases and 138 is the complete one. This branch adds no assertion
to that
file, and the file is byte-identical between `5f68e60df` and
`96ed8346f`.

Both halves land here: the kernel refuses a short table with one compare
per
request outside the token loop, and the helper sizes its table for the
sequence
length it declares.

**IT CLOSES #1394 AND IT DOES NOT CLOSE THE READ, and the difference is
worth
being exact about.** #1394 is scoped to `vt::cpu::PagedAttentionKernel`
and to
the seam case that fed it a short table; both halves land here, so
`Closes` is
honest. The same unbounded read is live on **five other backends** —
[#1406](#1406) owns it, under
row
`ENG-CUDAGRAPH-BREAK`, and the spec's `## Owed` carries the enumeration:

| Backend | Sites | Where |
|---|---|---|
| CUDA | 12 |
`cuda_paged_attn.cu:230,385,521,676,837,1067,1310,1387,1583,1663,1830,1912`
|
| ROCm | 7 | `rocm_paged_attn.hip:226,424,557,765,1064,1277,1530` |
| Metal | 5 | `metal_msl.h:1054,1142,1293,1341,1591` |
| Tenstorrent | 2 | `tenstorrent_ops.cpp:3070,3100`, the HOST fallback |
| Vulkan | 1 | `shaders/vt_paged_attn.comp:110` |

**27 sites, counted with `grep -c` per file so the counts SUM** — #1406
said nine
in its first revision and nineteen in its second, each time by stopping
at the
first surface that answered, so this one is re-derived rather than
inherited.
Tenstorrent's device-staging path (`:2258-2267`) is deliberately NOT in
the
count: it walks `c < max_blocks` and is already bounded. The seam cannot
hold the
bound for all six while `PagedAttentionArgs::max_seq_len` is documented
at
`include/vt/ops.h:806-812` as a value for which "an upper bound is
safe", and
Vulkan and Metal do not even pass the column count into their shaders
(`vulkan_ops.cpp:994-995`, `metal_ops.mm:940-941`), so fixing those two
means
widening a shader ABI.

**THE REFUSAL LANDS UNGATED, AND THIS IS A STAGED SLICE UNDER "NOTHING
LANDS
DEAD".** What is unreached is the TEST, not the code: the refusal sits
on the
production paged-attention path, but nothing feeds it a short table any
more —
because this pull request's own second half sizes `SpecAttnMeta`'s table
correctly. Measured: delete the guard at `cpu_paged_attn.cpp:145-155`
and
`test_qwen3_5_decode_graph_seam` stays 8/8 and `test_ops_paged_attn`
stays 14/14,
both exit 0; `grep -rn "block table is shorter" tests/` returns nothing.
The
detecting `CHECK_THROWS_AS` case is written and reviewed in
[#1407](#1407)
([#1390](#1390)), which merges
IMMEDIATELY after this one in the sequence #1393#1407#1391.
Duplicating it
here would leave two gates for one refusal and no owner for either.
Owning row:
`ENG-CUDAGRAPH-BREAK`; named in the spec's `## Owed`, in the commit
body, and
here. Until #1407 lands, the predicate is supported by the G1
measurement above —
`origin/main` with ONLY the kernel refusal and WITHOUT the test-data
fix, where
the refusal FIRED — and by no test in this tree.

## Review repairs

A fresh review returned PASS_WITH_FINDINGS. It verified the pool
arithmetic and
reproduced both detecting mutations; every finding was about closure
scope and
missing gates, and none changed the arithmetic. Repaired in `197958e42`:

| # | Finding | Repair |
|---|---|---|
| F1 | `Closes #1394` claimed while the read stayed live elsewhere |
`Closes` kept as honest on #1394's actual scope; #1406 named here and in
`## Owed`, and #1406's own enumeration corrected from 19 to **27** — it
was missing Tenstorrent, Vulkan and Metal entirely |
| F2 | the refusal lands with no detecting test | declared as a staged
slice in all three required places, with #1407 as the gate |
| F3 | the `## Owed` argument's stated basis was factually wrong |
replaced with the branch-level argument that actually holds, verified in
both drivers |
| F4 | `kStepKind` mislabelled the spec case's steps — "capture" printed
for the second COLD step, in the very case built to say which step
failed | `CompareStep` now takes the table; the spec case passes
`kSpecRingStepKind` |
| F5 | `PreGrowForCapture` ignores the soft cap | comment naming the
hazard: `Put` frees to the driver over the cap and is called from INSIDE
the captured forward, so a `cudaFree` would abort the capture exactly as
the `cudaMalloc` did — inert only because every platform sets the cap to
0 |
| F6 | `.agents/environment.md` closed a well-hedged entry with a
conclusion two events cannot support | softened to match the entry's own
register |
| F7 | `MarkStepBoundary()` is called on `Pool(b)` only | recorded in
`## Owed`; two unread counters, not a defect |
| F8 | the append-only index row for #1394 keeps withdrawn wording |
left as-is (append-only); the spec and #1406 both now say the issue body
is the authority |

### Re-gated after the repairs, on the merged tree

Built fresh at `a9837a9d6` (`Release`, `-DVLLM_CPP_CUDA=OFF
-DVLLM_CPP_TRITON=OFF
-DVLLM_CPP_SERVER=OFF`, `-j 4`), because merge-tree clean is not
merge-tree
builds and this merge touched `qwen3_5.cpp` from both sides:

| Gate | Result |
|---|---|
| `test_qwen3_5_decode_graph_seam` | exit 0, **8 cases / 8 passed**, 138
assertions |
| `test_ops_paged_attn` | exit 0, **14 cases / 14 passed**, 1643
assertions |
| `test_device_pool` | exit 0, **10 cases / 10 passed**, 37 assertions |
| `test_breakable_graph` | exit 0, **30 cases / 30 passed**, 265
assertions |
| `test_decode_graph_seam_g1_cuda` | compiles clean; **NOT A PASS** — 6
cases, `assertions: 0`, seven `SKIP: no CUDA backend registered` lines.
F4's label change is compile-verified only |

Two mutations, each with its compile status and each restored to an
empty
`git status`:

| Mutation | Compile | Result |
|---|---|---|
| **B**, delete the guard at `cpu_paged_attn.cpp:145-155` | `rc=0` |
`test_qwen3_5_decode_graph_seam` **exit 0, 8/8, 138**;
`test_ops_paged_attn` **exit 0, 14/14, 1643**. NOTHING GOES RED — which
is the F2 finding, measured here rather than accepted on report |
| **C**, revert only `SpecAttnMeta`'s table sizing, KEEP the guard |
`rc=0` | **exit 1, 8 cases / 7 passed / 1 failed**, refusal fires:
`paged_attention: the block table is shorter than the sequence it must
address ... at src/vt/cpu/cpu_paged_attn.cpp:152` |

**Mutation C is the load-bearing one, and mutation B is why.** B shows
no test in
this tree detects the guard's removal; C shows the predicate is right
anyway, by
a route that does not depend on heap layout — feed the kernel a short
table with
the guard in place and it refuses, which no allocator accident can
produce or
suppress. That is the evidence to cite for this refusal, NOT the
SIGSEGV: whether
the unbounded read faults is build- and allocator-dependent, and this
branch has
now produced both readings on different builds.

**A note on the 135, because it was misread once.** Mutation C reports
`assertions: 135` against the baseline's 138, and this run did NOT crash
— it
failed cleanly, exit 1. The three missing assertions are the ones after
the throw
point in the one aborted case. So 135 is what an early-terminating case
reads,
whichever way it terminates, and the earlier attempt to treat 138→135 as
a clean
"truncation" signature was reading a number that does not carry that
meaning.

FOLLOWING_AGENTS_PROTOCOL

Closes #1380
Closes #1394

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
…ine-matrix row by hand (#1305)

#1393 and #1407 landed, and both this branch and #1393 had edited the
`ENG-CUDAGRAPH-BREAK` row of `.agents/engine-matrix.md`. That row is a KEYED
RECORD, so AGENTS.md forbids accepting the automatic three-way merge: the
resolution takes `origin/main`'s version of the file WHOLE and re-applies this
branch's scoped edits on top of it, field by field.

What each side had changed, established before resolving rather than after. Of
the row's nine fields, #1393 moved exactly one — field 6, `Our tests/evidence` —
rewriting the "#1380 is BLOCKED" passage into the measured "#1380 FIXED" result
on `thor:gpu0` and `dgx:gpu0` and appending #1394. This branch moved three:
field 5 and field 9 by pure append, which #1393 never touched, and field 6 by
appending its own #1305 gate block. So field 6 is the only genuine merge, and it
takes #1393's text whole with this branch's block appended after it. Fields 1-4,
7 and 8 were asserted byte-identical on all three sides before anything was
written.

Verified rather than assumed: `git diff origin/main -- .agents/engine-matrix.md`
touches line 64 and nothing else, the other 290 lines are byte-for-byte equal to
`origin/main`, the row carries 10 pipes and 9 fields exactly as the table header
at line 55 declares, and the owner cell names #1305 once and #1390 once. #1393's
markers survive the resolution and none of this branch's do the reverse: the
`.;` this branch introduced and repaired stays repaired, and the file has no `.;`
left.

`.agents/specs/eng-cudagraph-break.md` auto-merged, which is correct for a
per-row spec rather than a keyed multi-row table, and both sides' sections are
present with no conflict markers anywhere in the tree.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
@localai-bot
localai-bot merged commit a68e6e4 into main Aug 19, 2026
1 check failed
@localai-bot
localai-bot deleted the row/ENG-CUDAGRAPH-DEVIDS branch August 19, 2026 23:01
localai-bot pushed a commit that referenced this pull request Aug 20, 2026
Nine commits landed on main while this branch carried the fresh review's
repairs: #1393, #1407 and #1391 among them. The branch touches
src/vllm/multimodal, tests/vllm/multimodal and the row's own spec, none of
which any of them edits, so the merge is textually clean; it is taken rather
than rebased because the branch is already pushed and a rebase would discard
the base its review was taken against.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants