From 1603b6d346762f8bfd9da5afdb96b5db50585edd Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 19 Aug 2026 18:02:27 +0000 Subject: [PATCH 1/9] fix(ENG-CUDAGRAPH-BREAK): three registrations embedded a STALE host vector and never read the device mirror (#1305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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] --- .../model_executor/models/step_token_ids.h | 125 ++++++++++++++++++ .../model_executor/models/deepseek_v2.cpp | 66 ++++++++- .../models/deepseek_v2_registry.cpp | 13 ++ .../models/glm4_moe_lite_registry.cpp | 13 ++ src/vllm/model_executor/models/qwen3_moe.cpp | 64 ++++++++- .../models/qwen3_moe_registry.cpp | 13 ++ 6 files changed, 281 insertions(+), 13 deletions(-) create mode 100644 include/vllm/model_executor/models/step_token_ids.h diff --git a/include/vllm/model_executor/models/step_token_ids.h b/include/vllm/model_executor/models/step_token_ids.h new file mode 100644 index 000000000..bdf2d5d40 --- /dev/null +++ b/include/vllm/model_executor/models/step_token_ids.h @@ -0,0 +1,125 @@ +// THE DECODE-GRAPH SLOT'S TOKEN-ID INPUT, ON THE SHARED SEAM. +// +// Row `ENG-CUDAGRAPH-BREAK`, spec `.agents/specs/eng-cudagraph-break.md`, +// issue #1305, with #1179 and #323 as the standing trackers for the decline +// this exists to make unnecessary. +// +// WHAT IT IS. One decode-graph size slot's per-step input identifiers, held in a +// DEVICE buffer whose address does not move for the life of that slot, refreshed +// once per step from the padded host vector and then, when the asynchronous +// device mirror is live, RE-READ over the real prefix from the mirror's own +// device buffer. `vt::PersistentStepInput` (`include/vt/persistent_step_input.h`) +// owns the address-stability rule, the pinned staging block and the counted +// choice of source; this type owns the one thing that seam deliberately does not +// — the destination itself, drawn from the caller's pooled allocator. +// +// WHY IT EXISTS. Before it, three shipped registrations +// (`qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp`, +// `glm4_moe_lite_registry.cpp`) routed a pure-decode step into a driver that +// embedded from `ModelForwardInput::token_ids` and never looked at +// `device_token_ids`. 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 vector deliberately stale +// (`src/vllm/v1/worker/gpu/runner.cpp`, the mirror arm), so those models +// generated from stale identifiers for every row past the first. Two other +// families already consumed the mirror by hand +// (`qwen3.cpp::ApplyDeviceTokenIdsOverride`, `qwen3_5.cpp`); a third and fourth +// hand-rolled copy is what this header refuses to be. +// +// WHAT IT DOES NOT CLAIM. The embed still sits OUTSIDE the captured region in +// every driver that uses this, because `vt::Embedding` allocates a device +// bounds-check flag and synchronizes the stream — both illegal under capture. So +// the identifiers are read once per step from a stable device address, not from +// inside the replay. The difference matters for exactly one future change +// (moving the embed inside the capture), and this type is the destination that +// change needs; it is not that change. The `qwen3.cpp` decline stays where it +// is: `ENG-CUDAGRAPH-BREAK` W4 (#1307) measured its recorded CAUSE false, and a +// mitigation whose failure mode is unexplained is not retired by a refactor. +#pragma once + +#include +#include +#include +#include + +#include "vllm/model_executor/models/dense_device_glue.h" // Dev, DBuf, MakeTensor +#include "vt/dtype.h" // DType, VT_CHECK +#include "vt/persistent_step_input.h" +#include "vt/tensor.h" + +namespace vllm { + +class StepTokenIds { + public: + // Allocate and BIND a device destination for `capacity` int32 identifiers. A + // slot calls this once per padded size; a capacity change reallocates, which is + // why a driver must invalidate the captured graph in the same step (the same + // obligation the block-table column count already carries). + void Ensure(dense_attn::Dev d, int64_t capacity) { + if (buf_ != nullptr && capacity_ == capacity) return; + // Construct the new block BEFORE releasing the old one, so the pool cannot + // hand back the address the previous capture baked while it is still bound. + auto next = std::make_unique(d, vt::DType::kI32, + std::vector{capacity}); + buf_ = std::move(next); + capacity_ = capacity; + view_ = dense_attn::MakeTensor(buf_->ptr(), vt::DType::kI32, d.q.device, + {capacity}); + cell_.Bind(d.b, buf_->ptr(), + static_cast(capacity) * sizeof(int32_t), /*staged=*/true); + } + + // Refresh this step's identifiers. `padded_host_ids` is the slot's persistent + // padded host vector, which is authoritative for the inert padding rows and, + // when no mirror is live, for every row. `device_ids` is the runner's device + // buffer for THIS step's real prefix, or null when the mirror is off; when it + // is present the real prefix is re-read from it ON THE QUEUE, so the copy is + // ordered after the combine that produced it instead of racing it. + // + // BOTH copies run every step, in this order, and that is not redundancy: the + // host arm is what fills the padding rows and the prefill rows the combine + // never touches, and the device arm is what corrects the decode rows the host + // vector is stale for. + void Refresh(dense_attn::Dev d, const std::vector& padded_host_ids, + const int32_t* device_ids, int64_t device_count) { + VT_CHECK(cell_.bound(), + "StepTokenIds::Refresh on an unbound cell; call Ensure() for this " + "step's padded size first"); + const int64_t T = static_cast(padded_host_ids.size()); + VT_CHECK(T <= capacity_, + "StepTokenIds::Refresh: the padded host ids are longer than the bound " + "device destination, whose address a captured graph has baked"); + view_ = dense_attn::MakeTensor(buf_->ptr(), vt::DType::kI32, d.q.device, {T}); + cell_.RefreshFromHost(d.q, padded_host_ids.data(), + static_cast(T) * sizeof(int32_t)); + if (device_ids == nullptr) return; + // A device buffer LONGER than this step's input would run past the end, which + // can only mean the runner and the model disagree about the step's shape. + // Fail loudly rather than embed past the padding. + VT_CHECK(device_count <= T, + "StepTokenIds::Refresh: the mirror's device ids are longer than this " + "step's padded input"); + cell_.RefreshFromDevice(d.q, device_ids, + static_cast(device_count) * sizeof(int32_t)); + } + + bool bound() const { return cell_.bound(); } + int64_t capacity() const { return capacity_; } + // The tensor to embed from: [T] for the step most recently refreshed. + const vt::Tensor& t() const { return view_; } + // WHICH ARM last refreshed this slot. The whole point of routing through the + // seam rather than writing a fifth private copy: a step that re-read the device + // mirror and one that uploaded a stale host vector leave the same bytes-shaped + // destination and the same token count, and no token gate can separate them. + vt::StepInputSource last_source() const { return cell_.last_source(); } + int64_t device_refreshes() const { return cell_.device_refreshes(); } + int64_t host_refreshes() const { return cell_.host_refreshes(); } + + private: + std::unique_ptr buf_; // the destination this type owns + vt::PersistentStepInput cell_; + vt::Tensor view_{}; + int64_t capacity_ = 0; +}; + +} // namespace vllm diff --git a/src/vllm/model_executor/models/deepseek_v2.cpp b/src/vllm/model_executor/models/deepseek_v2.cpp index 4a58da4b4..65621b085 100644 --- a/src/vllm/model_executor/models/deepseek_v2.cpp +++ b/src/vllm/model_executor/models/deepseek_v2.cpp @@ -78,6 +78,8 @@ #include "vllm/model_executor/models/dense_attn_block.h" // Dev/DBuf/ResidentWeight glue #include "vllm/model_executor/models/device_pool.h" #include "vllm/model_executor/models/mla_attention.h" +#include "vllm/model_executor/models/qwen3_5_internal.h" // detail::DeviceTokenIds +#include "vllm/model_executor/models/step_token_ids.h" // #1305: the slot's device ids #include "vllm/platforms/interface.h" #include "vt/backend.h" #include "vt/breakable_graph.h" // ENG-CUDAGRAPH-BREAK W3: the shared capture seam @@ -530,14 +532,47 @@ void GatherRows(Dev d, void* dst, const Tensor& src, const std::vector& // The EMBED step, hoisted out of the layer region so it can stay OUTSIDE a CUDA // graph capture (the embedding path takes a device flag through a cudaMalloc + // stream sync — the same reason qwen3_moe.cpp:200 keeps `EmbedInto` outside). -void EmbedInto(Dev d, DBuf& hidden, const std::vector& token_ids, +// #1305 — CONSUME the registry's scoped device-id override, once per forward. +// Both registrations that reach this TU (`DeepseekV2ForCausalLM` and +// `Glm4MoeLiteForCausalLM`) publish `ModelForwardInput::device_token_ids` through +// `detail::DeviceTokenIdsScope`; it is null on every path except the asynchronous +// CUDA runner, where the combine has already spliced each decode row's sampled +// token into the DEVICE identifiers and left the host vector deliberately stale. +// Taking it CLEARS it, so the first embed in a forward is the one that gets it. +detail::DeviceTokenIds TakeDeviceTokenIds() { + const detail::DeviceTokenIds ov = detail::DeviceTokenIdsOverride(); + if (ov.ids != nullptr) detail::DeviceTokenIdsOverride() = detail::DeviceTokenIds{}; + return ov; +} + +// EMBED FROM AN ALREADY-RESIDENT ID TENSOR. The decode-graph driver holds its +// identifiers in a `vllm::StepTokenIds` whose device address is stable for the +// life of the slot, so this arm takes the tensor instead of re-uploading a host +// vector into a fresh per-step allocation. +void EmbedInto(Dev d, DBuf& hidden, const Tensor& ids, const DeepseekV2Weights& weights) { const DeepseekV2Params& p = weights.params; - const int64_t T = static_cast(token_ids.size()); Tensor dtab = ResidentWeight(d, weights.embed_tokens, {p.vocab_size, p.hidden_size}); - DBuf dids(d, DType::kI32, {T}, token_ids.data()); Tensor h = hidden.t(); - vt::Embedding(d.q, h, dtab, dids.t()); + vt::Embedding(d.q, h, dtab, ids); +} + +void EmbedInto(Dev d, DBuf& hidden, const std::vector& token_ids, + const DeepseekV2Weights& weights) { + const int64_t T = static_cast(token_ids.size()); + DBuf dids(d, DType::kI32, {T}, token_ids.data()); + // #1305: the eager arms of these two registrations had the SAME defect as the + // graph arm — they embedded the host vector and never looked at the device + // mirror. The override's copy is enqueued on the main queue, so it is ordered + // AFTER the combine that produced it rather than racing it. + const detail::DeviceTokenIds ov = TakeDeviceTokenIds(); + if (ov.ids != nullptr) { + VT_CHECK(ov.count <= T, + "deepseek v2 embed: device input ids longer than the embed input"); + d.b.Copy(d.q, dids.ptr(), ov.ids, + static_cast(ov.count) * sizeof(int32_t)); + } + EmbedInto(d, hidden, dids.t(), weights); } // The CAPTURABLE region: everything after the embedding — the MLA step metadata @@ -912,6 +947,12 @@ struct DeepseekV2DecodeGraph::Impl { CommonAttentionMetadata attn_meta; std::unique_ptr hidden; // [S,H] bf16 persistent embed target std::unique_ptr logits; // [S,vocab] f32 held graph output + // #1305: this slot's per-step input IDENTIFIERS, in a device buffer whose + // address does not move, refreshed through `vt::PersistentStepInput` from the + // padded host vector and then from the runner's device mirror when it is + // live. Before it, every arm below re-uploaded the HOST vector into a fresh + // allocation and the mirror was never read at all. + StepTokenIds ids; // ENG-CUDAGRAPH-BREAK W3 (#1291): the instantiated graph, its handle // ownership, its release and its `captured()` state now live in the shared // seam instead of in a raw `void*` plus a `bool` this driver maintained by @@ -987,6 +1028,17 @@ ForwardLogits DeepseekV2DecodeGraph::Step( const bool cols_changed = (s.bt_cols != -1 && s.bt_cols != cols); s.Refresh(ptok, ppos, pam); s.bt_cols = cols; + // #1305 — THE IDENTIFIERS, ON THE SEAM. Bind this padded size's device + // destination once, then refresh it for this step: the padded host vector + // first (authoritative for the inert padding rows and for every row when no + // mirror is live), and then the runner's DEVICE identifiers over the real + // prefix when the asynchronous combine has patched them there. Both copies are + // enqueued on the main queue, so the second is ordered after the combine + // instead of racing it, and all three arms below embed from the SAME stable + // address. + const detail::DeviceTokenIds ov = TakeDeviceTokenIds(); + s.ids.Ensure(d, S); + s.ids.Refresh(d, s.token_ids, ov.ids, ov.count); if (cols_changed && s.graph.captured()) { // Reset() releases every segment through Backend::DestroyGraph and returns // the container to its as-constructed state, which is also what lets the @@ -1002,7 +1054,7 @@ ForwardLogits DeepseekV2DecodeGraph::Step( // stays exactly as informative as it is on the eager path, which is what the // paged-engine gate asserts on. if (s.graph.captured()) { - EmbedInto(d, *s.hidden, s.token_ids, impl_->weights); + EmbedInto(d, *s.hidden, s.ids.t(), impl_->weights); RecordMlaBatchSplit(BuildMlaBatchSplit(s.attn_meta), s.attn_meta.num_reqs); // Through the seam's container, never `Backend::ReplayGraph` directly: the // container replays its segments in order (one, here, because a decode @@ -1041,7 +1093,7 @@ ForwardLogits DeepseekV2DecodeGraph::Step( // Warm: the pool, weight residency and per-shape kernel scratch were warmed for // this size by the previous (eager) step. CAPTURE the layer region once. if (s.warm) { - EmbedInto(d, *s.hidden, s.token_ids, impl_->weights); + EmbedInto(d, *s.hidden, s.ids.t(), impl_->weights); // ENG-CUDAGRAPH-BREAK W3 (#1291): the capture is the SHARED SEAM's, not this // driver's hand-rolled `BeginCapture`/`EndCaptureGraph` pair. The scope owns // the segment, the handle, its release, the drain a mid-capture throw needs @@ -1118,7 +1170,7 @@ ForwardLogits DeepseekV2DecodeGraph::Step( // split workspace for this size) and defer capture to the next same-size step. // This is a real decode step — nothing is wasted. s.hidden = std::make_unique(d, DType::kBF16, std::vector{S, H}); - EmbedInto(d, *s.hidden, s.token_ids, impl_->weights); + EmbedInto(d, *s.hidden, s.ids.t(), impl_->weights); DBuf lg = ForwardLayers(d, s.hidden->t(), s.positions, s.attn_meta, attn_kv, impl_->weights, kNoGather); s.warm = true; diff --git a/src/vllm/model_executor/models/deepseek_v2_registry.cpp b/src/vllm/model_executor/models/deepseek_v2_registry.cpp index 3499aa8bf..57d0f021b 100644 --- a/src/vllm/model_executor/models/deepseek_v2_registry.cpp +++ b/src/vllm/model_executor/models/deepseek_v2_registry.cpp @@ -25,6 +25,7 @@ #include "vllm/model_executor/models/deepseek_v2.h" #include "vllm/model_executor/models/qwen3_5.h" // ForwardLogits carrier #include "vllm/model_executor/models/qwen3_5_common.h" // HostLogits +#include "vllm/model_executor/models/qwen3_5_internal.h" // detail::DeviceTokenIdsScope #include "vllm/platforms/interface.h" // GetPlatform(device.type).is_cuda() #include "vllm/v1/kv_cache_dtype.h" #include "vllm/v1/kv_cache_interface.h" @@ -86,6 +87,18 @@ ForwardLogits ForwardDeepseekV2ForCausalLM(LoadedModel& model, const ModelForwardInput& input) { auto& ds = ModelAs(model, "DeepseekV2ForCausalLM"); const DeepseekV2Weights& weights = ds.weights(); + // #1305 — PUBLISH the async runner's device-resident input ids for the duration + // of THIS forward. 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). Before this line, every + // arm below — the decode graph AND both eager arms — embedded that stale host + // vector and never looked at `input.device_token_ids` at all, so every row past + // the first generated from the previous step's identifiers. RAII-scoped so it + // cannot outlive the call, and null on every non-async-CUDA path, which makes + // this byte-identical when the mirror is off. + const detail::DeviceTokenIdsScope device_ids_scope( + input.device_token_ids, static_cast(input.token_ids.size())); // DECODE CUDA-GRAPH path (W9): route a PURE-DECODE CUDA step through the // model's graph driver, which pads the batch up to the nearest captured size diff --git a/src/vllm/model_executor/models/glm4_moe_lite_registry.cpp b/src/vllm/model_executor/models/glm4_moe_lite_registry.cpp index 40f029aee..5c19fe8f2 100644 --- a/src/vllm/model_executor/models/glm4_moe_lite_registry.cpp +++ b/src/vllm/model_executor/models/glm4_moe_lite_registry.cpp @@ -49,6 +49,7 @@ #include "vllm/model_executor/models/deepseek_v2.h" #include "vllm/model_executor/models/qwen3_5.h" // ForwardLogits carrier #include "vllm/model_executor/models/qwen3_5_common.h" // HostLogits +#include "vllm/model_executor/models/qwen3_5_internal.h" // detail::DeviceTokenIdsScope #include "vllm/platforms/interface.h" #include "vllm/v1/kv_cache_dtype.h" #include "vllm/v1/kv_cache_interface.h" @@ -113,6 +114,18 @@ ForwardLogits ForwardGlm4MoeLiteForCausalLM(LoadedModel& model, const ModelForwardInput& input) { auto& glm = ModelAs(model, "Glm4MoeLiteForCausalLM"); const DeepseekV2Weights& weights = glm.weights(); + // #1305 — PUBLISH the async runner's device-resident input ids for the duration + // of THIS forward. 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). Before this line, every + // arm below — the decode graph AND both eager arms — embedded that stale host + // vector and never looked at `input.device_token_ids` at all, so every row past + // the first generated from the previous step's identifiers. RAII-scoped so it + // cannot outlive the call, and null on every non-async-CUDA path, which makes + // this byte-identical when the mirror is off. + const detail::DeviceTokenIdsScope device_ids_scope( + input.device_token_ids, static_cast(input.token_ids.size())); // Identical dispatch to deepseek_v2_registry.cpp: a PURE-DECODE CUDA step goes // through the model's graph driver (pad-to-nearest capture size, replay); diff --git a/src/vllm/model_executor/models/qwen3_moe.cpp b/src/vllm/model_executor/models/qwen3_moe.cpp index 2dad9d959..2ad8d3cbf 100644 --- a/src/vllm/model_executor/models/qwen3_moe.cpp +++ b/src/vllm/model_executor/models/qwen3_moe.cpp @@ -37,6 +37,7 @@ #include "vllm/model_executor/models/device_pool.h" // DevicePool/Pool/ActivePool (shared) #include "vllm/model_executor/models/qwen3_5_internal.h" // detail::EndExpertStreamStep #include "vllm/model_executor/models/qwen3_5_moe_block.h" // RunMoeBlock (SEAM GAP #2) +#include "vllm/model_executor/models/step_token_ids.h" // #1305: the slot's device ids #include "vllm/platforms/interface.h" #include "vt/backend.h" #include "vt/breakable_graph.h" // ENG-CUDAGRAPH-BREAK W3: the shared capture seam @@ -116,13 +117,47 @@ void GatherRows(Dev d, void* dst, const Tensor& src, const std::vector& // and it consumes the HOST token_ids. The graph driver runs this per step into // its PERSISTENT hidden buffer, then captures/replays ForwardLayers over that // fixed hidden address. -void EmbedInto(Dev d, DBuf& hidden, const std::vector& token_ids, +// #1305 — CONSUME the registry's scoped device-id override, once per forward. +// `ForwardQwen3MoeForCausalLM` publishes `ModelForwardInput::device_token_ids` +// through `detail::DeviceTokenIdsScope`; it is null on every path except the +// asynchronous CUDA runner, where the combine has already spliced each decode +// row's sampled token into the DEVICE identifiers and left the host vector +// deliberately stale. Taking it CLEARS it, so the first embed in a forward is +// the one that gets it and a second, unrelated embed cannot be handed another +// step's rows. +detail::DeviceTokenIds TakeDeviceTokenIds() { + const detail::DeviceTokenIds ov = detail::DeviceTokenIdsOverride(); + if (ov.ids != nullptr) detail::DeviceTokenIdsOverride() = detail::DeviceTokenIds{}; + return ov; +} + +// EMBED FROM AN ALREADY-RESIDENT ID TENSOR. The decode-graph driver holds its +// identifiers in a `vllm::StepTokenIds` whose device address is stable for the +// life of the slot, so this arm takes the tensor instead of re-uploading a host +// vector into a fresh per-step allocation. +void EmbedInto(Dev d, DBuf& hidden, const Tensor& ids, const Qwen3MoeWeights& weights, const HfConfig& config) { - const int64_t T = static_cast(token_ids.size()); Tensor dtab = ResidentWeight(d, weights.embed_tokens, {config.vocab_size, config.hidden_size}); + vt::Embedding(d.q, hidden.t(), dtab, ids); +} + +void EmbedInto(Dev d, DBuf& hidden, const std::vector& token_ids, + const Qwen3MoeWeights& weights, const HfConfig& config) { + const int64_t T = static_cast(token_ids.size()); DBuf dids(d, DType::kI32, {T}, token_ids.data()); - vt::Embedding(d.q, hidden.t(), dtab, dids.t()); + // #1305: the eager arms of this model had the SAME defect as its graph arm — + // they embedded the host vector and never looked at the device mirror. The + // override's copy is enqueued on the main queue, so it is ordered AFTER the + // combine that produced it rather than racing it. + const detail::DeviceTokenIds ov = TakeDeviceTokenIds(); + if (ov.ids != nullptr) { + VT_CHECK(ov.count <= T, + "qwen3 moe embed: device input ids longer than the embed input"); + d.b.Copy(d.q, dids.ptr(), ov.ids, + static_cast(ov.count) * sizeof(int32_t)); + } + EmbedInto(d, hidden, dids.t(), weights, config); } // The CAPTURABLE region: everything AFTER the embedding — the residual stream @@ -414,6 +449,12 @@ struct Qwen3MoeDecodeGraph::Impl { CommonAttentionMetadata attn_meta; std::unique_ptr hidden; // [S,H] bf16 persistent embed target std::unique_ptr logits; // [S,vocab] f32 held graph output + // #1305: this slot's per-step input IDENTIFIERS, in a device buffer whose + // address does not move, refreshed through `vt::PersistentStepInput` from the + // padded host vector and then from the runner's device mirror when it is + // live. Before it, every arm below re-uploaded the HOST vector into a fresh + // allocation and the mirror was never read at all. + StepTokenIds ids; // ENG-CUDAGRAPH-BREAK W3 (#1291): the instantiated graph, its handle // ownership, its release and its `captured()` state now live in the shared // seam instead of in a raw `void*` plus a `bool` this driver maintained by @@ -498,6 +539,17 @@ ForwardLogits Qwen3MoeDecodeGraph::Step( const bool cols_changed = (s.fa_cols != -1 && s.fa_cols != cols); s.Refresh(ptok, ppos, pam); s.fa_cols = cols; + // #1305 — THE IDENTIFIERS, ON THE SEAM. Bind this padded size's device + // destination once, then refresh it for this step: the padded host vector + // first (authoritative for the inert padding rows and for every row when no + // mirror is live), and then the runner's DEVICE identifiers over the real + // prefix when the asynchronous combine has patched them there. Both copies are + // enqueued on the main queue, so the second is ordered after the combine + // instead of racing it, and all three arms below embed from the SAME stable + // address. + const detail::DeviceTokenIds ov = TakeDeviceTokenIds(); + s.ids.Ensure(d, S); + s.ids.Refresh(d, s.token_ids, ov.ids, ov.count); if (cols_changed && s.graph.captured()) { // Reset() releases every segment through Backend::DestroyGraph and returns // the container to its as-constructed state, which is also what lets the @@ -510,7 +562,7 @@ ForwardLogits Qwen3MoeDecodeGraph::Step( // Fast path: this size's graph is captured. Embed OUTSIDE the graph into the // persistent hidden buffer, then relaunch the captured layer region. if (s.graph.captured()) { - EmbedInto(d, *s.hidden, s.token_ids, impl_->weights, impl_->config); + EmbedInto(d, *s.hidden, s.ids.t(), impl_->weights, impl_->config); // Through the seam's container, never `Backend::ReplayGraph` directly: the // container replays its segments in order (one, here, because a decode // capture is kFull) and owns the G3 replay counter the reachability gate @@ -525,7 +577,7 @@ ForwardLogits Qwen3MoeDecodeGraph::Step( // this size by the previous (eager) step. CAPTURE the layer region once, // instantiate the graph, then launch it. if (s.warm) { - EmbedInto(d, *s.hidden, s.token_ids, impl_->weights, impl_->config); + EmbedInto(d, *s.hidden, s.ids.t(), impl_->weights, impl_->config); // ENG-CUDAGRAPH-BREAK W3 (#1291): the capture is the SHARED SEAM's, not this // driver's hand-rolled `BeginCapture`/`EndCaptureGraph` pair. The scope owns // the segment, the handle, its release, the drain a mid-capture throw needs @@ -610,7 +662,7 @@ ForwardLogits Qwen3MoeDecodeGraph::Step( // per-shape scratch for this size) and defer capture to the next same-size // step. This is a real decode step — nothing is wasted. s.hidden = std::make_unique(d, DType::kBF16, std::vector{S, H}); - EmbedInto(d, *s.hidden, s.token_ids, impl_->weights, impl_->config); + EmbedInto(d, *s.hidden, s.ids.t(), impl_->weights, impl_->config); DBuf lg = ForwardLayers(d, s.hidden->t(), s.positions, s.attn_meta, attn_kv, impl_->weights, impl_->config, kNoGather); s.warm = true; diff --git a/src/vllm/model_executor/models/qwen3_moe_registry.cpp b/src/vllm/model_executor/models/qwen3_moe_registry.cpp index 29f2435c2..2acd6c09b 100644 --- a/src/vllm/model_executor/models/qwen3_moe_registry.cpp +++ b/src/vllm/model_executor/models/qwen3_moe_registry.cpp @@ -23,6 +23,7 @@ #include "vllm/model_executor/models/qwen3_5.h" // ForwardLogits (shared carrier) #include "vllm/model_executor/models/qwen3_5_common.h" // HostLogits (W3) #include "vllm/model_executor/models/qwen3_moe.h" +#include "vllm/model_executor/models/qwen3_5_internal.h" // detail::DeviceTokenIdsScope #include "vllm/platforms/interface.h" // GetPlatform(device.type).is_cuda() #include "vllm/v1/kv_cache_dtype.h" #include "vllm/v1/kv_cache_interface.h" @@ -86,6 +87,18 @@ ForwardLogits ForwardQwen3MoeForCausalLM(LoadedModel& model, const ModelForwardInput& input) { auto& qwen = ModelAs(model, "Qwen3MoeForCausalLM"); const Qwen3MoeWeights& weights = qwen.weights(); + // #1305 — PUBLISH the async runner's device-resident input ids for the duration + // of THIS forward. 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). Before this line, every + // arm below — the decode graph AND both eager arms — embedded that stale host + // vector and never looked at `input.device_token_ids` at all, so every row past + // the first generated from the previous step's identifiers. RAII-scoped so it + // cannot outlive the call, and null on every non-async-CUDA path, which makes + // this byte-identical when the mirror is off. + const detail::DeviceTokenIdsScope device_ids_scope( + input.device_token_ids, static_cast(input.token_ids.size())); // DECODE CUDA-GRAPH path (W7): route a PURE-DECODE CUDA step through the // model's graph driver, which pads the batch up to the nearest captured size From 41323011c34fdcaf36a8f26449c1331f84eba82f Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 19 Aug 2026 18:08:18 +0000 Subject: [PATCH 2/9] test(ENG-CUDAGRAPH-BREAK): the mirror's identifiers, asserted through ModelRegistry::Forward (#1305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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] --- tests/CMakeLists.txt | 1 + .../vllm/models/test_moe_async_device_ids.cpp | 536 ++++++++++++++++++ 2 files changed, 537 insertions(+) create mode 100644 tests/vllm/models/test_moe_async_device_ids.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cd7b7f25a..56a72426f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -432,6 +432,7 @@ target_include_directories(test_linear_method PRIVATE ${CMAKE_SOURCE_DIR}/src) vllm_cpp_add_test(test_qwen3_break_point vllm/models/test_qwen3_break_point.cpp) vllm_cpp_add_test(test_qwen3_decode_graph_seam vllm/models/test_qwen3_decode_graph_seam.cpp) vllm_cpp_add_test(test_qwen3_moe_decode_graph_seam vllm/models/test_qwen3_moe_decode_graph_seam.cpp) +vllm_cpp_add_test(test_moe_async_device_ids vllm/models/test_moe_async_device_ids.cpp) vllm_cpp_add_test(test_voxtral_decode_graph_seam vllm/models/test_voxtral_decode_graph_seam.cpp) vllm_cpp_add_test(test_deepseek_v2_decode_graph_seam vllm/models/test_deepseek_v2_decode_graph_seam.cpp) vllm_cpp_add_test(test_qwen3_5_decode_graph_seam vllm/models/test_qwen3_5_decode_graph_seam.cpp) diff --git a/tests/vllm/models/test_moe_async_device_ids.cpp b/tests/vllm/models/test_moe_async_device_ids.cpp new file mode 100644 index 000000000..946949c60 --- /dev/null +++ b/tests/vllm/models/test_moe_async_device_ids.cpp @@ -0,0 +1,536 @@ +// THE ASYNCHRONOUS DEVICE-IDENTIFIER GATE for the MoE registrations, entered +// through `ModelRegistry::Forward` (#1305; row `ENG-CUDAGRAPH-BREAK`, spec +// `.agents/specs/eng-cudagraph-break.md`). +// +// WHAT IT MEASURES, and why it enters at the registry rather than at the driver. +// `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and +// `glm4_moe_lite_registry.cpp` each admit a pure-decode step to a decode-graph +// driver. Before #1305 none of the three looked at +// `ModelForwardInput::device_token_ids` at all, and neither did either model's +// eager arms. 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 for decode rows +// (`src/vllm/v1/worker/gpu/runner.cpp`, the mirror arm, which is the default), so +// those models generated from the previous step's identifiers. +// +// The defect is SILENTLY WRONG TOKENS and not a fault, so the gate has to assert +// the identifiers themselves. It does that the way +// `tests/vllm/models/test_kimi_linear_paged.cpp` does for the same contract: +// hand the RIGHT identifiers ONLY through `device_token_ids`, make the host +// vector deliberately wrong, and require the logits to equal a run that had the +// right host identifiers and no mirror. On CPU a host pointer is +// device-addressable, so the contract is directly testable here. +// +// THREE RUNS, because two of them cannot separate the cases: +// +// A right host ids, no mirror -> the reference +// B WRONG host ids, no mirror -> must DIFFER from A +// C WRONG host ids, mirror carries A's -> must EQUAL A +// +// B is the control. Without it a model that ignored its identifiers entirely +// would pass C, and so would a gate whose two runs happened to share a buffer. +// +// AND THE SEAM IS ASSERTED, not inferred. `vt::StepInputStats::device_refreshes` +// moves only inside `vt::PersistentStepInput::RefreshFromDevice`. A driver that +// hand-rolled the same copy would produce IDENTICAL logits and leave that counter +// at zero, which is exactly the fifth private copy #1305 exists to stop; and a +// step that re-read the mirror and one that uploaded a stale host vector leave +// the same bytes-shaped destination, so no token gate can separate them either. +// +// WHAT THIS HARNESS CANNOT SEE, named rather than claimed away. A CPU "replay" +// recomputes nothing (`decode_graph_seam_harness.h`), so only the COLD step and +// the CAPTURE step below actually run the forward; the two replay steps return +// the slot's persistent logits unchanged. That is why the comparison is per step +// over the first two steps and why the counters are asserted over all of them. +// The depth-2 four-concurrent battery on a real device is a different gate, it +// needs a GPU and a checkpoint, and the spec's `## Owed` records it as owed. +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "decode_graph_seam_harness.h" +#include "vllm/model_executor/model_loader/safetensors_reader.h" +#include "vllm/model_executor/models/model_registry.h" +#include "vllm/model_executor/models/qwen3_5.h" // ForwardLogits +#include "vllm/transformers_utils/hf_config.h" +#include "vt/backend.h" +#include "vt/dtype.h" +#include "vt/breakable_graph.h" +#include "vt/persistent_step_input.h" + +namespace { + +using vllm::HfConfig; +using vllm::ModelRegistry; +using vllm::ModelSource; +using vllm::PagedKvCache; +using vllm::SafetensorsFile; +using vllm::v1::CommonAttentionMetadata; +using vllm_test::StaticGraphCpu; +using vt::DType; + +vt::Queue Q() { return vt::Queue{vt::Device{vt::DeviceType::kCPU, 0}, nullptr}; } + +// ─── a synthetic safetensors checkpoint (the kimi paged fixture's shape) ───── +struct Fx { + std::string name, dtype; + std::vector shape; + std::string bytes; +}; + +std::string U64Le(uint64_t v) { + std::string s(8, '\0'); + for (int i = 0; i < 8; ++i) + s[static_cast(i)] = static_cast((v >> (8 * i)) & 0xff); + return s; +} +int64_t NumEl(const std::vector& s) { + int64_t n = 1; + for (int64_t d : s) n *= d; + return n; +} +std::string Bf16Bytes(size_t n, int seed, float scale) { + std::string s(n * 2, '\0'); + uint32_t r = static_cast(seed) * 2654435761u + 1u; + for (size_t i = 0; i < n; ++i) { + r = r * 1664525u + 1013904223u; + const float u = static_cast(r >> 8) / static_cast(1u << 24); + const uint16_t bf = vt::F32ToBF16((u - 0.5f) * 2.0f * scale); + s[i * 2] = static_cast(bf & 0xff); + s[i * 2 + 1] = static_cast((bf >> 8) & 0xff); + } + return s; +} +Fx Bf16(const std::string& n, std::vector sh, int seed, float scale = 0.08f) { + return {n, "BF16", sh, Bf16Bytes(static_cast(NumEl(sh)), seed, scale)}; +} +std::string BuildSt(const std::vector& ts) { + nlohmann::json hdr = nlohmann::json::object(); + std::string data; + for (const Fx& t : ts) { + const size_t start = data.size(); + data += t.bytes; + hdr[t.name] = {{"dtype", t.dtype}, + {"shape", t.shape}, + {"data_offsets", {start, data.size()}}}; + } + const std::string header = hdr.dump(); + return U64Le(header.size()) + header + data; +} + +class TempFile { + public: + explicit TempFile(const std::string& bytes, const char* ext = ".safetensors") { + static int c = 0; + path_ = (std::filesystem::temp_directory_path() / + ("moe_devids_" + std::to_string(::getpid()) + "_" + + std::to_string(c++) + ext)) + .string(); + std::ofstream out(path_, std::ios::binary); + out.write(bytes.data(), static_cast(bytes.size())); + } + ~TempFile() { std::remove(path_.c_str()); } + const std::string& path() const { return path_; } + + private: + std::string path_; +}; + +// The same tiny geometry `test_qwen3_moe_decode_graph_seam.cpp` and +// `test_qwen3_moe_forward.cpp` already gate, so this file runs the SAME +// arithmetic those do. +constexpr int64_t kH = 64, kL = 2, kHq = 4, kHkv = 2, kDh = 16, kV = 100; +constexpr int64_t kE = 4, kTopK = 2, kI = 32; + +std::string ConfigJson() { + nlohmann::json j; + j["architectures"] = std::vector{"Qwen3MoeForCausalLM"}; + j["model_type"] = "qwen3_moe"; + j["hidden_size"] = kH; + j["num_hidden_layers"] = kL; + j["num_attention_heads"] = kHq; + j["num_key_value_heads"] = kHkv; + j["head_dim"] = kDh; + j["intermediate_size"] = kI; + j["moe_intermediate_size"] = kI; + j["shared_expert_intermediate_size"] = 0; + j["num_experts"] = kE; + j["num_experts_per_tok"] = kTopK; + j["vocab_size"] = kV; + j["max_position_embeddings"] = 256; + j["rms_norm_eps"] = 1e-6; + j["rope_theta"] = 10000000.0; + j["tie_word_embeddings"] = false; + j["attention_bias"] = false; + j["torch_dtype"] = "bfloat16"; + return j.dump(); +} + +std::vector BuildTensors() { + std::vector v; + int s = 1; + v.push_back(Bf16("model.embed_tokens.weight", {kV, kH}, s++)); + v.push_back(Bf16("model.norm.weight", {kH}, s++, 0.5f)); + v.push_back(Bf16("lm_head.weight", {kV, kH}, s++)); + for (int64_t l = 0; l < kL; ++l) { + const std::string b = "model.layers." + std::to_string(l) + "."; + const std::string sa = b + "self_attn."; + const std::string mlp = b + "mlp."; + v.push_back(Bf16(b + "input_layernorm.weight", {kH}, s++, 0.5f)); + v.push_back(Bf16(b + "post_attention_layernorm.weight", {kH}, s++, 0.5f)); + v.push_back(Bf16(sa + "q_proj.weight", {kHq * kDh, kH}, s++)); + v.push_back(Bf16(sa + "k_proj.weight", {kHkv * kDh, kH}, s++)); + v.push_back(Bf16(sa + "v_proj.weight", {kHkv * kDh, kH}, s++)); + v.push_back(Bf16(sa + "o_proj.weight", {kH, kHq * kDh}, s++)); + v.push_back(Bf16(sa + "q_norm.weight", {kDh}, s++, 0.5f)); + v.push_back(Bf16(sa + "k_norm.weight", {kDh}, s++, 0.5f)); + v.push_back(Bf16(mlp + "gate.weight", {kE, kH}, s++)); + for (int64_t e = 0; e < kE; ++e) { + const std::string ex = mlp + "experts." + std::to_string(e) + "."; + v.push_back(Bf16(ex + "gate_proj.weight", {kI, kH}, s++)); + v.push_back(Bf16(ex + "up_proj.weight", {kI, kH}, s++)); + v.push_back(Bf16(ex + "down_proj.weight", {kH, kI}, s++)); + } + } + return v; +} + +struct Fixture { + std::unique_ptr st; + std::unique_ptr cfg_json; + std::vector shards; + HfConfig cfg; + Fixture(const std::string& config_json, const std::vector& tensors) { + st = std::make_unique(BuildSt(tensors)); + cfg_json = std::make_unique(config_json, ".json"); + shards.push_back(SafetensorsFile::Open(st->path())); + cfg = vllm::LoadHfConfig(cfg_json->path()); + } +}; + +// One two-request pure-decode step, both requests at the same position, in their +// own KV block. This is the shape `runner.cpp` builds for a condensed-dense +// decode batch, which is the only shape the mirror ever patches. +struct CachePool { + std::vector> buf; + std::vector attn_kv; + CachePool(int64_t num_blocks, int64_t block_size) { + for (int64_t l = 0; l < kL; ++l) + buf.emplace_back( + static_cast(num_blocks * 2 * block_size * kHkv * kDh), 0.0f); + for (auto& b : buf) { + PagedKvCache kv; + kv.data = b.data(); + kv.dtype = DType::kF32; + kv.num_blocks = num_blocks; + kv.block_size = block_size; + kv.num_kv_heads = kHkv; + kv.head_size = kDh; + attn_kv.push_back(kv); + } + } +}; + +// ─── the DeepSeek-V2 half ──────────────────────────────────────────────────── +// +// `DeepseekV2DecodeGraph` is reached by TWO registrations, +// `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp`, and it got the +// same change as `Qwen3MoeDecodeGraph`. It owes its own gate for the reason +// `decode_graph_seam_harness.h` states once: a driver that kept the old +// behaviour produces identical everything except the identifiers it embedded, +// and nothing but this comparison separates the two. +// +// The geometry is `tests/vllm/models/test_deepseek_v2_decode_graph_seam.cpp`'s +// CPU one: MLA with `q_lora_rank: null` (the V2-Lite direct-q branch), four +// routed experts top-2, NO shared expert and NO dense prefix, so every layer is +// a MoE layer. +constexpr int64_t kDsQkNope = 16, kDsQkRope = 8, kDsVHead = 16, kDsKvLora = 24; +constexpr int64_t kDsHeads = 4, kDsE = 4, kDsMoeI = 16; + +std::string DsConfigJson() { + nlohmann::json j; + j["architectures"] = std::vector{"DeepseekV2ForCausalLM"}; + j["model_type"] = "deepseek_v2"; + j["hidden_size"] = kH; + j["num_hidden_layers"] = kL; + j["num_attention_heads"] = kDsHeads; + j["num_key_value_heads"] = kDsHeads; + j["vocab_size"] = kV; + j["intermediate_size"] = 32; + j["moe_intermediate_size"] = kDsMoeI; + j["n_routed_experts"] = kDsE; + j["num_experts_per_tok"] = 2; + j["n_group"] = 1; + j["topk_group"] = 1; + j["norm_topk_prob"] = false; + j["scoring_func"] = "softmax"; + j["topk_method"] = "greedy"; + j["routed_scaling_factor"] = 1.0; + j["moe_layer_freq"] = 1; + j["q_lora_rank"] = nullptr; + j["rms_norm_eps"] = 1e-6; + j["rope_theta"] = 10000; + j["max_position_embeddings"] = 128; + j["tie_word_embeddings"] = false; + j["torch_dtype"] = "bfloat16"; + j["rope_scaling"] = {{"type", "yarn"}, + {"factor", 4}, + {"beta_fast", 32}, + {"beta_slow", 1}, + {"mscale", 0.707}, + {"mscale_all_dim", 0.707}, + {"original_max_position_embeddings", 32}}; + j["qk_nope_head_dim"] = kDsQkNope; + j["qk_rope_head_dim"] = kDsQkRope; + j["v_head_dim"] = kDsVHead; + j["kv_lora_rank"] = kDsKvLora; + j["first_k_dense_replace"] = 0; + j["n_shared_experts"] = 0; + return j.dump(); +} + +std::vector DsBuildTensors() { + std::vector v; + int s = 1; + const int64_t Dqk = kDsQkNope + kDsQkRope; + v.push_back(Bf16("model.embed_tokens.weight", {kV, kH}, s++)); + v.push_back(Bf16("model.norm.weight", {kH}, s++, 0.5f)); + v.push_back(Bf16("lm_head.weight", {kV, kH}, s++)); + for (int64_t l = 0; l < kL; ++l) { + const std::string b = "model.layers." + std::to_string(l) + "."; + const std::string sa = b + "self_attn."; + const std::string mlp = b + "mlp."; + v.push_back(Bf16(b + "input_layernorm.weight", {kH}, s++, 0.5f)); + v.push_back(Bf16(b + "post_attention_layernorm.weight", {kH}, s++, 0.5f)); + v.push_back(Bf16(sa + "q_proj.weight", {kDsHeads * Dqk, kH}, s++)); + v.push_back( + Bf16(sa + "kv_a_proj_with_mqa.weight", {kDsKvLora + kDsQkRope, kH}, s++)); + v.push_back(Bf16(sa + "kv_a_layernorm.weight", {kDsKvLora}, s++, 0.5f)); + v.push_back(Bf16(sa + "kv_b_proj.weight", + {kDsHeads * (kDsQkNope + kDsVHead), kDsKvLora}, s++)); + v.push_back(Bf16(sa + "o_proj.weight", {kH, kDsHeads * kDsVHead}, s++)); + v.push_back(Bf16(mlp + "gate.weight", {kDsE, kH}, s++)); + for (int64_t e = 0; e < kDsE; ++e) { + const std::string ex = mlp + "experts." + std::to_string(e) + "."; + v.push_back(Bf16(ex + "gate_proj.weight", {kDsMoeI, kH}, s++)); + v.push_back(Bf16(ex + "up_proj.weight", {kDsMoeI, kH}, s++)); + v.push_back(Bf16(ex + "down_proj.weight", {kH, kDsMoeI}, s++)); + } + } + return v; +} + +// One MLA cache per layer: [num_blocks, block_size, kv_lora_rank + qk_rope], +// num_kv_heads == 1, NO separate V (MLAAttentionSpec). +struct DsCachePool { + std::vector> buf; + std::vector attn_kv; + DsCachePool(int64_t num_blocks, int64_t block_size) { + const int64_t head_size = kDsKvLora + kDsQkRope; + for (int64_t l = 0; l < kL; ++l) + buf.emplace_back(static_cast(num_blocks * block_size * head_size), 0); + for (auto& b : buf) { + PagedKvCache kv; + kv.data = b.data(); + kv.dtype = DType::kBF16; + kv.num_blocks = num_blocks; + kv.block_size = block_size; + kv.num_kv_heads = 1; + kv.head_size = head_size; + attn_kv.push_back(kv); + } + } +}; + +constexpr int64_t kBlock = 8; + +CommonAttentionMetadata DecodeMeta(int32_t pos) { + CommonAttentionMetadata am; + am.num_reqs = 2; + am.num_actual_tokens = 2; + am.query_start_loc = {0, 1, 2}; + am.query_start_loc_cpu = am.query_start_loc; + am.seq_lens = {pos + 1, pos + 1}; + am.seq_lens_cpu = am.seq_lens; + am.max_query_len = 1; + am.max_seq_len = pos + 1; + am.block_table_num_cols = 1; + am.block_table_tensor = {0, 1}; // one block each + am.slot_mapping = {pos, static_cast(kBlock) + pos}; + am.causal = true; + return am; +} + +// The four decode steps, as the identifiers the model SHOULD see. +const std::vector>& TrueIds() { + static const std::vector> v = { + {11, 42}, {12, 7}, {13, 65}, {14, 3}}; + return v; +} + +// Drive `steps` pure-decode steps through ModelRegistry::Forward and return the +// downloaded [2, vocab] logits of each step. `mirror` selects run C: the host +// vector is replaced by zeros and the true identifiers travel only through +// `ModelForwardInput::device_token_ids`. +template +std::vector> Run(const Fixture& fx, bool stale_host, bool mirror, + int steps) { + vt::Queue q = Q(); + vt::Backend& be = vt::GetBackend(q.device.type); + std::unique_ptr model = + ModelRegistry::Load(fx.cfg, ModelSource::FromSafetensors(fx.shards)); + Pool pool(/*num_blocks=*/2, kBlock); + const vllm::v1::GDNAttentionMetadata gdn_meta{}; + std::vector gdn_state; + const std::vector no_gather; + + std::vector> out; + for (int t = 0; t < steps; ++t) { + const std::vector& truth = TrueIds()[static_cast(t)]; + const std::vector stale(truth.size(), 0); + const std::vector& host = stale_host ? stale : truth; + const std::vector positions = {t, t}; + const CommonAttentionMetadata am = DecodeMeta(t); + vllm::ModelForwardInput in{host, positions, am, gdn_meta, + pool.attn_kv, gdn_state, fx.cfg, q, + no_gather}; + in.num_reqs = 2; + in.pure_decode = true; + in.gdn_state_slots = 8; + in.uniform_query_len = 1; + // On CPU a host pointer IS device-addressable, which is what makes the + // mirror's contract directly testable without a GPU. + if (mirror) in.device_token_ids = truth.data(); + const vllm::ForwardLogits fl = ModelRegistry::Forward(*model, in); + REQUIRE(fl.on_device()); + std::vector rows(static_cast(2 * kV)); + be.Copy(q, rows.data(), fl.device_tensor.data, rows.size() * sizeof(float)); + be.Synchronize(q); + out.push_back(std::move(rows)); + } + return out; +} + +size_t Differing(const std::vector& a, const std::vector& b) { + REQUIRE(a.size() == b.size()); + size_t n = 0; + for (size_t i = 0; i < a.size(); ++i) + if (std::memcmp(&a[i], &b[i], sizeof(float)) != 0) ++n; + return n; +} + +} // namespace + +TEST_CASE( + "Qwen3MoeForCausalLM embeds the async mirror's DEVICE ids, not the stale host " + "vector") { + Fixture fx(ConfigJson(), BuildTensors()); + REQUIRE(fx.cfg.num_experts == kE); + REQUIRE_MESSAGE(vt::GraphCaptureEnabled(), + "this gate needs the CAPTURING lane; VLLM_CPP_CUDAGRAPH=0 is set"); + // The decode-graph arm of the registry admits itself only where the platform + // reports static-graph mode, which CPU does not. The harness swaps both + // registries so the driver's OWN predicate is what routes, exactly as the + // seam gates do. + StaticGraphCpu harness; + + constexpr int kSteps = 4; + + // RUN A — the reference: the identifiers arrive on the host, no mirror. + vt::ResetStepInputStats(); + const std::vector> ref = Run(fx, /*stale_host=*/false, + /*mirror=*/false, kSteps); + { + const vt::StepInputStats s = vt::GetStepInputStats(); + // The slot binds once and refreshes from the HOST every step; with no mirror + // the device arm must never be taken. + CHECK(s.host_refreshes == kSteps); + CHECK(s.device_refreshes == 0); + CHECK(s.binds >= 1); + } + + // RUN B — THE CONTROL. Stale host identifiers and no mirror: the logits must + // MOVE. Without this arm a model that ignored its identifiers entirely would + // satisfy run C. + vt::ResetStepInputStats(); + const std::vector> stale = Run(fx, /*stale_host=*/true, + /*mirror=*/false, kSteps); + CHECK(vt::GetStepInputStats().device_refreshes == 0); + // Only the COLD and CAPTURE steps recompute on this harness; a CPU replay + // returns the slot's persistent logits unchanged, so steps 2 and 3 carry no + // information either way and are not asserted on. + CHECK(Differing(ref[0], stale[0]) > 0); + CHECK(Differing(ref[1], stale[1]) > 0); + + // RUN C — THE GATE. The same stale host vector, with the true identifiers + // reaching the model ONLY through `device_token_ids`. + vt::ResetStepInputStats(); + const std::vector> via_device = Run(fx, /*stale_host=*/true, + /*mirror=*/true, kSteps); + { + const vt::StepInputStats s = vt::GetStepInputStats(); + // THE SEAM, ASSERTED. `device_refreshes` moves only inside + // `vt::PersistentStepInput::RefreshFromDevice`; a hand-rolled copy in the + // driver would produce identical logits and leave this at zero. + CHECK(s.device_refreshes == kSteps); + CHECK(s.host_refreshes == kSteps); + } + CHECK(Differing(ref[0], via_device[0]) == 0); + CHECK(Differing(ref[1], via_device[1]) == 0); + MESSAGE("registry forward, mirror vs host reference, bit for bit: " + << ref[0].size() << " values per step, " << Differing(ref[0], via_device[0]) + << " and " << Differing(ref[1], via_device[1]) << " differing"); +} + +TEST_CASE( + "DeepseekV2ForCausalLM embeds the async mirror's DEVICE ids, not the stale host " + "vector") { + Fixture fx(DsConfigJson(), DsBuildTensors()); + REQUIRE_MESSAGE(vt::GraphCaptureEnabled(), + "this gate needs the CAPTURING lane; VLLM_CPP_CUDAGRAPH=0 is set"); + StaticGraphCpu harness; + + constexpr int kSteps = 4; + + vt::ResetStepInputStats(); + const std::vector> ref = + Run(fx, /*stale_host=*/false, /*mirror=*/false, kSteps); + { + const vt::StepInputStats s = vt::GetStepInputStats(); + CHECK(s.host_refreshes == kSteps); + CHECK(s.device_refreshes == 0); + CHECK(s.binds >= 1); + } + + vt::ResetStepInputStats(); + const std::vector> stale = + Run(fx, /*stale_host=*/true, /*mirror=*/false, kSteps); + CHECK(vt::GetStepInputStats().device_refreshes == 0); + CHECK(Differing(ref[0], stale[0]) > 0); + CHECK(Differing(ref[1], stale[1]) > 0); + + vt::ResetStepInputStats(); + const std::vector> via_device = + Run(fx, /*stale_host=*/true, /*mirror=*/true, kSteps); + { + const vt::StepInputStats s = vt::GetStepInputStats(); + CHECK(s.device_refreshes == kSteps); + CHECK(s.host_refreshes == kSteps); + } + CHECK(Differing(ref[0], via_device[0]) == 0); + CHECK(Differing(ref[1], via_device[1]) == 0); + MESSAGE("registry forward, mirror vs host reference, bit for bit: " + << ref[0].size() << " values per step, " << Differing(ref[0], via_device[0]) + << " and " << Differing(ref[1], via_device[1]) << " differing"); +} From 831f8d3ce48d2a9230584a82065da9840042456e Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 19 Aug 2026 18:19:06 +0000 Subject: [PATCH 3/9] record(ENG-CUDAGRAPH-BREAK): #1305 resolved and narrowed, and a red main gate nobody had read (#1305, #1390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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] --- .agents/engine-matrix.md | 2 +- .agents/issue-index.md | 1 + .agents/specs/eng-cudagraph-break.md | 88 +++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/.agents/engine-matrix.md b/.agents/engine-matrix.md index ea857e53b..4bdd1867b 100644 --- a/.agents/engine-matrix.md +++ b/.agents/engine-matrix.md @@ -61,7 +61,7 @@ forensics: roadmap_v1.md and the parity ledger. | `ENG-PREEMPT-RECOMPUTE` | FCFS tail preemption with recompute | T0 | `vllm/v1/core/sched/scheduler.py:1142`; `tests/v1/core/test_scheduler.py:930` | `src/vllm/v1/core/sched/scheduler.cpp:102,157`; `src/vllm/v1/core/sched/request_queue.cpp:36` | `tests/vllm/v1/test_scheduler.cpp:247,295`; `tests/vllm/v1/test_request_queue.cpp:91` | `planned: specs/preemption.md` | `ANCHOR-BACKFILL` | - | | `ENG-CUDAGRAPH` | Decode graph capture/replay modes (host-cluster cleanup: capture-size set derived from `max_num_seqs` mirroring vLLM `_set_cudagraph_sizes`; 2026-07-18 graph-baked-scratch use-after-free fix — the 35B c2+ online-serving IMA blocker) | T0 | `vllm/config/compilation.py:53,1319,683-684,1438-1444`; `vllm/config/vllm.py:1667-1770`; `vllm/v1/worker/gpu/cudagraph_utils.py:116`; `tests/compile/test_config.py:122,229` | `src/vt/cuda/cuda_backend.cu:76,97,105`; `include/vllm/model_executor/models/decode_graph_sizes.h`; `src/vllm/model_executor/models/qwen3_5.cpp:3754,3952`; `src/vllm/v1/worker/gpu/runner.cpp:577,597`; graph-safe scratch (retire-on-grow so graph-baked scratch pointers stay valid) `src/vt/cuda/graph_safe_scratch.h`, `src/vt/cuda/cuda_moe_marlin.cu:75`, `src/vt/cuda/cuda_matmul_nvfp4.cu:766`, `src/vt/cuda/cuda_matmul_nvfp4_cutlass.cu:105`, `src/vt/cuda/cuda_matmul_fp8_cutlass.cu:95` | `tests/vt/test_cuda_backend.cpp:98`; `tests/vllm/models/test_decode_graph_sizes.cpp`; `tests/vt/test_graph_safe_scratch.cpp`; explicit 35B gate `tests/parity/test_qwen36_paged_engine.cpp:140` | [blocktable-host-cluster-cleanup.md](specs/blocktable-host-cluster-cleanup.md); [decode-graph-scratch-uaf-2026-07-18.md](specs/decode-graph-scratch-uaf-2026-07-18.md) | `PARTIAL` | **PREFILL capture REFUTED as a lever (2026-08-17, [#1161](https://github.com/mudler/vllm.cpp/issues/1161)).** vLLM's v1 default already captures prefill piecewise (`vllm/config/compilation.py:60-63,615,630` @ `555967922`) and it is in our denominator; SGLang reached the same coverage without `torch.compile` via BCG (`SGLANG-BCG` in [sglang-matrix.md](sglang-matrix.md)). Neither helps us: GB10 2026-07-09 measured prefill GPU-idle-between-launches at **3.8%** with GPU-busy >96% on both arms, and the 27B prefill gap at **92.5% non-GEMM glue GPU work** with the dominant GEMM at +0.17% and attention AHEAD. There are no launch bubbles in our prefill to collapse. Row stays `PARTIAL`; the real residuals are exec dedup ([#1162](https://github.com/mudler/vllm.cpp/issues/1162)) and the break-point seam ([#1163](https://github.com/mudler/vllm.cpp/issues/1163)). Spec [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | | `ENG-CUDAGRAPH-DEDUP` | Graph-executable dedup: hash each captured graph's topology and re-point ONE `cudaGraphExec` with `cudaGraphExecUpdate` on a signature hit, instead of instantiating one exec per padded bucket per model. A memory and capture-time change, NOT a throughput change — a deduped replay launches the same nodes, and the load-bearing gate is byte-identity rather than a ratio | T2 | vLLM has no analogue (its execs come from `torch.compile`, `vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); secondary oracle SGLang `python/sglang/srt/model_executor/runner_backend/cuda_graph_dedup_mixin.py:27-37,105-179,219-242,258-275,353-358` @ `f63458b5be` ([oracles/sglang.md](oracles/sglang.md)) | W1+W2 landing here behind `VT_CUDA_GRAPH_DEDUP`, default OFF until the device A/B measures the per-switch update cost: a device-agnostic dedup registry shared by both accelerator backends plus one CUDA/HIP ops table written once, wired into `EndCaptureGraph`/`ReplayGraph`/`DestroyGraph`. Baseline it replaces: `src/vt/cuda/cuda_backend.cu:222-232` instantiates a fresh exec per capture and destroys the raw graph, over the 7 (`max_num_seqs=32`) or 11 (64) buckets of `include/vllm/model_executor/models/decode_graph_sizes.h:32-41`, times NINE drivers (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight, missing the DFlash draft graph `src/vllm/model_executor/models/qwen3_dflash.cpp:771,870,1038,1091,1095,1106`) | `tests/vt/test_graph_dedup.cpp` 13/13 cases, 65 assertions, RED-first (written and run against an absent header, and the four cases added by the fresh review of #1178, three of them run against the unfixed source) and gated on every platform via a fake ops table whose launch log makes "the right nodes ran" an observable sequence over MORE than one replay per shape; 13/13 negative mutations detected (9 at implementation, 4 at review repair). That count covers `src/vt/graph_dedup.h` ONLY. `src/vt/graph_dedup_runtime.h` had NO executable coverage on any tier, and [#1184](https://github.com/mudler/vllm.cpp/issues/1184) is what hid in that gap: the file is DESIGNED to see runtime calls fail — a refused `cudaGraphExecUpdate` probe is the feature working — and never consumed the runtime's latched error, so the next unrelated kernel reported the refusal as its own failure and every `VT_CUDA_GRAPH_DEDUP=1` run died 6/6 on GB10 as `greedy_argmax launch: invalid device function` from a launch that had succeeded. Repaired structurally rather than at twelve sites: the clear lives in `ScopedLatchClear`'s destructor (`src/vt/graph_dedup_latch.h`) installed at the six `GraphDedupOps` entry points by `MakeLatchGuardedOps`, the table's only constructor, so no raw function address reaches a field and an unwired seventh operation leaves a null the registry refuses; one line covers CUDA and HIP. The device-free half of the signature walk moved to `src/vt/graph_dedup_signature.h` and is gated by `tests/vt/test_graph_dedup_runtime.cpp` 13/13 cases, 51 assertions, RED-first against the pre-fix guard (22 failed assertions reproducing the production message), 7/7 negative mutations detected — Kahn ordering, topological re-index, sorted edge emission, the depth-4 child bound and the four graph-level escapes. STILL compile-gated only: the five node-payload cases behind the device policy. **DEVICE A/B DELIVERED 2026-08-18 on `dgx:gpu0` (GB10, driver 580.173.02, nvcc 13.0.88, `rc` job f88d484b), and it SPLIT.** Gated commit `72de552c8`, whose four dedup sources are byte-identical to the merged `2a976eb9f` — the row squashed, so the gated tree is not an ancestor of the merge and that sha equality is what carries the claim. CORRECTNESS PASSES: 12/12 cells exit 0, zero `invalid device function` and zero `engine-fatal` in every cell log where the pre-fix head `e4ce5571a` died after exactly one replay, ON replays as often as OFF (60=60, 33=33, 43=43), and `--output-token-ids` is IDENTICAL over 10/10 comparisons with the three OFF/OFF controls passing FIRST and the three workloads hashing to three DIFFERENT values, so the identity is not vacuous. #1184 is closed by this run, because a CPU suite drives a fake runtime and cannot observe the real latched error. THE BENEFIT IS REFUTED for the case this row was filed for: `N == M` in every ON cell — 3 graphs to 3 execs on sizes [24 16 8], 2 to 2 on [16 8], 2 to 2 on [32 24] — with the registry's count CLIMBING 1→1, 2→2, 3→3, so more than one capture reached it and the 1:1 is a measurement rather than the single-capture artefact the first attempt produced. Cause pre-registered before the run and then confirmed, structural rather than a tuning miss: `AppendKernelPayload` hashes (`func`, `gridDim.{x,y,z}`, `blockDim.{x,y,z}`, `sharedMemBytes`) at `src/vt/graph_dedup_runtime.h:121-128` and the memcpy payload hashes the copy extent, so the padded batch dimension sits in the KEY, no candidate group ever forms and `cudaGraphExecUpdate` is NEVER ATTEMPTED. That contradicts this row's own premise — `graph_dedup.h`'s header says the fold is for "two padded batch sizes … the same node topology with different parameters" — and SGLang keys the same fields (`cuda_graph_dedup_mixin.py:105-114`), so whatever folds upstream is not decode buckets either. NO throughput or memory number is recorded: clocks unpinned AND the ON arm allocated exactly as many executables as OFF. Honest gaps: per-shape replay counts are unavailable (the driver prints a TOTAL, so B's ~30-per-shape is arithmetic); the driver's "N captured size(s)" counts SLOTS not captures (A reports 6, emits 3); the container's own cuBLASLt was never re-tested at CUDA 13.0 because the staged cu130 prefix was probed first and worked; only the Qwen3 dense decode driver was exercised. STILL OWED: the default flip, now NOT JUSTIFIED on this evidence rather than merely ungated; a COARSER key that could group two decode buckets at all, which the probe-before-fold design makes a cost question rather than an obviously unsafe one ([#1226](https://github.com/mudler/vllm.cpp/issues/1226), the next traceable hypothesis, deliberately NOT decided by this record); device-tier signature stability/discrimination tests; probing `current_raw` instead of `raws.front()` to retire the update-transitivity assumption; the ROCm compile; a supporting `orin:gpu0` leg, BLOCKED because the Jetson 540.4.0 driver cannot run a CUDA 13 runtime (`cudaGetDeviceCount err=35`); and reaching the feature from the default serving path at all — the async runner captures no decode graph, **W5, THE SAME DAY, CONFIRMED THE HYPOTHESIS THAT NEGATIVE PRODUCED ([#1226](https://github.com/mudler/vllm.cpp/issues/1226) DELIVERED).** Same box, `rc-worker-4b8lj`, boot_id `3fd9745a-d25a-426c-ba3c-97c958a85515` at both ends, GB10, driver `580.173.02`, `### DONE_AB_KEY 2026-08-18T20:58:46Z`, binary sha256 `ca114abb…c772ad` from `b48b51df1` (tar sha256 asserted before extraction). Drop the launch dimensions and the memcpy extents from the key and every bucket folds: `a_coarse` 3 graphs to 2 execs, `b_coarse` 2 to 1, `c_coarse` 2 to 1, each `probes=1 refused=0`, against `probes=0 refused=0` in every EXACT cell. **`probes=0` in the EXACT cells is the direct process-level proof of W4's source-level diagnosis** — with the launch dimensions in the key no candidate group forms and `cudaGraphExecUpdate` is never asked; drop them and it is asked once per fold and ACCEPTED EVERY TIME. The saving W4 recorded as unreachable is reachable via the key. Byte-identity holds on A (five cells, `59ebff4a…`) and C (four cells, `ff205260…`). **Workload B is VOID rather than a pass, and its cause is a NEW DEFECT that is not this row's:** the two `VT_CUDA_GRAPH_DEDUP`-unset control cells DISAGREED (`5973c5a1…` 2638 bytes vs `4cf79230…` 2650 bytes) on one binary, one workload, greedy `--temperature 0 --seed 777` at `--concurrency 16`, 23 s apart — 672 tokens both, so the byte delta is JSON width and not a length; exactly rows 17 and 18 of 21 differ, both mid-decode, both in the ragged tail `21 % 16` leaves. B's `b_off_a == b_exact` and `b_off_a == b_coarse_a` therefore compare against a baseline that does not reproduce itself and are WORTHLESS; only the OFF/OFF control made that visible, and without it B would have read as three more confirmations. Filed [#1283](https://github.com/mudler/vllm.cpp/issues/1283). **Caveats that bound this result:** nvcc was `13.3.73` here and `13.0.88` for the W4 baseline the recorded dgx gate stack names, so the OFF-vs-ON and EXACT-vs-COARSE comparisons WITHIN this binary are valid while this run and that baseline are NOT directly comparable; clocks unpinned (2405 MHz current, 3003 max, 2418 applications) and nothing measured bytes, so NO throughput and NO memory number is claimed or implied; only the Qwen3 dense decode driver was exercised; `refused=0` is ONE driver on ONE hardware and toolkit pair, which is no more a floor than W4's negative was a ceiling; and the coarse key is behind `VT_CUDA_GRAPH_DEDUP_COARSE_KEY`, default OFF, inside a default-OFF flag, on **PR [#1232](https://github.com/mudler/vllm.cpp/pull/1232) which is STILL A DRAFT — nothing on `main` folds today.** **Row stays `ACTIVE`, argued:** not `DONE`, because the fold is unreachable on every shipping configuration and the row's stated MEMORY saving has never been measured in bytes on either key; not `PARTIAL`, because nothing upstream is omitted — the coarse key is our own extension past SGLang, which keys the fields we started from; not `BLOCKED`, because nothing external stops the next step. What is owed is now a DECISION about the default plus the byte measurement and the probe-cost-at-real-churn measurement it needs, and landing #1232 first **W6, 2026-08-19, THE DEVICE-BYTE MEASUREMENT — THE BENEFIT QUESTION IS NOW CLOSED AND THE ANSWER IS NEGATIVE.** Tested `origin/main` `2c8f53d93`, which is PR #1232 LANDED, so the "nothing on `main` folds today" caveat every earlier record carried is RETIRED and this measures a configuration that ships. Same box, `rc` job `93f783de`, pod `rc-worker-4b8lj`, boot_id `3fd9745a-…` at BOTH ends, GB10, driver `580.173.02`, nvcc **13.0.88** (the W4 baseline toolkit; W5 ran 13.3.73, so W6 and W5 are NOT directly comparable while comparisons WITHIN this one binary are valid), binary sha256 `be697268…0ce657a7`, `### DONE_BYTES 2026-08-19T04:57:19Z`, 12/12 cells exit 0, zero VOID markers. **THE FOLD ENGAGES AT THE SHIPPED BUCKET SET**, which is the churn W5 could not produce: `vllm-bench` sets `max_num_seqs = concurrency`, so W32 captured `[1 2 4 8 16 24 32]` 7-of-7 and W64 captured `[1 … 64]` 11-of-11, exactly `decode_graph_sizes.h:32-41`, against the 2-3 buckets every earlier conclusion was drawn from. COARSE folds 7 graphs to 3 execs (`probes=7 refused=3`) and 11 to 5 (`probes=22 refused=16`); EXACT folds NOTHING at `probes=0`, reproducing W4 at four times the bucket count. Token ids byte-identical across every cell of a workload INCLUDING both OFF/OFF controls (`ff0db6c6…be9d` 11720 B; `e1cbf5fc…e5d0` 57620 B) — neither workload has #1283's ragged-tail shape and neither hit it. **THE SAVING DOES NOT SURVIVE ITS OWN NULL CONTROL.** `nvidia-smi --query-compute-apps` tail median (the `--query-gpu=memory.used` axis returns `[N/A]` on this box) shows W64 IDENTICAL to the megabyte in all five cells (9737) and W32's coarse arm reading 10-23 MiB HIGHER than OFF (3252/3262 vs 3262/3275). A `cudaMemGetInfo` shim summed over every instantiate gives a nominal 13.83 MiB at 7 buckets — **0.42% of a 3.25 GiB process** — and **−0.75 MiB, i.e. NOTHING, at 11**. That nominal effect is NOT ESTABLISHED on four independent grounds: `EXACT` is a TRUE NULL (same 7 and 11 retained execs, `probes=0`, so it allocates what OFF allocates) and disagrees with OFF by 10.6-13.1 MiB against a 13.83 MiB candidate; the W64 OFF/OFF pair disagrees with ITSELF by 18.2 MiB; one instantiate recorded a NEGATIVE delta (`-5,165,056` B); and `cudaGraphExecDestroy` reclaimed `0` in EVERY cell. Per-instantiate deltas for byte-identical 404-node graphs span 0 to 10,514,432 B and 17 of 27 instantiates in one cell read exactly zero, so these are POOL-GRANULAR readings and the coarse arm's throwaway probes grow that pool exactly like retained execs do. What CAN be priced: one ~390-node executable at **2.08-4.35 MiB**, 10.0-10.6 KB per node — the figure to re-run on a deep checkpoint. **THE MECHANISM INVERTS THIS ROW'S PREMISE.** The driver refuses **43% of probes at 7 buckets and 73% at 11**, every one of them `probe refused a fold (err=910 result=2)` = `cudaErrorGraphExecUpdateFailure` / `cudaGraphExecUpdateErrorTopologyChanged`. The shim's `cudaGraphGetNodes` reading says why false candidates form: the decode graphs are **TWO topologies, 376 and 404 nodes**, mixed across the buckets (`w32_off_a` captured `404 404 376 376 404 404 404`). Every refusal is about TOPOLOGY, never a parameter, so a COARSER key produces MORE false hits rather than more folds — the opposite of what W5's 2-bucket A/B suggested, and W5's `refused=0` is now explained as an artefact of workloads whose buckets only ever SHRANK, so exactly one pair was ever presented. **COST:** W32 OFF 7 instantiates / 0 updates vs COARSE 10 (3 retained + 7 probes) / 11 updates; W64 OFF 11 / 0 vs COARSE **27** (5 retained + 22 probes) / 28 updates — **2.45x the instantiate calls** to retain 6 fewer executables. **Peak transient did NOT double** — in every ON cell live-bytes peak == end, because `Register` destroys the probe before returning, so the feared "double the peak to save the steady state" trade did not occur. **A replay-time re-point DID occur** — 4 and 6 non-probe updates over 88 and 244 replays, ARITHMETIC over two printed totals and not a counter — with every cell exiting 0 and byte-identical, so `Replay`'s transitivity assumption neither aborted nor changed a token; W5 recorded that case as untested. **CAVEATS THAT BOUND THIS RESULT:** the clock pin was **REFUSED inside the lease** (`The current user does not have permission to change clocks for GPU 0000000F:01:00.0`, `clocks_pinned=0`), so **NO time-based figure is attributable** and the instantiate-wall and update-wall figures in `bytes.log` are diagnostics quoted nowhere as a result; `result=2` is ONE driver, ONE GB10, ONE toolkit; only the Qwen3 dense decode driver was exercised, as in W4 and W5; `VT_ASYNC_RUNNER=0` throughout, so the feature is STILL unreachable on the DEFAULT serving path (#1179); and `cudaMemGetInfo` cannot separate an executable's own cost from the pool chunk that satisfied it. **VERDICT, DELIVERED AND NEGATIVE:** `VT_CUDA_GRAPH_DEDUP` stays default OFF, now on MEASUREMENT rather than on silence; `VT_CUDA_GRAPH_DEDUP_COARSE_KEY` alone is a **NO-OP, not merely unsupported** — `GraphDedupCoarseKeyEnabled()` (`src/vt/graph_dedup.h:114`) is read only by the signature builder (`src/vt/graph_dedup_runtime.h:177`), only from `Register`, only under `GraphDedupEnabled()` (`src/vt/cuda/cuda_backend.cu:237`), so with dedup off its sole observable is one stderr line; both on is unsupported. **NOT A CEILING.** Three things would change it and each is traceable: find where the 376/404 split comes from (the FA-2 split-KV grid is the first suspect — a capture that fixes the node set across buckets removes every refusal); an instrument that resolves a single 2-4 MiB executable against driver pool granularity (`cuMemGetAllocationGranularity` or a pool-statistics query); and the same measurement on a 60-80 layer checkpoint, where bytes scale with node count. **Row STAYS `ACTIVE`, argued, and the argument is now narrow.** The MEASUREMENT obligations are discharged and the DECISION is delivered, which is the `DONE` case and it is a real one. Three things stop the flip and none is a checker technicality: the feature is unreachable on the DEFAULT serving path, owned by `ENG-CUDAGRAPH-BREAK` (#1179) and the "nothing lands dead" half of this row; two items still sit under #1162 itself — the device-tier signature stability/discrimination tests and probing `group.current_raw` instead of `raws.front()` to retire the transitivity assumption; and the `DONE` record surface owes a `.agents/parity-ledger.md` entry, a closing-commit owner in place of the claim, an exact test anchor and the RELEASE of `CLAIM-ENG-CUDAGRAPH-DEDUP`, which is an operator act and which this record-only branch does not own. Not `PARTIAL` — nothing upstream is omitted. Not `BLOCKED` — nothing external stops the next step. Full evidence: [benchmark-record.md](benchmark-record.md) entry `ENG-CUDAGRAPH-DEDUP W6`, raw at `/mnt/nas_share/rc/dedup-bytes/` | [eng-cudagraph-dedup.md](specs/eng-cudagraph-dedup.md); analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-DEDUP` ([#1162](https://github.com/mudler/vllm.cpp/issues/1162)) | -| `ENG-CUDAGRAPH-BREAK` | One shared `vt` capture seam that accepts BREAK POINTS, so a forward containing a host-dependent op is still graphed instead of falling out entirely — and so the NINE hand-rolled drivers become one (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight). **Coverage AND CORRECTNESS row, not a throughput row** | T1 | mirror vLLM `CUDAGraphMode.PIECEWISE` splitting at `splitting_ops` (`vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); construction from SGLang BCG `python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/breakable_cuda_graph.py:204-243,246-274,309-333,335-367` @ `f63458b5be` (decorator + runtime stream capture, no compiler); its unit suite `test/registered/cuda_graph/breakable/test_breakable_cuda_graph.py:30,172,230` (305 lines, 11 unit cases) is mapped case for case in the spec's `## Tests to port` | **W6 MOVED THE PREDICATE** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374), 2026-08-19): `GPUModelRunner::execute_model` names the step's ACTUAL uniform query length once through `v1::GraphEligibleQueryLen` (`src/vllm/v1/worker/gpu/cudagraph_dispatch.h`, INERT with no caller since #442 and now called from production) and ships it on `ModelForwardInput::uniform_query_len`; the two Qwen3.5 registrations stop re-deriving that test in twenty duplicated lines each, and both key their slot ring on `(S, q, spec)`. [#1020](https://github.com/mudler/vllm.cpp/issues/1020) CLOSES on the pair, and the key half was a LIVE collision rather than the enabler #1020 called it: `S = spec_step ? B : PadToCaptureSize(B)` puts a 4-request spec step at 1+1 tokens and an 8-request padded decode on the same `S == 8` at the base commit. The widening is BOUNDED by `VT_SPEC_GRAPH_MAX_QLENS` (default 2), because reading the actual length multiplies the spec shape ceiling by `1 + k`. Seven of the nine drivers still read `pure_decode` and are byte-identical. **What did NOT move is 'except at the break points'**: no driver in this tree serves a prefill or a mixed batch under any predicate, so that needs a prefill capture driver nobody has written and whose benefit D5 already refutes on this hardware — a publishable negative, recorded in the spec's `## Owed` as a row-level item. The pre-W6 baseline it replaces: all-or-nothing, `src/vllm/v1/worker/gpu/runner.cpp:1338-1341` routing only `pure_decode`; drivers `qwen3_5.h:275`, `qwen3_5_dense.h:391`, `qwen3_moe.h:117`, `qwen3.h:243`, `deepseek_v2.h:324`, `voxtral.h:126`, plus `deepseek_v4.cpp`, `laguna.cpp` — and the spike found the NINTH already written, `src/vllm/model_executor/models/qwen3_dflash.cpp:771,1091`. The re-derivation is measured, not asserted: `StepDevInputs` (`src/vllm/model_executor/models/qwen3_5.cpp:3894`, the persistent DEVICE input path) exists in ONE driver and `grep -c` returns 0 in `qwen3_moe.cpp`, `qwen3.cpp`, `deepseek_v2.cpp` and `voxtral.cpp`, which is why `src/vllm/model_executor/models/qwen3.cpp`'s `DenseDecodeGraphForward` DECLINES the graph outright when the async device-token mirror is live. **That decline is why this is also a CORRECTNESS row** ([#1179](https://github.com/mudler/vllm.cpp/issues/1179)): a SHIPPED model has already lost its decode graph to the duplication, on the driver's own measurement (`depth-1, graph ON PASS 78/78`; `depth-2, graph OFF PASS 82/82`; `depth-2, graph ON FAIL, slots 1-3 degenerate`), and the fix its comment names is the sibling's `StepDevInputs`. The row still makes NO throughput claim: the prefill refutation on the `ENG-CUDAGRAPH` row (3.8% host idle, >96% GPU-busy, 92.5% glue) stands unchanged | owed: bit-exactness vs eager on every migrated model over MORE than one replay, on a real GPU — **W2 did NOT meet it and says so**: no `rc` lease was obtainable in its window and a CPU harness cannot replay a captured segment, so it moves to W3 with the three drivers of the same shape (G1); the host-lifetime contract of `decode-graph-scratch-uaf-2026-07-18.md` enforced AT the seam — D1's INPUT half, making the intermediates a segment reads unavailable to the `DevicePool` free list, which becomes live only for the first PIECEWISE production capture (W4); the auxiliary-stream auto-join before every segment close (`:353-361`, spec D10), live at `src/vllm/model_executor/models/qwen3_5.cpp:6254-6255,6384` and `src/vllm/model_executor/models/laguna.cpp:2572-2576,2612` (W4, W5). **Delivered by W1** ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the reachability mutation (performed; deleting the call site reds `tests/vllm/models/test_qwen3_break_point.cpp` and leaves the unit suite green); the ported SGLang unit cases with their arithmetic chains and post-replay assertions; and the break-function OUTPUT writeback (`replay_fn`/`_copy_output` `breakable_cuda_graph.py:231-235,172-201`, spec D9), whose destination is a `vt::BreakSlot` the seam owns rather than a caller reference it cannot outlive **W6 gates** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): G2 at THREE levels because the claim has three parts — the engine (`tests/vllm/v1/spec_decode/test_mtp_depth.cpp`, a real LoadedEngine/EngineCore/Scheduler/runner stack, asserting `clamped_spec_steps`, measured 0/0/1/2/4 at k=1/2/3/4/6), the driver (`tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp`, two spec shapes of equal S and different q getting two rings and two captures), and the arithmetic (`tests/vllm/v1/worker/gpu/test_cudagraph_dispatch.cpp`). Five detecting mutations, each reddening ONE level and leaving the others green, plus an over-fire control. A SIXTH mutation was NOT detected and forced a repair: the per-request verify conjunct is redundant on every model that reads the field (both are GDN hybrids whose prefill trips the first conjunct), so it moved into `GraphEligibleQueryLen` where a mutation reds 4 assertions, and the spec records it as unreached defence in depth. **G1 re-run on `thor:gpu0` (sm_110, driver 595.78, nvcc 13.0.88): 2066 assertions, 0 failed, 0 differing on all five migrated drivers — W6 moves no logit.** The ring key's own device case is BLOCKED by [#1380](https://github.com/mudler/vllm.cpp/issues/1380), a pre-existing `cudaMalloc` inside a capturing stream on the spec arm that W6 neither caused nor regressed; the case PINS that refusal and is written to fail when #1380 is fixed. | spec [eng-cudagraph-break.md](specs/eng-cudagraph-break.md) (W0 spike DONE 2026-08-18: the existing `vt` capture vocabulary `include/vt/backend.h:208-222` expresses a SEGMENTED capture with NO new virtual, because `EndCaptureGraph` stores nothing (`src/vt/cuda/cuda_backend.cu:225-232`); a break point is expressible with one `thread_local` capture pointer plus a free function, no compiler and no decorator); **W1 DONE 2026-08-18 ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the seam LANDS** — `vt::BreakableGraph`, `vt::GraphCaptureScope` and `vt::GraphBreak` (`include/vt/breakable_graph.h`, `src/vt/breakable_graph.cpp`), the SGLang unit suite ported case for case (`tests/vt/test_breakable_graph.cpp`, 24 cases / 163 assertions, re-derived 2026-08-18 by `ninja test_breakable_graph && ./build/tests/test_breakable_graph`; the recorded 14/81 never re-derived at any head of this branch), and ONE break point registered at the DENSE ATTENTION ENTRY of `Qwen3ForCausalLM` (`src/vllm/model_executor/models/qwen3.cpp`, inside `RunLayer`). **The exit criterion W0 deliberately left open is ANSWERED on a leased GPU:** `cudaStreamEndCapture` then `cudaStreamBeginCapture` on the SAME stream mid-forward with EAGER work between is LEGAL under `cudaStreamCaptureModeThreadLocal` (`src/vt/cuda/cuda_backend.cu:204-206`) — `orin:gpu0` via an `rc` lease, driver 12060, 3 replays with fresh inputs, 0 mismatches, bare zero-work re-begin legal too. G2 reachability is `tests/vllm/models/test_qwen3_break_point.cpp`, which drives the production `Qwen3DenseModel::Forward` with a scope open and counts `num_hidden_layers + 1` segments (mutation: delete the call site ⇒ 1 segment ⇒ RED), and holds G4 in the same case at 500 logits / 0 differing bit for bit. STAGED SLICE, named: the scope and the container are not yet ENTERED from a production step — no driver opens a scope until W2 migrates `Qwen3DenseDecodeGraph` — and the spec's `## Owed` lists it with W2 as owner, alongside the D10 auxiliary-stream auto-join (W4/W5), G5's ROCm/Tenstorrent arms (W3) and G1 on a real GPU (W2). **The capture-failure drain is NOT among them: it landed HERE**, as behaviour (`std::uncaught_exceptions()` compared against the depth recorded at scope entry, so a break function or ordinary model code throwing mid-capture destroys the partial container instead of handing back a forward that reports `captured() == true`) and as three gated arms (tests 13a, 13b, 13c). The spec's `## Owed` strikes the item through and reads DELIVERED in W1; this cell said the opposite until 2026-08-18 because `cba969857` re-derived field 6 alone. **W2 DONE 2026-08-18 ([#1261](https://github.com/mudler/vllm.cpp/issues/1261)): `Qwen3DenseDecodeGraph` MIGRATED and the seam is ENTERED from a production step**, which retires W1's staged slice. `Qwen3DenseDecodeGraph::Step` opens a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` and replays through `BreakableGraph::Replay`; the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle, the `bool captured` flag, the `DestroyGraph` loop and the driver's own `VLLM_CPP_CUDAGRAPH` read are gone (re-derivation items 1, 2, 5, 6). The migration ADDED `vt::GraphCaptureMode`, mirroring vLLM's `CUDAGraphMode` (`vllm/config/compilation.py:59-63`), whose v1 default `FULL_AND_PIECEWISE` (`:63`) is documented at `:630-632` as a FULL graph for DECODE batches and a piecewise one for prefill/mixed, with `decode_mode()` (`:65-66`) selecting the full half and the runtime reading it at `vllm/v1/worker/gpu/cudagraph_utils.py:185-186`. A decode driver opened `kPiecewise` would have turned a fully graphed decode step into ONE EAGER ATTENTION CALL PER LAYER between graph replays — not vLLM's decode behaviour, and invisible to every token gate here. `GraphBreak` in a `kFull` scope takes the pass-through arm and `AppendBreak` REFUSES a registration in that mode. G2 is `tests/vllm/models/test_qwen3_decode_graph_seam.cpp` (3 cases / 124 assertions), which asserts the SEAM's counters because a driver calling `Backend::ReplayGraph` directly leaves an identical backend log; the mutation restoring the pre-W2 raw pair (18 lines, compiled clean) left `test_breakable_graph` 27/27, `test_qwen3_break_point` 2/2 and `test_qwen3_forward` 10/10 GREEN and reddened only this file. G4 in the same file: capture step vs `Qwen3DenseModel::Forward`, 100 logits, 0 differing. **The async decline at `qwen3.cpp` STANDS and is now GATED in both arms**: migrating the capture does not move the INPUTS, so the depth-2 race is untouched, and the fix is `StepDevInputs` as a SEAM capability, which is W4. **G1 is NOT met by W2** and is recorded owed rather than implied. **W3 DONE 2026-08-19 ([#1291](https://github.com/mudler/vllm.cpp/issues/1291)): the three remaining PLAIN BATCHED drivers migrate — `Qwen3MoeDecodeGraph`, `VoxtralDecodeGraph`, `DeepseekV2DecodeGraph` — one commit each, each with its own RED-first G2 gate.** Four of the nine drivers are now on the seam, and the six batched-driver `VLLM_CPP_CUDAGRAPH` reads `## Our baseline` item 1 counted are down to TWO, both in `qwen3_5.cpp` (W4). Each gate asserts the SEAM's counters and not the backend log, because a driver that kept its raw pair produces identical logits, an identical backend log and an identical `replay_count()`; red-first on four assertions each (`test_qwen3_moe_decode_graph_seam` 222/226, `test_voxtral_decode_graph_seam` 224/228, `test_deepseek_v2_decode_graph_seam` 224/228, all exit 1), green 3/3 each after. The G2 mutation — restoring each pre-W3 driver file, 25/102, 23/92 and 25/94 lines, each compiled clean — reddens ONLY its own gate and leaves `test_breakable_graph` 216/216 and W2's `test_qwen3_decode_graph_seam` 231/231 green. The gate harness is now SHARED (`tests/vllm/models/decode_graph_seam_harness.h`); three more copies inside `tests/` would have reproduced the duplication this row removes from `src/`. **G1 IS DELIVERED and is no longer owed** — the item W1 and W2 both carried. `tests/vllm/models/test_decode_graph_seam_g1_cuda.cpp` runs each driver COLD, CAPTURE and THREE consecutive replays against its own eager arm (selected by `max_num_reqs == 0`, so both arms are one binary on one device rather than two builds, each with its OWN device KV cache) on `thor:gpu0` through an `rc` lease — NVIDIA Thor sm_110, driver 595.78, nvcc 13.0.88, source `c905bb536`, 32 `.cu.o` objects, binary resolving `libcudart.so.13`/`libcublasLt.so.13`: **3 cases, 1600 assertions, exit 0, `5 steps x 100 logits, 0 differing, 4 replays` per driver.** The COUNT carries that claim, not the status line: with no CUDA backend the same file prints `SUCCESS!` over `assertions: 0`. Bounded honestly — synthetic tiny models rather than a checkpoint, and W2's driver shares the seam by argument rather than by measurement. **W3 also found a gate that could not fail.** The three gates' `breaks_registered == 0` mode guard is a TAUTOLOGY for any model with no registered break point, and the one production `vt::GraphBreak` in the tree is W1's in `qwen3.cpp`: flipping `kFull` to `kPiecewise` in `qwen3_moe.cpp`, one token, compiled clean and left that gate GREEN at 226/226. The mode was UNOBSERVABLE from outside a driver, so `vt::GraphBreakStats` gains `full_scopes`/`piecewise_scopes`, counted in `GraphCaptureScope`'s constructor on the ACTIVE path only, with an inert-scope control; the same flip now reds all three gates on exactly those two assertions. **NO break point is registered in these three models, deliberately**: under `kFull` it would be pass-through machinery no gate can exercise, and the break-point set is what the PIECEWISE arm needs (W4/W6). **The async decline, per driver:** Voxtral needs none (its only construction site is `VoxtralGenerateGreedy`, unreachable from the runner); Qwen3-Coder and DeepSeek carry a NEW FINDING instead — `qwen3_moe_registry.cpp:107`, `deepseek_v2_registry.cpp:106` and `glm4_moe_lite_registry.cpp:125` route an async step into a host-vector replay with no `device_token_ids` check at all, filed [#1305](https://github.com/mudler/vllm.cpp/issues/1305) with W4 as owner rather than mitigated on a measurement W3 cannot make. G5's ROCm/Tenstorrent arm is NOT discharged and moves to W5: the fleet carries no such device, so it is blocked on hardware rather than unattempted. **W4 DONE 2026-08-19 ([#1307](https://github.com/mudler/vllm.cpp/issues/1307)): the persistent device input path becomes a SEAM CAPABILITY, and the two Qwen3.5 drivers migrate.** `vt::PersistentStepInput` (`include/vt/persistent_step_input.h`, `src/vt/persistent_step_input.cpp`) binds a capture-stable device destination the DRIVER owns together with its pinned host staging block, and refreshes it in place from a host source or a DEVICE one; it owns the address-stability rule as a REFUSAL, the staging block, and the refreshing ARM as an observable (`last_source()`, `vt::StepInputStats`), and deliberately NOT the device allocation, because `Qwen3_5DecodeGraph` draws its retained inputs from a DEDICATED `DevicePool` so they never pop a block the captured forward's scratch then needs (D3). RED-first against a stub with the declared API and no guarantees: `tests/vt/test_persistent_step_input.cpp` 9 cases / 0 passed / 59 assertions / 32 failed / exit 1, GREEN after at 9/9 and 59/59; three mutations (delete the capacity refusal, make a null device source a silent no-op, collapse the host arm out of staging) each compiled clean and each reds exactly one case. `Qwen3_5DecodeGraph` and `Qwen3_5DenseDecodeGraph` open a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` in `kFull` and replay through it, and their `PinnedStepInputs`/`StageStepInputs` staging now runs THROUGH the capability, which is what makes it reachable rather than a class with a unit test. **Six of the nine drivers are on the seam** and `grep -rn 'std::getenv("VLLM_CPP_CUDAGRAPH")' src/` returns exactly ONE line, `src/vt/breakable_graph.cpp:61` — one switch, at last. Gate `tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp` RED-first on the MoE driver's five seam assertions (3 cases / 62 assertions / 5 failed / exit 1) and GREEN after at 7/7 and 129, G4 reading `40 values, 0 differing` per driver; G2 mutations: the whole pre-W4 file restored reds BOTH drivers (296 lines, 10 assertions), the MoE replay bypassing the container reds ONLY the MoE case (7 lines), the MoE `kFull`->`kPiecewise` flip reds ONLY its mode counters (3 lines), and deleting the `StageStepInputs` call site reds ONLY the reachability case while `test_persistent_step_input` stays 59/59 green — the difference between a class that works and a capability something reaches. **W4 FALSIFIED THIS ROW'S OWN PREMISE, which is its most important result.** This record and the spec both said the fix `qwen3.cpp`'s `DenseDecodeGraphForward`'s decline names already existed as `StepDevInputs`. It does not: `StepDevInputs` has NO token-id member, and its pinned sibling `PinnedStepInputs::token_ids` was allocated at capture, filled every step, zeroed by the poison hook, and NEVER uploaded or read — the embed runs OUTSIDE the captured region from the HOST vector in every batched driver, so **the decode graph carries no token ids to the device in ANY driver**. The dead block is removed. Consequently the DECLINE STANDS and [#1305](https://github.com/mudler/vllm.cpp/issues/1305) STAYS OPEN: W4 also read the decline's recorded cause against the tree at its own parent and found it falsified (the `DeviceTokenIdsScope` WAS live on the graph path, consumed by `EmbedInto` on all three arms at `qwen3.cpp:610,621,644 @ 338cbbfd1^`), so the measured failure is real and its mechanism is unidentified — not a state from which a refactor may retire a mitigation. The async battery was NOT run and W4 says so plainly: it needs `dgx` WITH the Qwen3-0.6B/4B checkpoints, `dgx:gpu0` was held by another session for W4's whole window, and W4's lease was `thor:gpu0`. Still NO throughput claim. W5 DONE 2026-08-19 ([#1335](https://github.com/mudler/vllm.cpp/issues/1335)): the THREE SINGLE-SHAPE drivers migrate — the DFlash draft graph, the DeepSeek V4 decode graph and the Laguna decode graph, whose own note at `laguna.cpp:2116-2119` asked for this seam by name and named V4's as the sibling that moves with it. **NINE OF NINE DRIVERS ARE 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 three per-model rollback switches stay (each an A/B lever for one driver); `VLLM_CPP_CUDAGRAPH` reaches all three for the first time. **D10, the auxiliary-stream fork/join, is DISCHARGED and REACHED** — `GraphCaptureScope` owns the outstanding-fork set and joins it before `EndCaptureGraph` (port of `breakable_cuda_graph.py:353-361` plus the `wait_stream` hook `:101-153`), registered by `vt::GraphNoteFork`/`GraphNoteJoin` from `laguna.cpp:2572-2576,2612`, the only fork inside a captured region by construction. Every prior stage opened `kFull`, which has ONE segment and so no between-segments window, so the rule could not be exercised before W5 and untested machinery was not landed for it. Gated as a COUNTER and an ORDER out of one backend trace, five arms including the control where the model joins first, and two mutations (deleting the join reds only the new case on 5 assertions; making it over-fire reds it on 8). DFlash is the ONE single-shape driver gateable without a GPU, because its admission predicate names neither a device type nor a kernel registry: `test_qwen3_dflash_decode_graph_seam.cpp` RED-first 3 cases/0 passed/16 assertions/7 failed exit 1, GREEN after 3/18, and the G2 mutation reds ONLY that file while seven other suites — the driver's own `test_dflash_propose` included — stay green. **G1 RE-RUN at W5's head on `thor:gpu0`** (sm_110, driver 595.78, nvcc 13.0.88, 32 `.cu.o`, source `79dc6b5bd`) because D10 put a join on the path of EVERY segment close, so the seam changed underneath the five measured drivers: `test_decode_graph_seam_g1_cuda` 5 cases / 2066 assertions / 0 failed, each reading `0 differing, 4 replays`, plus `test_breakable_graph` 265 on the same device. **And the one thing a green build could NOT have told us was measured separately**: Laguna's capture class sits behind `#ifdef VT_MARLIN_NVFP4`, so a passing build is the SAME OBSERVATION as one that compiled the region out. `-DVT_MARLIN_NVFP4=1` is on `laguna.cpp`'s own compile command, and an undeclared identifier injected immediately after its `GraphCaptureScope` line FAILED the object build under `-Werror` (`laguna.cpp:2735`) against an rc-0 baseline, restoring to an empty diff; the identical mutation on V4 failed at `deepseek_v4.cpp:1921`. Both migrated regions are COMPILED, which retires the could-not-even-be-built half. **G1 for all three and G2 for V4 and Laguna are OWED on hardware**, per driver and per reason: V4's `CanRunResidentDecode` refuses `kCPU` and needs the four CUDA-registered kernel families, Laguna's capture class exists only under `VT_MARLIN_NVFP4`. G5's ROCm/Tenstorrent arm stays BLOCKED — the fleet is all NVIDIA — and its owner moves from W5 to the ROW. Still NO throughput claim; analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) W6 DONE 2026-08-19 ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): the eligibility predicate, #1020, and the negative result on the piecewise arm. | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-BREAK-W6`; [#1163](https://github.com/mudler/vllm.cpp/issues/1163), [#1192](https://github.com/mudler/vllm.cpp/issues/1192), [#1261](https://github.com/mudler/vllm.cpp/issues/1261), [#1291](https://github.com/mudler/vllm.cpp/issues/1291), [#1307](https://github.com/mudler/vllm.cpp/issues/1307), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1020](https://github.com/mudler/vllm.cpp/issues/1020), [#1335](https://github.com/mudler/vllm.cpp/issues/1335), [#1374](https://github.com/mudler/vllm.cpp/issues/1374), [#1380](https://github.com/mudler/vllm.cpp/issues/1380) | +| `ENG-CUDAGRAPH-BREAK` | One shared `vt` capture seam that accepts BREAK POINTS, so a forward containing a host-dependent op is still graphed instead of falling out entirely — and so the NINE hand-rolled drivers become one (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight). **Coverage AND CORRECTNESS row, not a throughput row** | T1 | mirror vLLM `CUDAGraphMode.PIECEWISE` splitting at `splitting_ops` (`vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); construction from SGLang BCG `python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/breakable_cuda_graph.py:204-243,246-274,309-333,335-367` @ `f63458b5be` (decorator + runtime stream capture, no compiler); its unit suite `test/registered/cuda_graph/breakable/test_breakable_cuda_graph.py:30,172,230` (305 lines, 11 unit cases) is mapped case for case in the spec's `## Tests to port` | **W6 MOVED THE PREDICATE** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374), 2026-08-19): `GPUModelRunner::execute_model` names the step's ACTUAL uniform query length once through `v1::GraphEligibleQueryLen` (`src/vllm/v1/worker/gpu/cudagraph_dispatch.h`, INERT with no caller since #442 and now called from production) and ships it on `ModelForwardInput::uniform_query_len`; the two Qwen3.5 registrations stop re-deriving that test in twenty duplicated lines each, and both key their slot ring on `(S, q, spec)`. [#1020](https://github.com/mudler/vllm.cpp/issues/1020) CLOSES on the pair, and the key half was a LIVE collision rather than the enabler #1020 called it: `S = spec_step ? B : PadToCaptureSize(B)` puts a 4-request spec step at 1+1 tokens and an 8-request padded decode on the same `S == 8` at the base commit. The widening is BOUNDED by `VT_SPEC_GRAPH_MAX_QLENS` (default 2), because reading the actual length multiplies the spec shape ceiling by `1 + k`. Seven of the nine drivers still read `pure_decode` and are byte-identical. **What did NOT move is 'except at the break points'**: no driver in this tree serves a prefill or a mixed batch under any predicate, so that needs a prefill capture driver nobody has written and whose benefit D5 already refutes on this hardware — a publishable negative, recorded in the spec's `## Owed` as a row-level item. The pre-W6 baseline it replaces: all-or-nothing, `src/vllm/v1/worker/gpu/runner.cpp:1338-1341` routing only `pure_decode`; drivers `qwen3_5.h:275`, `qwen3_5_dense.h:391`, `qwen3_moe.h:117`, `qwen3.h:243`, `deepseek_v2.h:324`, `voxtral.h:126`, plus `deepseek_v4.cpp`, `laguna.cpp` — and the spike found the NINTH already written, `src/vllm/model_executor/models/qwen3_dflash.cpp:771,1091`. The re-derivation is measured, not asserted: `StepDevInputs` (`src/vllm/model_executor/models/qwen3_5.cpp:3894`, the persistent DEVICE input path) exists in ONE driver and `grep -c` returns 0 in `qwen3_moe.cpp`, `qwen3.cpp`, `deepseek_v2.cpp` and `voxtral.cpp`, which is why `src/vllm/model_executor/models/qwen3.cpp`'s `DenseDecodeGraphForward` DECLINES the graph outright when the async device-token mirror is live. **That decline is why this is also a CORRECTNESS row** ([#1179](https://github.com/mudler/vllm.cpp/issues/1179)): a SHIPPED model has already lost its decode graph to the duplication, on the driver's own measurement (`depth-1, graph ON PASS 78/78`; `depth-2, graph OFF PASS 82/82`; `depth-2, graph ON FAIL, slots 1-3 degenerate`), and the fix its comment names is the sibling's `StepDevInputs`. The row still makes NO throughput claim: the prefill refutation on the `ENG-CUDAGRAPH` row (3.8% host idle, >96% GPU-busy, 92.5% glue) stands unchanged; **#1305 CLOSED, and reading the tree found a larger defect than the issue described** (2026-08-19): `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp` never constructed a `detail::DeviceTokenIdsScope` and neither `qwen3_moe.cpp`'s nor `deepseek_v2.cpp`'s `EmbedInto` ever consulted one, so `ModelForwardInput::device_token_ids` reached NOTHING in either translation unit — the decode graph AND both eager arms embedded the host vector the runner's mirror arm deliberately leaves stale for decode rows. The three registries now publish the scope (the mechanism `qwen3.cpp`, `qwen3_5.cpp`, `mistral_registry.cpp`, `internlm2_registry.cpp` and `llama_registry.cpp` already use), and each decode-graph size slot holds a `vllm::StepTokenIds` (`include/vllm/model_executor/models/step_token_ids.h`) whose destination is a device buffer with a stable address, refreshed through `vt::PersistentStepInput` — host arm for the padded vector, DEVICE arm over the real prefix, both on the main queue so the second is ordered after the combine rather than racing it. That is `vt::PersistentStepInput::RefreshFromDevice`'s FIRST production caller, retiring the staged slice W4 landed with none, and it is the fix `qwen3.cpp`'s own decline comment names rather than a fifth private copy. `qwen3.cpp`'s decline is UNTOUCHED: W4 measured its recorded cause false and its real one is unidentified. | owed: bit-exactness vs eager on every migrated model over MORE than one replay, on a real GPU — **W2 did NOT meet it and says so**: no `rc` lease was obtainable in its window and a CPU harness cannot replay a captured segment, so it moves to W3 with the three drivers of the same shape (G1); the host-lifetime contract of `decode-graph-scratch-uaf-2026-07-18.md` enforced AT the seam — D1's INPUT half, making the intermediates a segment reads unavailable to the `DevicePool` free list, which becomes live only for the first PIECEWISE production capture (W4); the auxiliary-stream auto-join before every segment close (`:353-361`, spec D10), live at `src/vllm/model_executor/models/qwen3_5.cpp:6254-6255,6384` and `src/vllm/model_executor/models/laguna.cpp:2572-2576,2612` (W4, W5). **Delivered by W1** ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the reachability mutation (performed; deleting the call site reds `tests/vllm/models/test_qwen3_break_point.cpp` and leaves the unit suite green); the ported SGLang unit cases with their arithmetic chains and post-replay assertions; and the break-function OUTPUT writeback (`replay_fn`/`_copy_output` `breakable_cuda_graph.py:231-235,172-201`, spec D9), whose destination is a `vt::BreakSlot` the seam owns rather than a caller reference it cannot outlive **W6 gates** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): G2 at THREE levels because the claim has three parts — the engine (`tests/vllm/v1/spec_decode/test_mtp_depth.cpp`, a real LoadedEngine/EngineCore/Scheduler/runner stack, asserting `clamped_spec_steps`, measured 0/0/1/2/4 at k=1/2/3/4/6), the driver (`tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp`, two spec shapes of equal S and different q getting two rings and two captures), and the arithmetic (`tests/vllm/v1/worker/gpu/test_cudagraph_dispatch.cpp`). Five detecting mutations, each reddening ONE level and leaving the others green, plus an over-fire control. A SIXTH mutation was NOT detected and forced a repair: the per-request verify conjunct is redundant on every model that reads the field (both are GDN hybrids whose prefill trips the first conjunct), so it moved into `GraphEligibleQueryLen` where a mutation reds 4 assertions, and the spec records it as unreached defence in depth. **G1 re-run on `thor:gpu0` (sm_110, driver 595.78, nvcc 13.0.88): 2066 assertions, 0 failed, 0 differing on all five migrated drivers — W6 moves no logit.** The ring key's own device case is BLOCKED by [#1380](https://github.com/mudler/vllm.cpp/issues/1380), a pre-existing `cudaMalloc` inside a capturing stream on the spec arm that W6 neither caused nor regressed; the case PINS that refusal and is written to fail when #1380 is fixed.; **#1305 (2026-08-19)**: `tests/vllm/models/test_moe_async_device_ids.cpp`, entered at `ModelRegistry::Forward` over a synthetic safetensors checkpoint for `Qwen3MoeForCausalLM` and `DeepseekV2ForCausalLM` — the production entry point, not the driver type. Three runs each: right host ids and no mirror as the reference, stale host ids and no mirror as the CONTROL that must differ, stale host ids with the truth reaching the model ONLY through `device_token_ids` as the gate. RED first at 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 at 59 of 59, exit 0. TWO mutations, each compiled clean and each restored by sha256: deleting the registry's scope line — the production call site — reds 6 assertions across both cases, and swapping the seam's DEVICE arm for its HOST arm leaves the logits BIT IDENTICAL (0 of 200 differing) and reds only `device_refreshes` and `host_refreshes`, which is the arm no token gate can see. Neighbours green on the same binary: `test_qwen3_moe_decode_graph_seam` 228 of 228, `test_deepseek_v2_decode_graph_seam` 230 of 230, `test_qwen3_decode_graph_seam` 231 of 231, `test_voxtral_decode_graph_seam` 230 of 230, `test_breakable_graph` 265 of 265, `test_persistent_step_input` 66 of 66, `test_model_registry` 924 of 924, `test_qwen3_moe_forward` 504 of 504, `test_deepseek_v2_forward` 1052 of 1052. **NOT measured:** the depth-2 four-concurrent battery on a device, which needs a GPU and a real checkpoint; owed. **Found red on `main` and NOT caused here:** `test_qwen3_5_decode_graph_seam` exits 139 while its assertion line reads 135 of 135 passed ([#1390](https://github.com/mudler/vllm.cpp/issues/1390)). | spec [eng-cudagraph-break.md](specs/eng-cudagraph-break.md) (W0 spike DONE 2026-08-18: the existing `vt` capture vocabulary `include/vt/backend.h:208-222` expresses a SEGMENTED capture with NO new virtual, because `EndCaptureGraph` stores nothing (`src/vt/cuda/cuda_backend.cu:225-232`); a break point is expressible with one `thread_local` capture pointer plus a free function, no compiler and no decorator); **W1 DONE 2026-08-18 ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the seam LANDS** — `vt::BreakableGraph`, `vt::GraphCaptureScope` and `vt::GraphBreak` (`include/vt/breakable_graph.h`, `src/vt/breakable_graph.cpp`), the SGLang unit suite ported case for case (`tests/vt/test_breakable_graph.cpp`, 24 cases / 163 assertions, re-derived 2026-08-18 by `ninja test_breakable_graph && ./build/tests/test_breakable_graph`; the recorded 14/81 never re-derived at any head of this branch), and ONE break point registered at the DENSE ATTENTION ENTRY of `Qwen3ForCausalLM` (`src/vllm/model_executor/models/qwen3.cpp`, inside `RunLayer`). **The exit criterion W0 deliberately left open is ANSWERED on a leased GPU:** `cudaStreamEndCapture` then `cudaStreamBeginCapture` on the SAME stream mid-forward with EAGER work between is LEGAL under `cudaStreamCaptureModeThreadLocal` (`src/vt/cuda/cuda_backend.cu:204-206`) — `orin:gpu0` via an `rc` lease, driver 12060, 3 replays with fresh inputs, 0 mismatches, bare zero-work re-begin legal too. G2 reachability is `tests/vllm/models/test_qwen3_break_point.cpp`, which drives the production `Qwen3DenseModel::Forward` with a scope open and counts `num_hidden_layers + 1` segments (mutation: delete the call site ⇒ 1 segment ⇒ RED), and holds G4 in the same case at 500 logits / 0 differing bit for bit. STAGED SLICE, named: the scope and the container are not yet ENTERED from a production step — no driver opens a scope until W2 migrates `Qwen3DenseDecodeGraph` — and the spec's `## Owed` lists it with W2 as owner, alongside the D10 auxiliary-stream auto-join (W4/W5), G5's ROCm/Tenstorrent arms (W3) and G1 on a real GPU (W2). **The capture-failure drain is NOT among them: it landed HERE**, as behaviour (`std::uncaught_exceptions()` compared against the depth recorded at scope entry, so a break function or ordinary model code throwing mid-capture destroys the partial container instead of handing back a forward that reports `captured() == true`) and as three gated arms (tests 13a, 13b, 13c). The spec's `## Owed` strikes the item through and reads DELIVERED in W1; this cell said the opposite until 2026-08-18 because `cba969857` re-derived field 6 alone. **W2 DONE 2026-08-18 ([#1261](https://github.com/mudler/vllm.cpp/issues/1261)): `Qwen3DenseDecodeGraph` MIGRATED and the seam is ENTERED from a production step**, which retires W1's staged slice. `Qwen3DenseDecodeGraph::Step` opens a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` and replays through `BreakableGraph::Replay`; the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle, the `bool captured` flag, the `DestroyGraph` loop and the driver's own `VLLM_CPP_CUDAGRAPH` read are gone (re-derivation items 1, 2, 5, 6). The migration ADDED `vt::GraphCaptureMode`, mirroring vLLM's `CUDAGraphMode` (`vllm/config/compilation.py:59-63`), whose v1 default `FULL_AND_PIECEWISE` (`:63`) is documented at `:630-632` as a FULL graph for DECODE batches and a piecewise one for prefill/mixed, with `decode_mode()` (`:65-66`) selecting the full half and the runtime reading it at `vllm/v1/worker/gpu/cudagraph_utils.py:185-186`. A decode driver opened `kPiecewise` would have turned a fully graphed decode step into ONE EAGER ATTENTION CALL PER LAYER between graph replays — not vLLM's decode behaviour, and invisible to every token gate here. `GraphBreak` in a `kFull` scope takes the pass-through arm and `AppendBreak` REFUSES a registration in that mode. G2 is `tests/vllm/models/test_qwen3_decode_graph_seam.cpp` (3 cases / 124 assertions), which asserts the SEAM's counters because a driver calling `Backend::ReplayGraph` directly leaves an identical backend log; the mutation restoring the pre-W2 raw pair (18 lines, compiled clean) left `test_breakable_graph` 27/27, `test_qwen3_break_point` 2/2 and `test_qwen3_forward` 10/10 GREEN and reddened only this file. G4 in the same file: capture step vs `Qwen3DenseModel::Forward`, 100 logits, 0 differing. **The async decline at `qwen3.cpp` STANDS and is now GATED in both arms**: migrating the capture does not move the INPUTS, so the depth-2 race is untouched, and the fix is `StepDevInputs` as a SEAM capability, which is W4. **G1 is NOT met by W2** and is recorded owed rather than implied. **W3 DONE 2026-08-19 ([#1291](https://github.com/mudler/vllm.cpp/issues/1291)): the three remaining PLAIN BATCHED drivers migrate — `Qwen3MoeDecodeGraph`, `VoxtralDecodeGraph`, `DeepseekV2DecodeGraph` — one commit each, each with its own RED-first G2 gate.** Four of the nine drivers are now on the seam, and the six batched-driver `VLLM_CPP_CUDAGRAPH` reads `## Our baseline` item 1 counted are down to TWO, both in `qwen3_5.cpp` (W4). Each gate asserts the SEAM's counters and not the backend log, because a driver that kept its raw pair produces identical logits, an identical backend log and an identical `replay_count()`; red-first on four assertions each (`test_qwen3_moe_decode_graph_seam` 222/226, `test_voxtral_decode_graph_seam` 224/228, `test_deepseek_v2_decode_graph_seam` 224/228, all exit 1), green 3/3 each after. The G2 mutation — restoring each pre-W3 driver file, 25/102, 23/92 and 25/94 lines, each compiled clean — reddens ONLY its own gate and leaves `test_breakable_graph` 216/216 and W2's `test_qwen3_decode_graph_seam` 231/231 green. The gate harness is now SHARED (`tests/vllm/models/decode_graph_seam_harness.h`); three more copies inside `tests/` would have reproduced the duplication this row removes from `src/`. **G1 IS DELIVERED and is no longer owed** — the item W1 and W2 both carried. `tests/vllm/models/test_decode_graph_seam_g1_cuda.cpp` runs each driver COLD, CAPTURE and THREE consecutive replays against its own eager arm (selected by `max_num_reqs == 0`, so both arms are one binary on one device rather than two builds, each with its OWN device KV cache) on `thor:gpu0` through an `rc` lease — NVIDIA Thor sm_110, driver 595.78, nvcc 13.0.88, source `c905bb536`, 32 `.cu.o` objects, binary resolving `libcudart.so.13`/`libcublasLt.so.13`: **3 cases, 1600 assertions, exit 0, `5 steps x 100 logits, 0 differing, 4 replays` per driver.** The COUNT carries that claim, not the status line: with no CUDA backend the same file prints `SUCCESS!` over `assertions: 0`. Bounded honestly — synthetic tiny models rather than a checkpoint, and W2's driver shares the seam by argument rather than by measurement. **W3 also found a gate that could not fail.** The three gates' `breaks_registered == 0` mode guard is a TAUTOLOGY for any model with no registered break point, and the one production `vt::GraphBreak` in the tree is W1's in `qwen3.cpp`: flipping `kFull` to `kPiecewise` in `qwen3_moe.cpp`, one token, compiled clean and left that gate GREEN at 226/226. The mode was UNOBSERVABLE from outside a driver, so `vt::GraphBreakStats` gains `full_scopes`/`piecewise_scopes`, counted in `GraphCaptureScope`'s constructor on the ACTIVE path only, with an inert-scope control; the same flip now reds all three gates on exactly those two assertions. **NO break point is registered in these three models, deliberately**: under `kFull` it would be pass-through machinery no gate can exercise, and the break-point set is what the PIECEWISE arm needs (W4/W6). **The async decline, per driver:** Voxtral needs none (its only construction site is `VoxtralGenerateGreedy`, unreachable from the runner); Qwen3-Coder and DeepSeek carry a NEW FINDING instead — `qwen3_moe_registry.cpp:107`, `deepseek_v2_registry.cpp:106` and `glm4_moe_lite_registry.cpp:125` route an async step into a host-vector replay with no `device_token_ids` check at all, filed [#1305](https://github.com/mudler/vllm.cpp/issues/1305) with W4 as owner rather than mitigated on a measurement W3 cannot make. G5's ROCm/Tenstorrent arm is NOT discharged and moves to W5: the fleet carries no such device, so it is blocked on hardware rather than unattempted. **W4 DONE 2026-08-19 ([#1307](https://github.com/mudler/vllm.cpp/issues/1307)): the persistent device input path becomes a SEAM CAPABILITY, and the two Qwen3.5 drivers migrate.** `vt::PersistentStepInput` (`include/vt/persistent_step_input.h`, `src/vt/persistent_step_input.cpp`) binds a capture-stable device destination the DRIVER owns together with its pinned host staging block, and refreshes it in place from a host source or a DEVICE one; it owns the address-stability rule as a REFUSAL, the staging block, and the refreshing ARM as an observable (`last_source()`, `vt::StepInputStats`), and deliberately NOT the device allocation, because `Qwen3_5DecodeGraph` draws its retained inputs from a DEDICATED `DevicePool` so they never pop a block the captured forward's scratch then needs (D3). RED-first against a stub with the declared API and no guarantees: `tests/vt/test_persistent_step_input.cpp` 9 cases / 0 passed / 59 assertions / 32 failed / exit 1, GREEN after at 9/9 and 59/59; three mutations (delete the capacity refusal, make a null device source a silent no-op, collapse the host arm out of staging) each compiled clean and each reds exactly one case. `Qwen3_5DecodeGraph` and `Qwen3_5DenseDecodeGraph` open a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` in `kFull` and replay through it, and their `PinnedStepInputs`/`StageStepInputs` staging now runs THROUGH the capability, which is what makes it reachable rather than a class with a unit test. **Six of the nine drivers are on the seam** and `grep -rn 'std::getenv("VLLM_CPP_CUDAGRAPH")' src/` returns exactly ONE line, `src/vt/breakable_graph.cpp:61` — one switch, at last. Gate `tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp` RED-first on the MoE driver's five seam assertions (3 cases / 62 assertions / 5 failed / exit 1) and GREEN after at 7/7 and 129, G4 reading `40 values, 0 differing` per driver; G2 mutations: the whole pre-W4 file restored reds BOTH drivers (296 lines, 10 assertions), the MoE replay bypassing the container reds ONLY the MoE case (7 lines), the MoE `kFull`->`kPiecewise` flip reds ONLY its mode counters (3 lines), and deleting the `StageStepInputs` call site reds ONLY the reachability case while `test_persistent_step_input` stays 59/59 green — the difference between a class that works and a capability something reaches. **W4 FALSIFIED THIS ROW'S OWN PREMISE, which is its most important result.** This record and the spec both said the fix `qwen3.cpp`'s `DenseDecodeGraphForward`'s decline names already existed as `StepDevInputs`. It does not: `StepDevInputs` has NO token-id member, and its pinned sibling `PinnedStepInputs::token_ids` was allocated at capture, filled every step, zeroed by the poison hook, and NEVER uploaded or read — the embed runs OUTSIDE the captured region from the HOST vector in every batched driver, so **the decode graph carries no token ids to the device in ANY driver**. The dead block is removed. Consequently the DECLINE STANDS and [#1305](https://github.com/mudler/vllm.cpp/issues/1305) STAYS OPEN: W4 also read the decline's recorded cause against the tree at its own parent and found it falsified (the `DeviceTokenIdsScope` WAS live on the graph path, consumed by `EmbedInto` on all three arms at `qwen3.cpp:610,621,644 @ 338cbbfd1^`), so the measured failure is real and its mechanism is unidentified — not a state from which a refactor may retire a mitigation. The async battery was NOT run and W4 says so plainly: it needs `dgx` WITH the Qwen3-0.6B/4B checkpoints, `dgx:gpu0` was held by another session for W4's whole window, and W4's lease was `thor:gpu0`. Still NO throughput claim. W5 DONE 2026-08-19 ([#1335](https://github.com/mudler/vllm.cpp/issues/1335)): the THREE SINGLE-SHAPE drivers migrate — the DFlash draft graph, the DeepSeek V4 decode graph and the Laguna decode graph, whose own note at `laguna.cpp:2116-2119` asked for this seam by name and named V4's as the sibling that moves with it. **NINE OF NINE DRIVERS ARE 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 three per-model rollback switches stay (each an A/B lever for one driver); `VLLM_CPP_CUDAGRAPH` reaches all three for the first time. **D10, the auxiliary-stream fork/join, is DISCHARGED and REACHED** — `GraphCaptureScope` owns the outstanding-fork set and joins it before `EndCaptureGraph` (port of `breakable_cuda_graph.py:353-361` plus the `wait_stream` hook `:101-153`), registered by `vt::GraphNoteFork`/`GraphNoteJoin` from `laguna.cpp:2572-2576,2612`, the only fork inside a captured region by construction. Every prior stage opened `kFull`, which has ONE segment and so no between-segments window, so the rule could not be exercised before W5 and untested machinery was not landed for it. Gated as a COUNTER and an ORDER out of one backend trace, five arms including the control where the model joins first, and two mutations (deleting the join reds only the new case on 5 assertions; making it over-fire reds it on 8). DFlash is the ONE single-shape driver gateable without a GPU, because its admission predicate names neither a device type nor a kernel registry: `test_qwen3_dflash_decode_graph_seam.cpp` RED-first 3 cases/0 passed/16 assertions/7 failed exit 1, GREEN after 3/18, and the G2 mutation reds ONLY that file while seven other suites — the driver's own `test_dflash_propose` included — stay green. **G1 RE-RUN at W5's head on `thor:gpu0`** (sm_110, driver 595.78, nvcc 13.0.88, 32 `.cu.o`, source `79dc6b5bd`) because D10 put a join on the path of EVERY segment close, so the seam changed underneath the five measured drivers: `test_decode_graph_seam_g1_cuda` 5 cases / 2066 assertions / 0 failed, each reading `0 differing, 4 replays`, plus `test_breakable_graph` 265 on the same device. **And the one thing a green build could NOT have told us was measured separately**: Laguna's capture class sits behind `#ifdef VT_MARLIN_NVFP4`, so a passing build is the SAME OBSERVATION as one that compiled the region out. `-DVT_MARLIN_NVFP4=1` is on `laguna.cpp`'s own compile command, and an undeclared identifier injected immediately after its `GraphCaptureScope` line FAILED the object build under `-Werror` (`laguna.cpp:2735`) against an rc-0 baseline, restoring to an empty diff; the identical mutation on V4 failed at `deepseek_v4.cpp:1921`. Both migrated regions are COMPILED, which retires the could-not-even-be-built half. **G1 for all three and G2 for V4 and Laguna are OWED on hardware**, per driver and per reason: V4's `CanRunResidentDecode` refuses `kCPU` and needs the four CUDA-registered kernel families, Laguna's capture class exists only under `VT_MARLIN_NVFP4`. G5's ROCm/Tenstorrent arm stays BLOCKED — the fleet is all NVIDIA — and its owner moves from W5 to the ROW. Still NO throughput claim; analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) W6 DONE 2026-08-19 ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): the eligibility predicate, #1020, and the negative result on the piecewise arm. | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-BREAK-W6`; [#1163](https://github.com/mudler/vllm.cpp/issues/1163), [#1192](https://github.com/mudler/vllm.cpp/issues/1192), [#1261](https://github.com/mudler/vllm.cpp/issues/1261), [#1291](https://github.com/mudler/vllm.cpp/issues/1291), [#1307](https://github.com/mudler/vllm.cpp/issues/1307), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1020](https://github.com/mudler/vllm.cpp/issues/1020), [#1335](https://github.com/mudler/vllm.cpp/issues/1335), [#1374](https://github.com/mudler/vllm.cpp/issues/1374), [#1380](https://github.com/mudler/vllm.cpp/issues/1380), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1390](https://github.com/mudler/vllm.cpp/issues/1390) | | `ENG-CUDAGRAPH-DIFFUSION` | Capture the LTX-2.5 denoise loop (fixed shapes, many identical iterations — the ideal graph target). **BLOCKED, and the blocker is ours:** the render does almost no device compute to capture | T2 | SGLang enabled BCG on this shape AFTER our pin — LTX-2 H200 two-stage 10.75s->6.90s (`d4be483efb`), SANA 1024px -26% (`6c7498113f`), SANA denoise 0.73->0.457s (`56ef810cad`). Dated events, NOT pinned evidence; their win is mostly PyTorch host tax we do not pay | NO capture at all: `grep` for capture across `src/vllm/model_executor/models/ltx2*.cpp` returns nothing | blocked by [#1024](https://github.com/mudler/vllm.cpp/issues/1024) (GPU util **exactly 0 in 321 of 347 samples**, 1.00 core of 20 held for 17+ min after staging), [#1007](https://github.com/mudler/vllm.cpp/issues/1007) (VAE decode has no device arm), [#1087](https://github.com/mudler/vllm.cpp/issues/1087) (**57-66% of wall** is ONE resolution-CONSTANT serial host phase), [#1010](https://github.com/mudler/vllm.cpp/issues/1010) (no phase-boundary log). Decision point is a MEASUREMENT of GPU-busy vs wall once device-resident, not an implementation. **The unblock order now has an owning row:** `LTX25-DEVICE-RESIDENCY` ([#1264](https://github.com/mudler/vllm.cpp/issues/1264), [ltx25-device-residency.md](specs/ltx25-device-residency.md)) stages those defects W0-W6 and carries this decision point as its W7 — if the loop comes back GPU-bound, #1164 closes as a refutation the way [#1161](https://github.com/mudler/vllm.cpp/issues/1161) closed prefill capture | [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | `INVENTORIED` | [#1164](https://github.com/mudler/vllm.cpp/issues/1164) | | `ENG-BATCH-INVARIANT` | Opt-in deterministic execution across scheduler batch sizes (`VLLM_BATCH_INVARIANT=1`): batch-invariant matmul/norm/attention/collectives plus persistent-scheduler NVFP4; production default remains off | T1 | default/env `vllm/envs.py:89,576-578`; initialization `vllm/v1/worker/gpu_worker.py:1262`; NVFP4 dispatch `csrc/libtorch_stable/quantization/fp4/nvfp4_scaled_mm_sm120_kernels.cu:212-220`; suite fixture `tests/v1/determinism/conftest.py:9-12`; operator/e2e `tests/v1/determinism/test_nvfp4_batch_invariant_scaled_mm.py`, `tests/v1/determinism/test_nvfp4_batch_invariant.py` @ `702f481` | - | [W3-C3R executed contract](specs/nvfp4-persistent-plan-cache.md#w3-c3r-batch-shape-localization-and-gate-correction-2026-07-13): production-default ours and vLLM both change outputs across batch shapes; no local opt-in implementation is claimed | `planned: specs/batch-invariant-execution.md` | `INVENTORIED` | - | | `ENG-ASYNC-SCHED` | Async/overlap scheduling (AsyncScheduler placeholders + depth-2 batch-queue step + async D2H on a copy stream); vLLM's DEFAULT at the pin — mirror obligation per B3. **Host-side machinery + runner device-input half + sampler-OUTPUT half LANDED + CPU-gated (2026-07-16):** `AsyncScheduler` placeholder accounting, `step_with_batch_queue` depth-2, `ResolveAsyncScheduling` default-ON-when-compatible + `MaxConcurrentBatches`, `VT_ASYNC_SCHED` rollback; the runner device-input path `combine_sampled_and_draft_tokens`; PLUS the sampler-OUTPUT half — `vt::Backend` event/pinned primitives (`AllocPinned`/events, CUDA cudaHostAlloc+cudaEvent, CPU sync-degeneration), `AsyncGPUModelRunnerOutput` (device sampled-id snapshot → non-blocking D2H on a copy queue + event; `get_output()` waits only that event; MAIN queue never blocked), `Sampler::forward(sampled_ids_out)` device-resident greedy, `GPUModelRunner::sample_tokens_async` + `runner_supports_async`, and the `Executor`+`step_with_batch_queue` seam resolving `get_output()` at CONSUME time. All behind `VT_ASYNC_RUNNER`/`set_async_input_combine`, default OFF. Sync path byte-identical (placeholder sites INERT while count 0; combine off; `sample_tokens_async` degenerates to sync when async off; `sampled_ids_out=nullptr`). **ENABLE-FLIP LANDED + CPU-gated (2026-07-16):** (1) `LoadedEngine` now reorders `runner_` before the scheduler and builds an `AsyncScheduler` + `max_concurrent_batches=2` when `ResolveAsyncScheduling(runner_.runner_supports_async())` resolves ON (else the byte-identical synchronous `Scheduler` + depth-1); the resolved mcb threads into `AsyncLLM`→`EngineCoreProc` (`step_with_batch_queue`) and the "Asynchronous scheduling is enabled/disabled" log mirrors vLLM for A/B audit; (2) the device combine/scatter kernel (`_combine_sampled_and_draft_tokens_kernel` + last_sampled scatter) is ported to CUDA (`src/vt/cuda/cuda_combine_tokens.cu`), main-stream-ordered on the CUDA async path so it DELETES `sample_tokens_async`'s pre-scatter `Synchronize`; the CPU backend keeps the host loop. `VT_ASYNC_RUNNER=1` engages full W3; `VT_ASYNC_SCHED=0` is the same-binary rollback. Production default (no env) stays synchronous byte-identical. **FULL W3 DGX proof RAN twice** — `f086b64` (5/5 gates PASS; c16 TPOT −5.4 ms WIN, tput neutral, TTFT +36 % = Little's-law repayment) and the 2026-07-16 re-proof on the THROUGHPUT-lever fix (persistent pooled sampled-id/pinned buffers + `Sampler` greedy scratch removing ALL per-step `cudaMalloc`/`cudaFree`/`cudaHostAlloc`/event-create from the sampled-id path, incl. the overlap-killing `cudaFree` inside `get_output`; mirrors `gpu_model_runner.py:873-878` + `async_utils.py:12-70`): token-exactness **6/6 PASS**, interleaved c16 **tput −0.32 % (gate ≥+1.5 % FAILS), TPOT −4.95 ms retained, TTFT +34.8 %** — the allocator lever is REFUTED as the tput unlock (≤0.1 % of a ~165 ms c16 step). **DEFAULT FLIPPED ON 2026-07-17** (`VT_ASYNC_RUNNER` default ON via the pure `AsyncRunnerFlagIsOn` predicate, mirroring `vllm/config/vllm.py:992-1044`): the discriminator (`6ea7856`) proved vLLM's own async pays the identical +26–31 % TTFT / −0.7 to −0.9 % tput / −2.6 to −4.3 ms TPOT envelope and W3-ON nets positive (both binding ITL-tail anomalies flip to PASS), so the "needs a throughput lever" ship-gate is RETIRED — W3 is a parity/mirror obligation with a tails+TPOT win. The flip is TOKEN-NEUTRAL (async-ON ≡ async-OFF bit-identical on DGX). `VT_ASYNC_RUNNER=0` = runner-level rollback, `VT_ASYNC_SCHED=0` = scheduler-level rollback. TTFT means rise into vLLM's async envelope BY DESIGN — the next binding grid runs async by default and its TTFT must NOT be misread as a regression. **ROBUSTNESS FIX 2026-07-20 (`discard_request_mask`):** the runner was missing vLLM's `discard_request_mask`, so `GPUModelRunner` emitted a sampled token for prefill-CHUNK requests too; under async this drained a `num_output_placeholders` never reserved (the `is_prefill_chunk` path adds none) → the `async_scheduler.cpp` `num_output_placeholders >= 0` assertion aborted on c8 + short-output (chunked prefill + preemption). FIX mirrors vLLM: `execute_model` computes `exec_state_.discard[i] = seq_len < num_tokens` (`gpu_model_runner.py:2048`); `sample_tokens` clears those rows to empty (`outputs.py:303`), the async path passes `invalid_req_indices` to `AsyncGPUModelRunnerOutput::get_output` (`gpu_model_runner.py:3625` + `outputs.py:303`). Scheduler UNCHANGED (assertion kept — it was correct once the runner honors `scheduler.py:1888-1890`). Sync/non-chunked decode byte-identical (mask all-zero); DGX 27B 235/235 + 35B 315/315, `vllm-bench` c8+short-output+chunked+kv-pressure no longer crashes, memcheck 0. Ledger [parity-ledger.md](parity-ledger.md) 2026-07-20 row | T1 | `vllm/v1/core/sched/async_scheduler.py:12`; `vllm/config/vllm.py:490,990,1038`; `vllm/v1/engine/core.py:519`; `vllm/v1/worker/gpu/input_batch.py:304-406`; `vllm/v1/worker/gpu/async_utils.py:12-70`; `vllm/v1/worker/gpu/gpu_model_runner.py:242-332`; `vllm/v1/outputs.py:298-307` | `src/vllm/v1/core/sched/async_scheduler.cpp:10,45`; placeholder plumbing `src/vllm/v1/core/sched/scheduler.cpp:148,164,605`; `src/vllm/v1/engine/core.cpp:91` (`step_with_batch_queue`, async-output seam); `src/vllm/v1/engine/core_proc.cpp:32,46`; config `include/vllm/config/scheduler.h:117,165,188`, `src/vllm/config/scheduler.cpp:12`; `include/vllm/v1/request.h:187`; runner input leaf `src/vllm/v1/worker/gpu/prepare_inputs.cpp`, `src/vllm/v1/worker/gpu/input_batch.cpp`; runner output leaf `include/vt/backend.h`+`src/vt/backend.cpp`+`src/vt/cuda/cuda_backend.cu` (event/pinned), `include/vllm/v1/worker/gpu/async_output.{h,cpp}` (`AsyncGPUModelRunnerOutput`), `src/vllm/v1/sample/sampler.cpp` (`sampled_ids_out`), `src/vllm/v1/worker/gpu/runner.cpp` (`sample_tokens_async`/`runner_supports_async`), `src/vllm/v1/executor/executor.cpp`+`include/vllm/v1/worker/gpu/model_runner_base.h` (async seam); enable-flip `include/vllm/entrypoints/model_loader.h`+`src/vllm/entrypoints/model_loader.cpp` (`runner_` before scheduler, `ResolveAsyncEnabled`/`MakeScheduler`, `AsyncScheduler`+mcb=2, log), `include/vllm/v1/engine/async_llm.h`+`src/vllm/v1/engine/async_llm.cpp` (mcb param → `EngineCoreProc`); device kernel `include/vt/cuda/combine_tokens.h`+`src/vt/cuda/cuda_combine_tokens.cu`, wired `src/vllm/v1/worker/gpu/runner.cpp` (CUDA combine/scatter branch removes the pre-sync) | `tests/vllm/v1/test_async_scheduler.cpp:1` (6 cases, 54 asserts; RED vs base Scheduler 2/6 fail); depth-2 engine cycle `tests/vllm/v1/test_engine_core_proc.cpp:479` (mcb=2, async-output seam); config resolution `tests/vllm/test_scheduler_config.cpp:75`; enable-flip construction matrix `tests/vllm/entrypoints/test_loaded_engine_dense.cpp` (runner×VT_ASYNC_SCHED → scheduler type + mcb; RED = un-flipped engine, 3/3 ON-arm asserts fail); runner input leaf `test_combine_tokens.cpp` (RED = stale → 5/7 fail), `test_input_batch.cpp`, `test_runner.cpp` (async-ON≡sync); output leaf `tests/vt/test_backend.cpp` (event/pinned contract), `tests/vllm/v1/worker/test_async_output.cpp` (materialize/flush/snapshot; RED = +1 splice), `test_runner.cpp` (`sample_tokens_async` decode ≡ sync); full CPU ctest 111/111, tools 164/164. Prior diagnostic `3812d8` six-leg control: total **1.002153×**, TTFT **0.862159×**, no GPU-time reduction (neutral for speed). **DEFAULT-FLIP (2026-07-17):** new pure CPU flag test [test_async_runner_flag.cpp](../tests/vllm/v1/worker/test_async_runner_flag.cpp) (11 asserts, default-ON/'0'-off); construction matrix [test_loaded_engine_dense.cpp](../tests/vllm/entrypoints/test_loaded_engine_dense.cpp) INVERTED (default → AsyncScheduler+mcb=2; RED verified 5 asserts fail vs un-flipped). CPU clean `-Werror` rebuild, full serial ctest **116/116**, tools **164/164**. **DGX re-confirmation** (evidence `dgx:~/work/vllm.cpp-async-flip`, CUTLASS+FA2 hard-verified, one flock): shipping default (async ON + RMSNorm-fast OFF) → **27B 235/235 + 35B 315/315** with the "Asynchronous scheduling is enabled (mcb=2)" log, and both rollback arms (`VT_ASYNC_RUNNER=0`, `VT_ASYNC_SCHED=0`) 235/235 + 315/315 log "disabled"; async arms BIT-IDENTICAL (token-neutral). Closing record [parity-ledger.md#L502](parity-ledger.md#L502) | [async-serving.md](specs/async-serving.md) | `DONE` | `6ea7856` | diff --git a/.agents/issue-index.md b/.agents/issue-index.md index d07b5a10a..d1d0b49c2 100644 --- a/.agents/issue-index.md +++ b/.agents/issue-index.md @@ -455,3 +455,4 @@ rather than merged. `scripts/check-agent-record.py` gates both. | [#1380](https://github.com/mudler/vllm.cpp/issues/1380) | `ENG-CUDAGRAPH-BREAK` | A speculative decode-graph capture does a `cudaMalloc` inside the captured region and throws on `thor:gpu0` (sm_110), and the queue is POISONED afterwards. Located to the SECOND parity-ring slot: slot 0 cold, slot 1 cold, slot 0 captures (`captured()` true, `replay_count()` 1), slot 1's capture throws `cudaMalloc: operation not permitted when stream is capturing`, and the next step fails with `embedding: operation failed due to a previous error during capture` without opening a scope. So a REPLAY is unreachable on a speculative shape there, on a default-ON path (`VT_SPEC_DECODE_GRAPH`). The driver's own pre-grow names this case and covers only the retained `[S, vocab]` logits. Found by [#1374](https://github.com/mudler/vllm.cpp/issues/1374) and PRE-EXISTING — the case drives the driver directly, bypassing the predicate, and the five migrated drivers read 2066 assertions / 0 differing on the same binary. NOT fixed in flow: it is a device-level allocation defect on a path W6 did not write, it needs `dgx`/sm_121a and a real checkpoint to scope, and `AGENTS.md` routes a surprising fix to the normal row, spec and fresh-review path. Owned by row `ENG-CUDAGRAPH-BREAK`, under `## Owed` in [`eng-cudagraph-break.md`](specs/eng-cudagraph-break.md) | bug | | [#1376](https://github.com/mudler/vllm.cpp/issues/1376) | `ENG-CUDAGRAPH-BREAK` | `main` was red on `tests/scripts/test_check_gate_commands.py`, measured at `601b576c6` in a detached worktree of `origin/main`: 8 failures of 44 tests, every one a comparison between the computed runnable population and `RUNNABLE_BASELINE`. `ENG-CUDAGRAPH-BREAK` was in the first and absent from the second. Cause: W5 of that row ([#1361](https://github.com/mudler/vllm.cpp/issues/1361)) filled its spec's `## Gates` section with runnable evidence, including a named test binary with its case and assertion counts and an exit status, which is exactly what moves a row into the runnable population. The ratchet's own error text instructs a re-pin in the SAME change, and the re-pin was not made. This is the growth case the ratchet exists to force a decision about, not a defect in that row's work. **It landed with no remote verdict**: the continuous integration lane that would have caught it independently has not executed for this repository since roughly 07:43Z on 19 August 2026, with runs queueing and none starting while GitHub reports Actions operational. FIXED IN FLOW while merging `origin/main` into `row/ENG-HF-MODEL-DOWNLOAD` for [#1280](https://github.com/mudler/vllm.cpp/issues/1280), because the fix is small and clear and a red `main` blocks every other row's gate. The entry is added with a justifying comment in the form the neighbouring entries use, no checker semantics change, and no test is weakened. After the re-pin the suite reports 45 tests OK and the audit reads 39 runnable of 119 gated rows | bug | | [#1375](https://github.com/mudler/vllm.cpp/issues/1375) | `MODEL-DIFFUSION-LTX25` | First end-to-end per-forward cost for the FULL 21.004 B LTX-2.5 DiT on GB10, measured on run `20260819T150230Z` with binary `0a43a750` built from [`7b9e207b1`](https://github.com/mudler/vllm.cpp/commit/7b9e207b1) (#1252). At 1024x576/25f (2304 latent tokens) the governor resolved **7 forward starts from the GPU busy/idle edge counter** and measured `per_forward ~162.0 s` with `first_dit = 481.5 s`, so the recipe's fixed 60 forwards (30 steps x 2 CFG legs, `ltx2_pipeline.cpp:521-529`) project **10 803 s against the rung's 7 153 s budget** and the rung was refused rather than run to the wall. The same lease then COMPLETED 768x448/25f (1344 tokens) in 2990 s, so the ceiling is geometry against lease length, not a defect. TWO instrument facts belong with the number, because both have already caused a wrong reading: `gpu_edges=0` means the GPU never went idle long enough to sample an edge (SATURATED), not that no work ran — this rung sampled 85% of 3191 samples above 50% utilisation; and `eu-stack` resolves no frames in the rc worker container, so phase attribution came from the cpu%/rss signature rather than from symbols. Owned by the LTX-2.5 row; spec [`ltx-2-5.md`](specs/ltx-2-5.md) | measurement | +| [#1390](https://github.com/mudler/vllm.cpp/issues/1390) | `ENG-CUDAGRAPH-BREAK` | `test_qwen3_5_decode_graph_seam` SIGSEGVs on `main` and every assertion passes. Measured at `5f68e60df`, which is `origin/main` exactly, CPU Release x86_64: 8 cases, 7 passed, 1 failed, an assertion line reading 135 of 135 passed and 0 failed, exit 139, with `W6: two spec shapes of EQUAL S and different q get two graphs` reporting `CRASHED: SIGSEGV`. The number a reader greps says 135/135, so only the exit status and the `CRASHED` line carry the verdict. ORDER-DEPENDENT: `-tc="W6*"` alone passes 9/9 exit 0, so the crash needs state an earlier case in the same process left behind — a doctest binary runs every case in one process, and a leaked pool block, a leaked backend or platform registration, or a driver slot captured under one shape and re-entered under another are all live candidates. `gdb -batch -ex run -ex bt` puts the fault inside `vt::cpu::PagedAttentionKernel` on a `vt::cpu::Threadpool` worker, which is what a block table, slot mapping or sequence length that does not describe the handed KV cache looks like. The crashing case is the one [#1374](https://github.com/mudler/vllm.cpp/issues/1374) added for the `(S, q, spec)` ring key. Found while landing [#1305](https://github.com/mudler/vllm.cpp/issues/1305) and NOT caused by it: reverse-applying that branch's whole source change and rebuilding leaves the same exit 139, and that change touches `qwen3_moe.cpp`, `deepseek_v2.cpp` and three registry translation units, none of which this binary's crashing case executes. NOT fixed in flow, because `AGENTS.md` routes a surprising fix to the normal row, spec and fresh-review path: it is a segmentation fault in another stage's newly landed code, its mechanism is an unlocated cross-case state leak, and `src/vllm/model_executor/models/qwen3_5.cpp` is under concurrent edit for [#1380](https://github.com/mudler/vllm.cpp/issues/1380). Owner: row `ENG-CUDAGRAPH-BREAK`, under `## Owed` in [eng-cudagraph-break.md](specs/eng-cudagraph-break.md) | bug | diff --git a/.agents/specs/eng-cudagraph-break.md b/.agents/specs/eng-cudagraph-break.md index bbeaebcd1..b6b8be26b 100644 --- a/.agents/specs/eng-cudagraph-break.md +++ b/.agents/specs/eng-cudagraph-break.md @@ -1517,6 +1517,16 @@ region by construction, and that driver is the production caller. Gated as a counter and an ORDER out of one backend trace, with two mutations proving neither the rule nor its control arm is vacuous. +**#1305 IS FIXED, and reading the tree made it a bigger defect than the issue +described.** The three registrations it names never published +`detail::DeviceTokenIdsScope` and neither model's `EmbedInto` ever consulted one, +so `device_token_ids` reached nothing in either translation unit — the eager arms +as well as the decode graph. Both now consume it, and each decode-graph slot +holds a `vllm::StepTokenIds` on `vt::PersistentStepInput`, which is +`RefreshFromDevice`'s first production caller. What is NOT closed is the depth-2 +battery on a device; `## Owed` carries it, and `qwen3.cpp`'s decline is +untouched. + **W4 corrected a premise this spec had asserted three times.** The decode graph carries NO token ids to the device in any driver, `StepDevInputs` included, so the `qwen3.cpp` async decline was never one refactor away from removable. It @@ -1775,7 +1785,22 @@ Each item names the stage that owns it. Nothing here is claimed by W1. "a record edit rides in the pull request whose change made the record stale". The decline itself is UNCHANGED. - **`RefreshFromDevice` therefore lands with NO production caller, and that is + **`RefreshFromDevice` HAS A PRODUCTION CALLER as of + [#1305](https://github.com/mudler/vllm.cpp/issues/1305), which retires the + staged slice below.** `Qwen3MoeDecodeGraph` and `DeepseekV2DecodeGraph` each + give their padded size slot a `vllm::StepTokenIds` + (`include/vllm/model_executor/models/step_token_ids.h`), whose destination is a + device buffer with a stable address and whose refresh takes the DEVICE arm + whenever the runner's mirror is live; `last_source()` and `StepInputSource` + gain their reader with it. Reached from `ModelRegistry::Forward` through + `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and + `glm4_moe_lite_registry.cpp`, and gated at + `tests/vllm/models/test_moe_async_device_ids.cpp`, which enters at that entry + point over a synthetic safetensors checkpoint and reds when the registry's + scope line is deleted. The paragraph below is the record as W4 wrote it and is + kept for provenance. + + **`RefreshFromDevice` landed with NO production caller, and that was the staged slice AGENTS.md admits rather than an oversight.** `grep -rn RefreshFromDevice src/ include/` returns the definition alone; `last_source()` and `StepInputSource` have no production reader either. The @@ -1824,6 +1849,67 @@ Each item names the stage that owns it. Nothing here is claimed by W1. run the battery shape against `Qwen3MoeDecodeGraph` and `DeepseekV2DecodeGraph` and find out whether they degenerate at depth 2 at all. Owner: row **`ENG-CUDAGRAPH-BREAK`**, the stage that gets that window. + + **RESOLVED, AND NOT THE WAY EITHER W3 OR W4 EXPECTED, because reading the tree + found a LARGER defect than the one #1305 describes and a fix that needs no + decline at all.** #1305 reads as a graph-arm hazard. It is not: those three + registrations never constructed a `detail::DeviceTokenIdsScope` and neither + `qwen3_moe.cpp`'s nor `deepseek_v2.cpp`'s `EmbedInto` ever consulted one, so + `ModelForwardInput::device_token_ids` reached NOTHING in either translation + unit. The decode graph, `ForwardDevice` and `Forward` all embedded the host + vector the runner's mirror arm deliberately leaves stale for decode rows. That + is a defect on the EAGER path too, which no decline could have mitigated, and + it is why the fix is the consumption rather than the refusal. + + What landed: the three registries publish the scope, the same 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; and each decode-graph size slot holds a `vllm::StepTokenIds` + whose destination is a device buffer with a stable address, refreshed through + `vt::PersistentStepInput` — host arm for the padded vector, DEVICE arm over the + real prefix, both on the main queue so the second is ordered after the combine + rather than racing it. That is the version of the fix this row was scoped to + produce, and it gives `RefreshFromDevice` its first production caller instead + of a fifth private copy. + + Gated at `tests/vllm/models/test_moe_async_device_ids.cpp`, entered at + `ModelRegistry::Forward` over a synthetic safetensors checkpoint for both + architectures: three runs each — right host ids and no mirror as the reference, + stale host ids and no mirror as the CONTROL that must differ, stale host ids + with the truth reaching the model only through `device_token_ids` as the gate. + RED before the fix at 2 cases / 59 assertions / 12 failed / exit 1, with 200 of + 200 logit values differing per step on both architectures; GREEN after at + 59/59. Two mutations: deleting the registry scope line reds it at 6 assertions, + and swapping the seam's DEVICE arm for its HOST arm leaves the logits BIT + IDENTICAL and reds only the counters, which is the arm no token gate can see. + + **WHAT IS STILL OWED, narrowed rather than closed.** The depth-2 + four-concurrent battery against these two models on a real device has NOT been + run: it needs a GPU and a real checkpoint, and this stage had neither. So the + fix is proven to embed the mirror's identifiers and is NOT proven to close the + degeneration `qwen3.cpp`'s decline was measured against — whose own cause W4 + established is unidentified. `qwen3.cpp`'s decline therefore STANDS, untouched. + Owner: row **`ENG-CUDAGRAPH-BREAK`**, the stage that gets a `dgx` window with + checkpoints; the same window the decline entry above already owes two runs to. +- **`test_qwen3_5_decode_graph_seam` SIGSEGVs on `main`, in W6's own case, and + every assertion passes** + ([#1390](https://github.com/mudler/vllm.cpp/issues/1390), found while landing + [#1305](https://github.com/mudler/vllm.cpp/issues/1305), not caused by it). + Measured at `5f68e60df`, which is `origin/main` exactly, CPU Release: `8 cases, + 7 passed, 1 failed, 135 assertions, 135 passed, 0 failed`, exit 139, with + `W6: two spec shapes of EQUAL S and different q get two graphs` reporting + `CRASHED: SIGSEGV`. **The assertion counter cannot see it** — the number a + reader greps says 135/135 — so only the exit status and the `CRASHED` line + carry the verdict. It is ORDER-DEPENDENT: `-tc="W6*"` alone passes at 9/9, + exit 0, so the crash needs state an earlier case in the same process left + behind. `gdb` puts the fault inside `vt::cpu::PagedAttentionKernel` on a + threadpool worker, which is what a block table or slot mapping that does not + describe the handed KV cache looks like. Reverse-applying #1305's whole source + change and rebuilding leaves the same exit 139, and that change executes none + of this binary's crashing path. NOT fixed in flow: a segmentation fault in + another stage's newly landed code, mechanism unlocated, in a file under + concurrent edit for [#1380](https://github.com/mudler/vllm.cpp/issues/1380). + Owner: row **`ENG-CUDAGRAPH-BREAK`**, the stage that owns W6. - **An exception CAUGHT INSIDE the capture scope leaves a partial capture the drain cannot see.** The `uncaught_exceptions()` comparison in `~GraphCaptureScope` detects an exception that is PROPAGATING at scope exit. A From ebce3a1e6cede5bc84d9b52f121fe6834708cd35 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 19 Aug 2026 18:23:42 +0000 Subject: [PATCH 4/9] test(ENG-CUDAGRAPH-BREAK): compare EVERY step, and draw the mirror's buffer from the backend (#1305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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] --- .agents/engine-matrix.md | 2 +- .agents/specs/eng-cudagraph-break.md | 25 +++++--- .../vllm/models/test_moe_async_device_ids.cpp | 58 +++++++++++++------ 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/.agents/engine-matrix.md b/.agents/engine-matrix.md index 4bdd1867b..fc633b0bb 100644 --- a/.agents/engine-matrix.md +++ b/.agents/engine-matrix.md @@ -61,7 +61,7 @@ forensics: roadmap_v1.md and the parity ledger. | `ENG-PREEMPT-RECOMPUTE` | FCFS tail preemption with recompute | T0 | `vllm/v1/core/sched/scheduler.py:1142`; `tests/v1/core/test_scheduler.py:930` | `src/vllm/v1/core/sched/scheduler.cpp:102,157`; `src/vllm/v1/core/sched/request_queue.cpp:36` | `tests/vllm/v1/test_scheduler.cpp:247,295`; `tests/vllm/v1/test_request_queue.cpp:91` | `planned: specs/preemption.md` | `ANCHOR-BACKFILL` | - | | `ENG-CUDAGRAPH` | Decode graph capture/replay modes (host-cluster cleanup: capture-size set derived from `max_num_seqs` mirroring vLLM `_set_cudagraph_sizes`; 2026-07-18 graph-baked-scratch use-after-free fix — the 35B c2+ online-serving IMA blocker) | T0 | `vllm/config/compilation.py:53,1319,683-684,1438-1444`; `vllm/config/vllm.py:1667-1770`; `vllm/v1/worker/gpu/cudagraph_utils.py:116`; `tests/compile/test_config.py:122,229` | `src/vt/cuda/cuda_backend.cu:76,97,105`; `include/vllm/model_executor/models/decode_graph_sizes.h`; `src/vllm/model_executor/models/qwen3_5.cpp:3754,3952`; `src/vllm/v1/worker/gpu/runner.cpp:577,597`; graph-safe scratch (retire-on-grow so graph-baked scratch pointers stay valid) `src/vt/cuda/graph_safe_scratch.h`, `src/vt/cuda/cuda_moe_marlin.cu:75`, `src/vt/cuda/cuda_matmul_nvfp4.cu:766`, `src/vt/cuda/cuda_matmul_nvfp4_cutlass.cu:105`, `src/vt/cuda/cuda_matmul_fp8_cutlass.cu:95` | `tests/vt/test_cuda_backend.cpp:98`; `tests/vllm/models/test_decode_graph_sizes.cpp`; `tests/vt/test_graph_safe_scratch.cpp`; explicit 35B gate `tests/parity/test_qwen36_paged_engine.cpp:140` | [blocktable-host-cluster-cleanup.md](specs/blocktable-host-cluster-cleanup.md); [decode-graph-scratch-uaf-2026-07-18.md](specs/decode-graph-scratch-uaf-2026-07-18.md) | `PARTIAL` | **PREFILL capture REFUTED as a lever (2026-08-17, [#1161](https://github.com/mudler/vllm.cpp/issues/1161)).** vLLM's v1 default already captures prefill piecewise (`vllm/config/compilation.py:60-63,615,630` @ `555967922`) and it is in our denominator; SGLang reached the same coverage without `torch.compile` via BCG (`SGLANG-BCG` in [sglang-matrix.md](sglang-matrix.md)). Neither helps us: GB10 2026-07-09 measured prefill GPU-idle-between-launches at **3.8%** with GPU-busy >96% on both arms, and the 27B prefill gap at **92.5% non-GEMM glue GPU work** with the dominant GEMM at +0.17% and attention AHEAD. There are no launch bubbles in our prefill to collapse. Row stays `PARTIAL`; the real residuals are exec dedup ([#1162](https://github.com/mudler/vllm.cpp/issues/1162)) and the break-point seam ([#1163](https://github.com/mudler/vllm.cpp/issues/1163)). Spec [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | | `ENG-CUDAGRAPH-DEDUP` | Graph-executable dedup: hash each captured graph's topology and re-point ONE `cudaGraphExec` with `cudaGraphExecUpdate` on a signature hit, instead of instantiating one exec per padded bucket per model. A memory and capture-time change, NOT a throughput change — a deduped replay launches the same nodes, and the load-bearing gate is byte-identity rather than a ratio | T2 | vLLM has no analogue (its execs come from `torch.compile`, `vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); secondary oracle SGLang `python/sglang/srt/model_executor/runner_backend/cuda_graph_dedup_mixin.py:27-37,105-179,219-242,258-275,353-358` @ `f63458b5be` ([oracles/sglang.md](oracles/sglang.md)) | W1+W2 landing here behind `VT_CUDA_GRAPH_DEDUP`, default OFF until the device A/B measures the per-switch update cost: a device-agnostic dedup registry shared by both accelerator backends plus one CUDA/HIP ops table written once, wired into `EndCaptureGraph`/`ReplayGraph`/`DestroyGraph`. Baseline it replaces: `src/vt/cuda/cuda_backend.cu:222-232` instantiates a fresh exec per capture and destroys the raw graph, over the 7 (`max_num_seqs=32`) or 11 (64) buckets of `include/vllm/model_executor/models/decode_graph_sizes.h:32-41`, times NINE drivers (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight, missing the DFlash draft graph `src/vllm/model_executor/models/qwen3_dflash.cpp:771,870,1038,1091,1095,1106`) | `tests/vt/test_graph_dedup.cpp` 13/13 cases, 65 assertions, RED-first (written and run against an absent header, and the four cases added by the fresh review of #1178, three of them run against the unfixed source) and gated on every platform via a fake ops table whose launch log makes "the right nodes ran" an observable sequence over MORE than one replay per shape; 13/13 negative mutations detected (9 at implementation, 4 at review repair). That count covers `src/vt/graph_dedup.h` ONLY. `src/vt/graph_dedup_runtime.h` had NO executable coverage on any tier, and [#1184](https://github.com/mudler/vllm.cpp/issues/1184) is what hid in that gap: the file is DESIGNED to see runtime calls fail — a refused `cudaGraphExecUpdate` probe is the feature working — and never consumed the runtime's latched error, so the next unrelated kernel reported the refusal as its own failure and every `VT_CUDA_GRAPH_DEDUP=1` run died 6/6 on GB10 as `greedy_argmax launch: invalid device function` from a launch that had succeeded. Repaired structurally rather than at twelve sites: the clear lives in `ScopedLatchClear`'s destructor (`src/vt/graph_dedup_latch.h`) installed at the six `GraphDedupOps` entry points by `MakeLatchGuardedOps`, the table's only constructor, so no raw function address reaches a field and an unwired seventh operation leaves a null the registry refuses; one line covers CUDA and HIP. The device-free half of the signature walk moved to `src/vt/graph_dedup_signature.h` and is gated by `tests/vt/test_graph_dedup_runtime.cpp` 13/13 cases, 51 assertions, RED-first against the pre-fix guard (22 failed assertions reproducing the production message), 7/7 negative mutations detected — Kahn ordering, topological re-index, sorted edge emission, the depth-4 child bound and the four graph-level escapes. STILL compile-gated only: the five node-payload cases behind the device policy. **DEVICE A/B DELIVERED 2026-08-18 on `dgx:gpu0` (GB10, driver 580.173.02, nvcc 13.0.88, `rc` job f88d484b), and it SPLIT.** Gated commit `72de552c8`, whose four dedup sources are byte-identical to the merged `2a976eb9f` — the row squashed, so the gated tree is not an ancestor of the merge and that sha equality is what carries the claim. CORRECTNESS PASSES: 12/12 cells exit 0, zero `invalid device function` and zero `engine-fatal` in every cell log where the pre-fix head `e4ce5571a` died after exactly one replay, ON replays as often as OFF (60=60, 33=33, 43=43), and `--output-token-ids` is IDENTICAL over 10/10 comparisons with the three OFF/OFF controls passing FIRST and the three workloads hashing to three DIFFERENT values, so the identity is not vacuous. #1184 is closed by this run, because a CPU suite drives a fake runtime and cannot observe the real latched error. THE BENEFIT IS REFUTED for the case this row was filed for: `N == M` in every ON cell — 3 graphs to 3 execs on sizes [24 16 8], 2 to 2 on [16 8], 2 to 2 on [32 24] — with the registry's count CLIMBING 1→1, 2→2, 3→3, so more than one capture reached it and the 1:1 is a measurement rather than the single-capture artefact the first attempt produced. Cause pre-registered before the run and then confirmed, structural rather than a tuning miss: `AppendKernelPayload` hashes (`func`, `gridDim.{x,y,z}`, `blockDim.{x,y,z}`, `sharedMemBytes`) at `src/vt/graph_dedup_runtime.h:121-128` and the memcpy payload hashes the copy extent, so the padded batch dimension sits in the KEY, no candidate group ever forms and `cudaGraphExecUpdate` is NEVER ATTEMPTED. That contradicts this row's own premise — `graph_dedup.h`'s header says the fold is for "two padded batch sizes … the same node topology with different parameters" — and SGLang keys the same fields (`cuda_graph_dedup_mixin.py:105-114`), so whatever folds upstream is not decode buckets either. NO throughput or memory number is recorded: clocks unpinned AND the ON arm allocated exactly as many executables as OFF. Honest gaps: per-shape replay counts are unavailable (the driver prints a TOTAL, so B's ~30-per-shape is arithmetic); the driver's "N captured size(s)" counts SLOTS not captures (A reports 6, emits 3); the container's own cuBLASLt was never re-tested at CUDA 13.0 because the staged cu130 prefix was probed first and worked; only the Qwen3 dense decode driver was exercised. STILL OWED: the default flip, now NOT JUSTIFIED on this evidence rather than merely ungated; a COARSER key that could group two decode buckets at all, which the probe-before-fold design makes a cost question rather than an obviously unsafe one ([#1226](https://github.com/mudler/vllm.cpp/issues/1226), the next traceable hypothesis, deliberately NOT decided by this record); device-tier signature stability/discrimination tests; probing `current_raw` instead of `raws.front()` to retire the update-transitivity assumption; the ROCm compile; a supporting `orin:gpu0` leg, BLOCKED because the Jetson 540.4.0 driver cannot run a CUDA 13 runtime (`cudaGetDeviceCount err=35`); and reaching the feature from the default serving path at all — the async runner captures no decode graph, **W5, THE SAME DAY, CONFIRMED THE HYPOTHESIS THAT NEGATIVE PRODUCED ([#1226](https://github.com/mudler/vllm.cpp/issues/1226) DELIVERED).** Same box, `rc-worker-4b8lj`, boot_id `3fd9745a-d25a-426c-ba3c-97c958a85515` at both ends, GB10, driver `580.173.02`, `### DONE_AB_KEY 2026-08-18T20:58:46Z`, binary sha256 `ca114abb…c772ad` from `b48b51df1` (tar sha256 asserted before extraction). Drop the launch dimensions and the memcpy extents from the key and every bucket folds: `a_coarse` 3 graphs to 2 execs, `b_coarse` 2 to 1, `c_coarse` 2 to 1, each `probes=1 refused=0`, against `probes=0 refused=0` in every EXACT cell. **`probes=0` in the EXACT cells is the direct process-level proof of W4's source-level diagnosis** — with the launch dimensions in the key no candidate group forms and `cudaGraphExecUpdate` is never asked; drop them and it is asked once per fold and ACCEPTED EVERY TIME. The saving W4 recorded as unreachable is reachable via the key. Byte-identity holds on A (five cells, `59ebff4a…`) and C (four cells, `ff205260…`). **Workload B is VOID rather than a pass, and its cause is a NEW DEFECT that is not this row's:** the two `VT_CUDA_GRAPH_DEDUP`-unset control cells DISAGREED (`5973c5a1…` 2638 bytes vs `4cf79230…` 2650 bytes) on one binary, one workload, greedy `--temperature 0 --seed 777` at `--concurrency 16`, 23 s apart — 672 tokens both, so the byte delta is JSON width and not a length; exactly rows 17 and 18 of 21 differ, both mid-decode, both in the ragged tail `21 % 16` leaves. B's `b_off_a == b_exact` and `b_off_a == b_coarse_a` therefore compare against a baseline that does not reproduce itself and are WORTHLESS; only the OFF/OFF control made that visible, and without it B would have read as three more confirmations. Filed [#1283](https://github.com/mudler/vllm.cpp/issues/1283). **Caveats that bound this result:** nvcc was `13.3.73` here and `13.0.88` for the W4 baseline the recorded dgx gate stack names, so the OFF-vs-ON and EXACT-vs-COARSE comparisons WITHIN this binary are valid while this run and that baseline are NOT directly comparable; clocks unpinned (2405 MHz current, 3003 max, 2418 applications) and nothing measured bytes, so NO throughput and NO memory number is claimed or implied; only the Qwen3 dense decode driver was exercised; `refused=0` is ONE driver on ONE hardware and toolkit pair, which is no more a floor than W4's negative was a ceiling; and the coarse key is behind `VT_CUDA_GRAPH_DEDUP_COARSE_KEY`, default OFF, inside a default-OFF flag, on **PR [#1232](https://github.com/mudler/vllm.cpp/pull/1232) which is STILL A DRAFT — nothing on `main` folds today.** **Row stays `ACTIVE`, argued:** not `DONE`, because the fold is unreachable on every shipping configuration and the row's stated MEMORY saving has never been measured in bytes on either key; not `PARTIAL`, because nothing upstream is omitted — the coarse key is our own extension past SGLang, which keys the fields we started from; not `BLOCKED`, because nothing external stops the next step. What is owed is now a DECISION about the default plus the byte measurement and the probe-cost-at-real-churn measurement it needs, and landing #1232 first **W6, 2026-08-19, THE DEVICE-BYTE MEASUREMENT — THE BENEFIT QUESTION IS NOW CLOSED AND THE ANSWER IS NEGATIVE.** Tested `origin/main` `2c8f53d93`, which is PR #1232 LANDED, so the "nothing on `main` folds today" caveat every earlier record carried is RETIRED and this measures a configuration that ships. Same box, `rc` job `93f783de`, pod `rc-worker-4b8lj`, boot_id `3fd9745a-…` at BOTH ends, GB10, driver `580.173.02`, nvcc **13.0.88** (the W4 baseline toolkit; W5 ran 13.3.73, so W6 and W5 are NOT directly comparable while comparisons WITHIN this one binary are valid), binary sha256 `be697268…0ce657a7`, `### DONE_BYTES 2026-08-19T04:57:19Z`, 12/12 cells exit 0, zero VOID markers. **THE FOLD ENGAGES AT THE SHIPPED BUCKET SET**, which is the churn W5 could not produce: `vllm-bench` sets `max_num_seqs = concurrency`, so W32 captured `[1 2 4 8 16 24 32]` 7-of-7 and W64 captured `[1 … 64]` 11-of-11, exactly `decode_graph_sizes.h:32-41`, against the 2-3 buckets every earlier conclusion was drawn from. COARSE folds 7 graphs to 3 execs (`probes=7 refused=3`) and 11 to 5 (`probes=22 refused=16`); EXACT folds NOTHING at `probes=0`, reproducing W4 at four times the bucket count. Token ids byte-identical across every cell of a workload INCLUDING both OFF/OFF controls (`ff0db6c6…be9d` 11720 B; `e1cbf5fc…e5d0` 57620 B) — neither workload has #1283's ragged-tail shape and neither hit it. **THE SAVING DOES NOT SURVIVE ITS OWN NULL CONTROL.** `nvidia-smi --query-compute-apps` tail median (the `--query-gpu=memory.used` axis returns `[N/A]` on this box) shows W64 IDENTICAL to the megabyte in all five cells (9737) and W32's coarse arm reading 10-23 MiB HIGHER than OFF (3252/3262 vs 3262/3275). A `cudaMemGetInfo` shim summed over every instantiate gives a nominal 13.83 MiB at 7 buckets — **0.42% of a 3.25 GiB process** — and **−0.75 MiB, i.e. NOTHING, at 11**. That nominal effect is NOT ESTABLISHED on four independent grounds: `EXACT` is a TRUE NULL (same 7 and 11 retained execs, `probes=0`, so it allocates what OFF allocates) and disagrees with OFF by 10.6-13.1 MiB against a 13.83 MiB candidate; the W64 OFF/OFF pair disagrees with ITSELF by 18.2 MiB; one instantiate recorded a NEGATIVE delta (`-5,165,056` B); and `cudaGraphExecDestroy` reclaimed `0` in EVERY cell. Per-instantiate deltas for byte-identical 404-node graphs span 0 to 10,514,432 B and 17 of 27 instantiates in one cell read exactly zero, so these are POOL-GRANULAR readings and the coarse arm's throwaway probes grow that pool exactly like retained execs do. What CAN be priced: one ~390-node executable at **2.08-4.35 MiB**, 10.0-10.6 KB per node — the figure to re-run on a deep checkpoint. **THE MECHANISM INVERTS THIS ROW'S PREMISE.** The driver refuses **43% of probes at 7 buckets and 73% at 11**, every one of them `probe refused a fold (err=910 result=2)` = `cudaErrorGraphExecUpdateFailure` / `cudaGraphExecUpdateErrorTopologyChanged`. The shim's `cudaGraphGetNodes` reading says why false candidates form: the decode graphs are **TWO topologies, 376 and 404 nodes**, mixed across the buckets (`w32_off_a` captured `404 404 376 376 404 404 404`). Every refusal is about TOPOLOGY, never a parameter, so a COARSER key produces MORE false hits rather than more folds — the opposite of what W5's 2-bucket A/B suggested, and W5's `refused=0` is now explained as an artefact of workloads whose buckets only ever SHRANK, so exactly one pair was ever presented. **COST:** W32 OFF 7 instantiates / 0 updates vs COARSE 10 (3 retained + 7 probes) / 11 updates; W64 OFF 11 / 0 vs COARSE **27** (5 retained + 22 probes) / 28 updates — **2.45x the instantiate calls** to retain 6 fewer executables. **Peak transient did NOT double** — in every ON cell live-bytes peak == end, because `Register` destroys the probe before returning, so the feared "double the peak to save the steady state" trade did not occur. **A replay-time re-point DID occur** — 4 and 6 non-probe updates over 88 and 244 replays, ARITHMETIC over two printed totals and not a counter — with every cell exiting 0 and byte-identical, so `Replay`'s transitivity assumption neither aborted nor changed a token; W5 recorded that case as untested. **CAVEATS THAT BOUND THIS RESULT:** the clock pin was **REFUSED inside the lease** (`The current user does not have permission to change clocks for GPU 0000000F:01:00.0`, `clocks_pinned=0`), so **NO time-based figure is attributable** and the instantiate-wall and update-wall figures in `bytes.log` are diagnostics quoted nowhere as a result; `result=2` is ONE driver, ONE GB10, ONE toolkit; only the Qwen3 dense decode driver was exercised, as in W4 and W5; `VT_ASYNC_RUNNER=0` throughout, so the feature is STILL unreachable on the DEFAULT serving path (#1179); and `cudaMemGetInfo` cannot separate an executable's own cost from the pool chunk that satisfied it. **VERDICT, DELIVERED AND NEGATIVE:** `VT_CUDA_GRAPH_DEDUP` stays default OFF, now on MEASUREMENT rather than on silence; `VT_CUDA_GRAPH_DEDUP_COARSE_KEY` alone is a **NO-OP, not merely unsupported** — `GraphDedupCoarseKeyEnabled()` (`src/vt/graph_dedup.h:114`) is read only by the signature builder (`src/vt/graph_dedup_runtime.h:177`), only from `Register`, only under `GraphDedupEnabled()` (`src/vt/cuda/cuda_backend.cu:237`), so with dedup off its sole observable is one stderr line; both on is unsupported. **NOT A CEILING.** Three things would change it and each is traceable: find where the 376/404 split comes from (the FA-2 split-KV grid is the first suspect — a capture that fixes the node set across buckets removes every refusal); an instrument that resolves a single 2-4 MiB executable against driver pool granularity (`cuMemGetAllocationGranularity` or a pool-statistics query); and the same measurement on a 60-80 layer checkpoint, where bytes scale with node count. **Row STAYS `ACTIVE`, argued, and the argument is now narrow.** The MEASUREMENT obligations are discharged and the DECISION is delivered, which is the `DONE` case and it is a real one. Three things stop the flip and none is a checker technicality: the feature is unreachable on the DEFAULT serving path, owned by `ENG-CUDAGRAPH-BREAK` (#1179) and the "nothing lands dead" half of this row; two items still sit under #1162 itself — the device-tier signature stability/discrimination tests and probing `group.current_raw` instead of `raws.front()` to retire the transitivity assumption; and the `DONE` record surface owes a `.agents/parity-ledger.md` entry, a closing-commit owner in place of the claim, an exact test anchor and the RELEASE of `CLAIM-ENG-CUDAGRAPH-DEDUP`, which is an operator act and which this record-only branch does not own. Not `PARTIAL` — nothing upstream is omitted. Not `BLOCKED` — nothing external stops the next step. Full evidence: [benchmark-record.md](benchmark-record.md) entry `ENG-CUDAGRAPH-DEDUP W6`, raw at `/mnt/nas_share/rc/dedup-bytes/` | [eng-cudagraph-dedup.md](specs/eng-cudagraph-dedup.md); analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-DEDUP` ([#1162](https://github.com/mudler/vllm.cpp/issues/1162)) | -| `ENG-CUDAGRAPH-BREAK` | One shared `vt` capture seam that accepts BREAK POINTS, so a forward containing a host-dependent op is still graphed instead of falling out entirely — and so the NINE hand-rolled drivers become one (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight). **Coverage AND CORRECTNESS row, not a throughput row** | T1 | mirror vLLM `CUDAGraphMode.PIECEWISE` splitting at `splitting_ops` (`vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); construction from SGLang BCG `python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/breakable_cuda_graph.py:204-243,246-274,309-333,335-367` @ `f63458b5be` (decorator + runtime stream capture, no compiler); its unit suite `test/registered/cuda_graph/breakable/test_breakable_cuda_graph.py:30,172,230` (305 lines, 11 unit cases) is mapped case for case in the spec's `## Tests to port` | **W6 MOVED THE PREDICATE** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374), 2026-08-19): `GPUModelRunner::execute_model` names the step's ACTUAL uniform query length once through `v1::GraphEligibleQueryLen` (`src/vllm/v1/worker/gpu/cudagraph_dispatch.h`, INERT with no caller since #442 and now called from production) and ships it on `ModelForwardInput::uniform_query_len`; the two Qwen3.5 registrations stop re-deriving that test in twenty duplicated lines each, and both key their slot ring on `(S, q, spec)`. [#1020](https://github.com/mudler/vllm.cpp/issues/1020) CLOSES on the pair, and the key half was a LIVE collision rather than the enabler #1020 called it: `S = spec_step ? B : PadToCaptureSize(B)` puts a 4-request spec step at 1+1 tokens and an 8-request padded decode on the same `S == 8` at the base commit. The widening is BOUNDED by `VT_SPEC_GRAPH_MAX_QLENS` (default 2), because reading the actual length multiplies the spec shape ceiling by `1 + k`. Seven of the nine drivers still read `pure_decode` and are byte-identical. **What did NOT move is 'except at the break points'**: no driver in this tree serves a prefill or a mixed batch under any predicate, so that needs a prefill capture driver nobody has written and whose benefit D5 already refutes on this hardware — a publishable negative, recorded in the spec's `## Owed` as a row-level item. The pre-W6 baseline it replaces: all-or-nothing, `src/vllm/v1/worker/gpu/runner.cpp:1338-1341` routing only `pure_decode`; drivers `qwen3_5.h:275`, `qwen3_5_dense.h:391`, `qwen3_moe.h:117`, `qwen3.h:243`, `deepseek_v2.h:324`, `voxtral.h:126`, plus `deepseek_v4.cpp`, `laguna.cpp` — and the spike found the NINTH already written, `src/vllm/model_executor/models/qwen3_dflash.cpp:771,1091`. The re-derivation is measured, not asserted: `StepDevInputs` (`src/vllm/model_executor/models/qwen3_5.cpp:3894`, the persistent DEVICE input path) exists in ONE driver and `grep -c` returns 0 in `qwen3_moe.cpp`, `qwen3.cpp`, `deepseek_v2.cpp` and `voxtral.cpp`, which is why `src/vllm/model_executor/models/qwen3.cpp`'s `DenseDecodeGraphForward` DECLINES the graph outright when the async device-token mirror is live. **That decline is why this is also a CORRECTNESS row** ([#1179](https://github.com/mudler/vllm.cpp/issues/1179)): a SHIPPED model has already lost its decode graph to the duplication, on the driver's own measurement (`depth-1, graph ON PASS 78/78`; `depth-2, graph OFF PASS 82/82`; `depth-2, graph ON FAIL, slots 1-3 degenerate`), and the fix its comment names is the sibling's `StepDevInputs`. The row still makes NO throughput claim: the prefill refutation on the `ENG-CUDAGRAPH` row (3.8% host idle, >96% GPU-busy, 92.5% glue) stands unchanged; **#1305 CLOSED, and reading the tree found a larger defect than the issue described** (2026-08-19): `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp` never constructed a `detail::DeviceTokenIdsScope` and neither `qwen3_moe.cpp`'s nor `deepseek_v2.cpp`'s `EmbedInto` ever consulted one, so `ModelForwardInput::device_token_ids` reached NOTHING in either translation unit — the decode graph AND both eager arms embedded the host vector the runner's mirror arm deliberately leaves stale for decode rows. The three registries now publish the scope (the mechanism `qwen3.cpp`, `qwen3_5.cpp`, `mistral_registry.cpp`, `internlm2_registry.cpp` and `llama_registry.cpp` already use), and each decode-graph size slot holds a `vllm::StepTokenIds` (`include/vllm/model_executor/models/step_token_ids.h`) whose destination is a device buffer with a stable address, refreshed through `vt::PersistentStepInput` — host arm for the padded vector, DEVICE arm over the real prefix, both on the main queue so the second is ordered after the combine rather than racing it. That is `vt::PersistentStepInput::RefreshFromDevice`'s FIRST production caller, retiring the staged slice W4 landed with none, and it is the fix `qwen3.cpp`'s own decline comment names rather than a fifth private copy. `qwen3.cpp`'s decline is UNTOUCHED: W4 measured its recorded cause false and its real one is unidentified. | owed: bit-exactness vs eager on every migrated model over MORE than one replay, on a real GPU — **W2 did NOT meet it and says so**: no `rc` lease was obtainable in its window and a CPU harness cannot replay a captured segment, so it moves to W3 with the three drivers of the same shape (G1); the host-lifetime contract of `decode-graph-scratch-uaf-2026-07-18.md` enforced AT the seam — D1's INPUT half, making the intermediates a segment reads unavailable to the `DevicePool` free list, which becomes live only for the first PIECEWISE production capture (W4); the auxiliary-stream auto-join before every segment close (`:353-361`, spec D10), live at `src/vllm/model_executor/models/qwen3_5.cpp:6254-6255,6384` and `src/vllm/model_executor/models/laguna.cpp:2572-2576,2612` (W4, W5). **Delivered by W1** ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the reachability mutation (performed; deleting the call site reds `tests/vllm/models/test_qwen3_break_point.cpp` and leaves the unit suite green); the ported SGLang unit cases with their arithmetic chains and post-replay assertions; and the break-function OUTPUT writeback (`replay_fn`/`_copy_output` `breakable_cuda_graph.py:231-235,172-201`, spec D9), whose destination is a `vt::BreakSlot` the seam owns rather than a caller reference it cannot outlive **W6 gates** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): G2 at THREE levels because the claim has three parts — the engine (`tests/vllm/v1/spec_decode/test_mtp_depth.cpp`, a real LoadedEngine/EngineCore/Scheduler/runner stack, asserting `clamped_spec_steps`, measured 0/0/1/2/4 at k=1/2/3/4/6), the driver (`tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp`, two spec shapes of equal S and different q getting two rings and two captures), and the arithmetic (`tests/vllm/v1/worker/gpu/test_cudagraph_dispatch.cpp`). Five detecting mutations, each reddening ONE level and leaving the others green, plus an over-fire control. A SIXTH mutation was NOT detected and forced a repair: the per-request verify conjunct is redundant on every model that reads the field (both are GDN hybrids whose prefill trips the first conjunct), so it moved into `GraphEligibleQueryLen` where a mutation reds 4 assertions, and the spec records it as unreached defence in depth. **G1 re-run on `thor:gpu0` (sm_110, driver 595.78, nvcc 13.0.88): 2066 assertions, 0 failed, 0 differing on all five migrated drivers — W6 moves no logit.** The ring key's own device case is BLOCKED by [#1380](https://github.com/mudler/vllm.cpp/issues/1380), a pre-existing `cudaMalloc` inside a capturing stream on the spec arm that W6 neither caused nor regressed; the case PINS that refusal and is written to fail when #1380 is fixed.; **#1305 (2026-08-19)**: `tests/vllm/models/test_moe_async_device_ids.cpp`, entered at `ModelRegistry::Forward` over a synthetic safetensors checkpoint for `Qwen3MoeForCausalLM` and `DeepseekV2ForCausalLM` — the production entry point, not the driver type. Three runs each: right host ids and no mirror as the reference, stale host ids and no mirror as the CONTROL that must differ, stale host ids with the truth reaching the model ONLY through `device_token_ids` as the gate. RED first at 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 at 59 of 59, exit 0. TWO mutations, each compiled clean and each restored by sha256: deleting the registry's scope line — the production call site — reds 6 assertions across both cases, and swapping the seam's DEVICE arm for its HOST arm leaves the logits BIT IDENTICAL (0 of 200 differing) and reds only `device_refreshes` and `host_refreshes`, which is the arm no token gate can see. Neighbours green on the same binary: `test_qwen3_moe_decode_graph_seam` 228 of 228, `test_deepseek_v2_decode_graph_seam` 230 of 230, `test_qwen3_decode_graph_seam` 231 of 231, `test_voxtral_decode_graph_seam` 230 of 230, `test_breakable_graph` 265 of 265, `test_persistent_step_input` 66 of 66, `test_model_registry` 924 of 924, `test_qwen3_moe_forward` 504 of 504, `test_deepseek_v2_forward` 1052 of 1052. **NOT measured:** the depth-2 four-concurrent battery on a device, which needs a GPU and a real checkpoint; owed. **Found red on `main` and NOT caused here:** `test_qwen3_5_decode_graph_seam` exits 139 while its assertion line reads 135 of 135 passed ([#1390](https://github.com/mudler/vllm.cpp/issues/1390)). | spec [eng-cudagraph-break.md](specs/eng-cudagraph-break.md) (W0 spike DONE 2026-08-18: the existing `vt` capture vocabulary `include/vt/backend.h:208-222` expresses a SEGMENTED capture with NO new virtual, because `EndCaptureGraph` stores nothing (`src/vt/cuda/cuda_backend.cu:225-232`); a break point is expressible with one `thread_local` capture pointer plus a free function, no compiler and no decorator); **W1 DONE 2026-08-18 ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the seam LANDS** — `vt::BreakableGraph`, `vt::GraphCaptureScope` and `vt::GraphBreak` (`include/vt/breakable_graph.h`, `src/vt/breakable_graph.cpp`), the SGLang unit suite ported case for case (`tests/vt/test_breakable_graph.cpp`, 24 cases / 163 assertions, re-derived 2026-08-18 by `ninja test_breakable_graph && ./build/tests/test_breakable_graph`; the recorded 14/81 never re-derived at any head of this branch), and ONE break point registered at the DENSE ATTENTION ENTRY of `Qwen3ForCausalLM` (`src/vllm/model_executor/models/qwen3.cpp`, inside `RunLayer`). **The exit criterion W0 deliberately left open is ANSWERED on a leased GPU:** `cudaStreamEndCapture` then `cudaStreamBeginCapture` on the SAME stream mid-forward with EAGER work between is LEGAL under `cudaStreamCaptureModeThreadLocal` (`src/vt/cuda/cuda_backend.cu:204-206`) — `orin:gpu0` via an `rc` lease, driver 12060, 3 replays with fresh inputs, 0 mismatches, bare zero-work re-begin legal too. G2 reachability is `tests/vllm/models/test_qwen3_break_point.cpp`, which drives the production `Qwen3DenseModel::Forward` with a scope open and counts `num_hidden_layers + 1` segments (mutation: delete the call site ⇒ 1 segment ⇒ RED), and holds G4 in the same case at 500 logits / 0 differing bit for bit. STAGED SLICE, named: the scope and the container are not yet ENTERED from a production step — no driver opens a scope until W2 migrates `Qwen3DenseDecodeGraph` — and the spec's `## Owed` lists it with W2 as owner, alongside the D10 auxiliary-stream auto-join (W4/W5), G5's ROCm/Tenstorrent arms (W3) and G1 on a real GPU (W2). **The capture-failure drain is NOT among them: it landed HERE**, as behaviour (`std::uncaught_exceptions()` compared against the depth recorded at scope entry, so a break function or ordinary model code throwing mid-capture destroys the partial container instead of handing back a forward that reports `captured() == true`) and as three gated arms (tests 13a, 13b, 13c). The spec's `## Owed` strikes the item through and reads DELIVERED in W1; this cell said the opposite until 2026-08-18 because `cba969857` re-derived field 6 alone. **W2 DONE 2026-08-18 ([#1261](https://github.com/mudler/vllm.cpp/issues/1261)): `Qwen3DenseDecodeGraph` MIGRATED and the seam is ENTERED from a production step**, which retires W1's staged slice. `Qwen3DenseDecodeGraph::Step` opens a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` and replays through `BreakableGraph::Replay`; the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle, the `bool captured` flag, the `DestroyGraph` loop and the driver's own `VLLM_CPP_CUDAGRAPH` read are gone (re-derivation items 1, 2, 5, 6). The migration ADDED `vt::GraphCaptureMode`, mirroring vLLM's `CUDAGraphMode` (`vllm/config/compilation.py:59-63`), whose v1 default `FULL_AND_PIECEWISE` (`:63`) is documented at `:630-632` as a FULL graph for DECODE batches and a piecewise one for prefill/mixed, with `decode_mode()` (`:65-66`) selecting the full half and the runtime reading it at `vllm/v1/worker/gpu/cudagraph_utils.py:185-186`. A decode driver opened `kPiecewise` would have turned a fully graphed decode step into ONE EAGER ATTENTION CALL PER LAYER between graph replays — not vLLM's decode behaviour, and invisible to every token gate here. `GraphBreak` in a `kFull` scope takes the pass-through arm and `AppendBreak` REFUSES a registration in that mode. G2 is `tests/vllm/models/test_qwen3_decode_graph_seam.cpp` (3 cases / 124 assertions), which asserts the SEAM's counters because a driver calling `Backend::ReplayGraph` directly leaves an identical backend log; the mutation restoring the pre-W2 raw pair (18 lines, compiled clean) left `test_breakable_graph` 27/27, `test_qwen3_break_point` 2/2 and `test_qwen3_forward` 10/10 GREEN and reddened only this file. G4 in the same file: capture step vs `Qwen3DenseModel::Forward`, 100 logits, 0 differing. **The async decline at `qwen3.cpp` STANDS and is now GATED in both arms**: migrating the capture does not move the INPUTS, so the depth-2 race is untouched, and the fix is `StepDevInputs` as a SEAM capability, which is W4. **G1 is NOT met by W2** and is recorded owed rather than implied. **W3 DONE 2026-08-19 ([#1291](https://github.com/mudler/vllm.cpp/issues/1291)): the three remaining PLAIN BATCHED drivers migrate — `Qwen3MoeDecodeGraph`, `VoxtralDecodeGraph`, `DeepseekV2DecodeGraph` — one commit each, each with its own RED-first G2 gate.** Four of the nine drivers are now on the seam, and the six batched-driver `VLLM_CPP_CUDAGRAPH` reads `## Our baseline` item 1 counted are down to TWO, both in `qwen3_5.cpp` (W4). Each gate asserts the SEAM's counters and not the backend log, because a driver that kept its raw pair produces identical logits, an identical backend log and an identical `replay_count()`; red-first on four assertions each (`test_qwen3_moe_decode_graph_seam` 222/226, `test_voxtral_decode_graph_seam` 224/228, `test_deepseek_v2_decode_graph_seam` 224/228, all exit 1), green 3/3 each after. The G2 mutation — restoring each pre-W3 driver file, 25/102, 23/92 and 25/94 lines, each compiled clean — reddens ONLY its own gate and leaves `test_breakable_graph` 216/216 and W2's `test_qwen3_decode_graph_seam` 231/231 green. The gate harness is now SHARED (`tests/vllm/models/decode_graph_seam_harness.h`); three more copies inside `tests/` would have reproduced the duplication this row removes from `src/`. **G1 IS DELIVERED and is no longer owed** — the item W1 and W2 both carried. `tests/vllm/models/test_decode_graph_seam_g1_cuda.cpp` runs each driver COLD, CAPTURE and THREE consecutive replays against its own eager arm (selected by `max_num_reqs == 0`, so both arms are one binary on one device rather than two builds, each with its OWN device KV cache) on `thor:gpu0` through an `rc` lease — NVIDIA Thor sm_110, driver 595.78, nvcc 13.0.88, source `c905bb536`, 32 `.cu.o` objects, binary resolving `libcudart.so.13`/`libcublasLt.so.13`: **3 cases, 1600 assertions, exit 0, `5 steps x 100 logits, 0 differing, 4 replays` per driver.** The COUNT carries that claim, not the status line: with no CUDA backend the same file prints `SUCCESS!` over `assertions: 0`. Bounded honestly — synthetic tiny models rather than a checkpoint, and W2's driver shares the seam by argument rather than by measurement. **W3 also found a gate that could not fail.** The three gates' `breaks_registered == 0` mode guard is a TAUTOLOGY for any model with no registered break point, and the one production `vt::GraphBreak` in the tree is W1's in `qwen3.cpp`: flipping `kFull` to `kPiecewise` in `qwen3_moe.cpp`, one token, compiled clean and left that gate GREEN at 226/226. The mode was UNOBSERVABLE from outside a driver, so `vt::GraphBreakStats` gains `full_scopes`/`piecewise_scopes`, counted in `GraphCaptureScope`'s constructor on the ACTIVE path only, with an inert-scope control; the same flip now reds all three gates on exactly those two assertions. **NO break point is registered in these three models, deliberately**: under `kFull` it would be pass-through machinery no gate can exercise, and the break-point set is what the PIECEWISE arm needs (W4/W6). **The async decline, per driver:** Voxtral needs none (its only construction site is `VoxtralGenerateGreedy`, unreachable from the runner); Qwen3-Coder and DeepSeek carry a NEW FINDING instead — `qwen3_moe_registry.cpp:107`, `deepseek_v2_registry.cpp:106` and `glm4_moe_lite_registry.cpp:125` route an async step into a host-vector replay with no `device_token_ids` check at all, filed [#1305](https://github.com/mudler/vllm.cpp/issues/1305) with W4 as owner rather than mitigated on a measurement W3 cannot make. G5's ROCm/Tenstorrent arm is NOT discharged and moves to W5: the fleet carries no such device, so it is blocked on hardware rather than unattempted. **W4 DONE 2026-08-19 ([#1307](https://github.com/mudler/vllm.cpp/issues/1307)): the persistent device input path becomes a SEAM CAPABILITY, and the two Qwen3.5 drivers migrate.** `vt::PersistentStepInput` (`include/vt/persistent_step_input.h`, `src/vt/persistent_step_input.cpp`) binds a capture-stable device destination the DRIVER owns together with its pinned host staging block, and refreshes it in place from a host source or a DEVICE one; it owns the address-stability rule as a REFUSAL, the staging block, and the refreshing ARM as an observable (`last_source()`, `vt::StepInputStats`), and deliberately NOT the device allocation, because `Qwen3_5DecodeGraph` draws its retained inputs from a DEDICATED `DevicePool` so they never pop a block the captured forward's scratch then needs (D3). RED-first against a stub with the declared API and no guarantees: `tests/vt/test_persistent_step_input.cpp` 9 cases / 0 passed / 59 assertions / 32 failed / exit 1, GREEN after at 9/9 and 59/59; three mutations (delete the capacity refusal, make a null device source a silent no-op, collapse the host arm out of staging) each compiled clean and each reds exactly one case. `Qwen3_5DecodeGraph` and `Qwen3_5DenseDecodeGraph` open a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` in `kFull` and replay through it, and their `PinnedStepInputs`/`StageStepInputs` staging now runs THROUGH the capability, which is what makes it reachable rather than a class with a unit test. **Six of the nine drivers are on the seam** and `grep -rn 'std::getenv("VLLM_CPP_CUDAGRAPH")' src/` returns exactly ONE line, `src/vt/breakable_graph.cpp:61` — one switch, at last. Gate `tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp` RED-first on the MoE driver's five seam assertions (3 cases / 62 assertions / 5 failed / exit 1) and GREEN after at 7/7 and 129, G4 reading `40 values, 0 differing` per driver; G2 mutations: the whole pre-W4 file restored reds BOTH drivers (296 lines, 10 assertions), the MoE replay bypassing the container reds ONLY the MoE case (7 lines), the MoE `kFull`->`kPiecewise` flip reds ONLY its mode counters (3 lines), and deleting the `StageStepInputs` call site reds ONLY the reachability case while `test_persistent_step_input` stays 59/59 green — the difference between a class that works and a capability something reaches. **W4 FALSIFIED THIS ROW'S OWN PREMISE, which is its most important result.** This record and the spec both said the fix `qwen3.cpp`'s `DenseDecodeGraphForward`'s decline names already existed as `StepDevInputs`. It does not: `StepDevInputs` has NO token-id member, and its pinned sibling `PinnedStepInputs::token_ids` was allocated at capture, filled every step, zeroed by the poison hook, and NEVER uploaded or read — the embed runs OUTSIDE the captured region from the HOST vector in every batched driver, so **the decode graph carries no token ids to the device in ANY driver**. The dead block is removed. Consequently the DECLINE STANDS and [#1305](https://github.com/mudler/vllm.cpp/issues/1305) STAYS OPEN: W4 also read the decline's recorded cause against the tree at its own parent and found it falsified (the `DeviceTokenIdsScope` WAS live on the graph path, consumed by `EmbedInto` on all three arms at `qwen3.cpp:610,621,644 @ 338cbbfd1^`), so the measured failure is real and its mechanism is unidentified — not a state from which a refactor may retire a mitigation. The async battery was NOT run and W4 says so plainly: it needs `dgx` WITH the Qwen3-0.6B/4B checkpoints, `dgx:gpu0` was held by another session for W4's whole window, and W4's lease was `thor:gpu0`. Still NO throughput claim. W5 DONE 2026-08-19 ([#1335](https://github.com/mudler/vllm.cpp/issues/1335)): the THREE SINGLE-SHAPE drivers migrate — the DFlash draft graph, the DeepSeek V4 decode graph and the Laguna decode graph, whose own note at `laguna.cpp:2116-2119` asked for this seam by name and named V4's as the sibling that moves with it. **NINE OF NINE DRIVERS ARE 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 three per-model rollback switches stay (each an A/B lever for one driver); `VLLM_CPP_CUDAGRAPH` reaches all three for the first time. **D10, the auxiliary-stream fork/join, is DISCHARGED and REACHED** — `GraphCaptureScope` owns the outstanding-fork set and joins it before `EndCaptureGraph` (port of `breakable_cuda_graph.py:353-361` plus the `wait_stream` hook `:101-153`), registered by `vt::GraphNoteFork`/`GraphNoteJoin` from `laguna.cpp:2572-2576,2612`, the only fork inside a captured region by construction. Every prior stage opened `kFull`, which has ONE segment and so no between-segments window, so the rule could not be exercised before W5 and untested machinery was not landed for it. Gated as a COUNTER and an ORDER out of one backend trace, five arms including the control where the model joins first, and two mutations (deleting the join reds only the new case on 5 assertions; making it over-fire reds it on 8). DFlash is the ONE single-shape driver gateable without a GPU, because its admission predicate names neither a device type nor a kernel registry: `test_qwen3_dflash_decode_graph_seam.cpp` RED-first 3 cases/0 passed/16 assertions/7 failed exit 1, GREEN after 3/18, and the G2 mutation reds ONLY that file while seven other suites — the driver's own `test_dflash_propose` included — stay green. **G1 RE-RUN at W5's head on `thor:gpu0`** (sm_110, driver 595.78, nvcc 13.0.88, 32 `.cu.o`, source `79dc6b5bd`) because D10 put a join on the path of EVERY segment close, so the seam changed underneath the five measured drivers: `test_decode_graph_seam_g1_cuda` 5 cases / 2066 assertions / 0 failed, each reading `0 differing, 4 replays`, plus `test_breakable_graph` 265 on the same device. **And the one thing a green build could NOT have told us was measured separately**: Laguna's capture class sits behind `#ifdef VT_MARLIN_NVFP4`, so a passing build is the SAME OBSERVATION as one that compiled the region out. `-DVT_MARLIN_NVFP4=1` is on `laguna.cpp`'s own compile command, and an undeclared identifier injected immediately after its `GraphCaptureScope` line FAILED the object build under `-Werror` (`laguna.cpp:2735`) against an rc-0 baseline, restoring to an empty diff; the identical mutation on V4 failed at `deepseek_v4.cpp:1921`. Both migrated regions are COMPILED, which retires the could-not-even-be-built half. **G1 for all three and G2 for V4 and Laguna are OWED on hardware**, per driver and per reason: V4's `CanRunResidentDecode` refuses `kCPU` and needs the four CUDA-registered kernel families, Laguna's capture class exists only under `VT_MARLIN_NVFP4`. G5's ROCm/Tenstorrent arm stays BLOCKED — the fleet is all NVIDIA — and its owner moves from W5 to the ROW. Still NO throughput claim; analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) W6 DONE 2026-08-19 ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): the eligibility predicate, #1020, and the negative result on the piecewise arm. | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-BREAK-W6`; [#1163](https://github.com/mudler/vllm.cpp/issues/1163), [#1192](https://github.com/mudler/vllm.cpp/issues/1192), [#1261](https://github.com/mudler/vllm.cpp/issues/1261), [#1291](https://github.com/mudler/vllm.cpp/issues/1291), [#1307](https://github.com/mudler/vllm.cpp/issues/1307), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1020](https://github.com/mudler/vllm.cpp/issues/1020), [#1335](https://github.com/mudler/vllm.cpp/issues/1335), [#1374](https://github.com/mudler/vllm.cpp/issues/1374), [#1380](https://github.com/mudler/vllm.cpp/issues/1380), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1390](https://github.com/mudler/vllm.cpp/issues/1390) | +| `ENG-CUDAGRAPH-BREAK` | One shared `vt` capture seam that accepts BREAK POINTS, so a forward containing a host-dependent op is still graphed instead of falling out entirely — and so the NINE hand-rolled drivers become one (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight). **Coverage AND CORRECTNESS row, not a throughput row** | T1 | mirror vLLM `CUDAGraphMode.PIECEWISE` splitting at `splitting_ops` (`vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); construction from SGLang BCG `python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/breakable_cuda_graph.py:204-243,246-274,309-333,335-367` @ `f63458b5be` (decorator + runtime stream capture, no compiler); its unit suite `test/registered/cuda_graph/breakable/test_breakable_cuda_graph.py:30,172,230` (305 lines, 11 unit cases) is mapped case for case in the spec's `## Tests to port` | **W6 MOVED THE PREDICATE** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374), 2026-08-19): `GPUModelRunner::execute_model` names the step's ACTUAL uniform query length once through `v1::GraphEligibleQueryLen` (`src/vllm/v1/worker/gpu/cudagraph_dispatch.h`, INERT with no caller since #442 and now called from production) and ships it on `ModelForwardInput::uniform_query_len`; the two Qwen3.5 registrations stop re-deriving that test in twenty duplicated lines each, and both key their slot ring on `(S, q, spec)`. [#1020](https://github.com/mudler/vllm.cpp/issues/1020) CLOSES on the pair, and the key half was a LIVE collision rather than the enabler #1020 called it: `S = spec_step ? B : PadToCaptureSize(B)` puts a 4-request spec step at 1+1 tokens and an 8-request padded decode on the same `S == 8` at the base commit. The widening is BOUNDED by `VT_SPEC_GRAPH_MAX_QLENS` (default 2), because reading the actual length multiplies the spec shape ceiling by `1 + k`. Seven of the nine drivers still read `pure_decode` and are byte-identical. **What did NOT move is 'except at the break points'**: no driver in this tree serves a prefill or a mixed batch under any predicate, so that needs a prefill capture driver nobody has written and whose benefit D5 already refutes on this hardware — a publishable negative, recorded in the spec's `## Owed` as a row-level item. The pre-W6 baseline it replaces: all-or-nothing, `src/vllm/v1/worker/gpu/runner.cpp:1338-1341` routing only `pure_decode`; drivers `qwen3_5.h:275`, `qwen3_5_dense.h:391`, `qwen3_moe.h:117`, `qwen3.h:243`, `deepseek_v2.h:324`, `voxtral.h:126`, plus `deepseek_v4.cpp`, `laguna.cpp` — and the spike found the NINTH already written, `src/vllm/model_executor/models/qwen3_dflash.cpp:771,1091`. The re-derivation is measured, not asserted: `StepDevInputs` (`src/vllm/model_executor/models/qwen3_5.cpp:3894`, the persistent DEVICE input path) exists in ONE driver and `grep -c` returns 0 in `qwen3_moe.cpp`, `qwen3.cpp`, `deepseek_v2.cpp` and `voxtral.cpp`, which is why `src/vllm/model_executor/models/qwen3.cpp`'s `DenseDecodeGraphForward` DECLINES the graph outright when the async device-token mirror is live. **That decline is why this is also a CORRECTNESS row** ([#1179](https://github.com/mudler/vllm.cpp/issues/1179)): a SHIPPED model has already lost its decode graph to the duplication, on the driver's own measurement (`depth-1, graph ON PASS 78/78`; `depth-2, graph OFF PASS 82/82`; `depth-2, graph ON FAIL, slots 1-3 degenerate`), and the fix its comment names is the sibling's `StepDevInputs`. The row still makes NO throughput claim: the prefill refutation on the `ENG-CUDAGRAPH` row (3.8% host idle, >96% GPU-busy, 92.5% glue) stands unchanged; **#1305 CLOSED, and reading the tree found a larger defect than the issue described** (2026-08-19): `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp` never constructed a `detail::DeviceTokenIdsScope` and neither `qwen3_moe.cpp`'s nor `deepseek_v2.cpp`'s `EmbedInto` ever consulted one, so `ModelForwardInput::device_token_ids` reached NOTHING in either translation unit — the decode graph AND both eager arms embedded the host vector the runner's mirror arm deliberately leaves stale for decode rows. The three registries now publish the scope (the mechanism `qwen3.cpp`, `qwen3_5.cpp`, `mistral_registry.cpp`, `internlm2_registry.cpp` and `llama_registry.cpp` already use), and each decode-graph size slot holds a `vllm::StepTokenIds` (`include/vllm/model_executor/models/step_token_ids.h`) whose destination is a device buffer with a stable address, refreshed through `vt::PersistentStepInput` — host arm for the padded vector, DEVICE arm over the real prefix, both on the main queue so the second is ordered after the combine rather than racing it. That is `vt::PersistentStepInput::RefreshFromDevice`'s FIRST production caller, retiring the staged slice W4 landed with none, and it is the fix `qwen3.cpp`'s own decline comment names rather than a fifth private copy. `qwen3.cpp`'s decline is UNTOUCHED: W4 measured its recorded cause false and its real one is unidentified. | owed: bit-exactness vs eager on every migrated model over MORE than one replay, on a real GPU — **W2 did NOT meet it and says so**: no `rc` lease was obtainable in its window and a CPU harness cannot replay a captured segment, so it moves to W3 with the three drivers of the same shape (G1); the host-lifetime contract of `decode-graph-scratch-uaf-2026-07-18.md` enforced AT the seam — D1's INPUT half, making the intermediates a segment reads unavailable to the `DevicePool` free list, which becomes live only for the first PIECEWISE production capture (W4); the auxiliary-stream auto-join before every segment close (`:353-361`, spec D10), live at `src/vllm/model_executor/models/qwen3_5.cpp:6254-6255,6384` and `src/vllm/model_executor/models/laguna.cpp:2572-2576,2612` (W4, W5). **Delivered by W1** ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the reachability mutation (performed; deleting the call site reds `tests/vllm/models/test_qwen3_break_point.cpp` and leaves the unit suite green); the ported SGLang unit cases with their arithmetic chains and post-replay assertions; and the break-function OUTPUT writeback (`replay_fn`/`_copy_output` `breakable_cuda_graph.py:231-235,172-201`, spec D9), whose destination is a `vt::BreakSlot` the seam owns rather than a caller reference it cannot outlive **W6 gates** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): G2 at THREE levels because the claim has three parts — the engine (`tests/vllm/v1/spec_decode/test_mtp_depth.cpp`, a real LoadedEngine/EngineCore/Scheduler/runner stack, asserting `clamped_spec_steps`, measured 0/0/1/2/4 at k=1/2/3/4/6), the driver (`tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp`, two spec shapes of equal S and different q getting two rings and two captures), and the arithmetic (`tests/vllm/v1/worker/gpu/test_cudagraph_dispatch.cpp`). Five detecting mutations, each reddening ONE level and leaving the others green, plus an over-fire control. A SIXTH mutation was NOT detected and forced a repair: the per-request verify conjunct is redundant on every model that reads the field (both are GDN hybrids whose prefill trips the first conjunct), so it moved into `GraphEligibleQueryLen` where a mutation reds 4 assertions, and the spec records it as unreached defence in depth. **G1 re-run on `thor:gpu0` (sm_110, driver 595.78, nvcc 13.0.88): 2066 assertions, 0 failed, 0 differing on all five migrated drivers — W6 moves no logit.** The ring key's own device case is BLOCKED by [#1380](https://github.com/mudler/vllm.cpp/issues/1380), a pre-existing `cudaMalloc` inside a capturing stream on the spec arm that W6 neither caused nor regressed; the case PINS that refusal and is written to fail when #1380 is fixed.; **#1305 (2026-08-19)**: `tests/vllm/models/test_moe_async_device_ids.cpp`, entered at `ModelRegistry::Forward` over a synthetic safetensors checkpoint for `Qwen3MoeForCausalLM` and `DeepseekV2ForCausalLM` — the production entry point, not the driver type. Three runs each: right host ids and no mirror as the reference, stale host ids and no mirror as the CONTROL that must differ, stale host ids with the truth reaching the model ONLY through `device_token_ids` as the gate. RED first at 2 cases / 65 assertions / 10 failed / exit 1, with 800 of 800 logit values differing over four steps on both architectures and every counter at 0; GREEN after at 65 of 65, exit 0. TWO mutations, each compiled clean and each restored by sha256: deleting the registry's scope line — the production call site — reds 4 assertions across both cases and puts all 800 values back, and swapping the seam's DEVICE arm for its HOST arm leaves the logits BIT IDENTICAL at 0 of 800 differing and reds only `device_refreshes` and `host_refreshes`, which is the arm no token gate can see. Neighbours green on the same binary: `test_qwen3_moe_decode_graph_seam` 228 of 228, `test_deepseek_v2_decode_graph_seam` 230 of 230, `test_qwen3_decode_graph_seam` 231 of 231, `test_voxtral_decode_graph_seam` 230 of 230, `test_breakable_graph` 265 of 265, `test_persistent_step_input` 66 of 66, `test_model_registry` 924 of 924, `test_qwen3_moe_forward` 504 of 504, `test_deepseek_v2_forward` 1052 of 1052. **NOT measured:** the depth-2 four-concurrent battery on a device, which needs a GPU and a real checkpoint; owed. **Found red on `main` and NOT caused here:** `test_qwen3_5_decode_graph_seam` exits 139 while its assertion line reads 135 of 135 passed ([#1390](https://github.com/mudler/vllm.cpp/issues/1390)). | spec [eng-cudagraph-break.md](specs/eng-cudagraph-break.md) (W0 spike DONE 2026-08-18: the existing `vt` capture vocabulary `include/vt/backend.h:208-222` expresses a SEGMENTED capture with NO new virtual, because `EndCaptureGraph` stores nothing (`src/vt/cuda/cuda_backend.cu:225-232`); a break point is expressible with one `thread_local` capture pointer plus a free function, no compiler and no decorator); **W1 DONE 2026-08-18 ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the seam LANDS** — `vt::BreakableGraph`, `vt::GraphCaptureScope` and `vt::GraphBreak` (`include/vt/breakable_graph.h`, `src/vt/breakable_graph.cpp`), the SGLang unit suite ported case for case (`tests/vt/test_breakable_graph.cpp`, 24 cases / 163 assertions, re-derived 2026-08-18 by `ninja test_breakable_graph && ./build/tests/test_breakable_graph`; the recorded 14/81 never re-derived at any head of this branch), and ONE break point registered at the DENSE ATTENTION ENTRY of `Qwen3ForCausalLM` (`src/vllm/model_executor/models/qwen3.cpp`, inside `RunLayer`). **The exit criterion W0 deliberately left open is ANSWERED on a leased GPU:** `cudaStreamEndCapture` then `cudaStreamBeginCapture` on the SAME stream mid-forward with EAGER work between is LEGAL under `cudaStreamCaptureModeThreadLocal` (`src/vt/cuda/cuda_backend.cu:204-206`) — `orin:gpu0` via an `rc` lease, driver 12060, 3 replays with fresh inputs, 0 mismatches, bare zero-work re-begin legal too. G2 reachability is `tests/vllm/models/test_qwen3_break_point.cpp`, which drives the production `Qwen3DenseModel::Forward` with a scope open and counts `num_hidden_layers + 1` segments (mutation: delete the call site ⇒ 1 segment ⇒ RED), and holds G4 in the same case at 500 logits / 0 differing bit for bit. STAGED SLICE, named: the scope and the container are not yet ENTERED from a production step — no driver opens a scope until W2 migrates `Qwen3DenseDecodeGraph` — and the spec's `## Owed` lists it with W2 as owner, alongside the D10 auxiliary-stream auto-join (W4/W5), G5's ROCm/Tenstorrent arms (W3) and G1 on a real GPU (W2). **The capture-failure drain is NOT among them: it landed HERE**, as behaviour (`std::uncaught_exceptions()` compared against the depth recorded at scope entry, so a break function or ordinary model code throwing mid-capture destroys the partial container instead of handing back a forward that reports `captured() == true`) and as three gated arms (tests 13a, 13b, 13c). The spec's `## Owed` strikes the item through and reads DELIVERED in W1; this cell said the opposite until 2026-08-18 because `cba969857` re-derived field 6 alone. **W2 DONE 2026-08-18 ([#1261](https://github.com/mudler/vllm.cpp/issues/1261)): `Qwen3DenseDecodeGraph` MIGRATED and the seam is ENTERED from a production step**, which retires W1's staged slice. `Qwen3DenseDecodeGraph::Step` opens a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` and replays through `BreakableGraph::Replay`; the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle, the `bool captured` flag, the `DestroyGraph` loop and the driver's own `VLLM_CPP_CUDAGRAPH` read are gone (re-derivation items 1, 2, 5, 6). The migration ADDED `vt::GraphCaptureMode`, mirroring vLLM's `CUDAGraphMode` (`vllm/config/compilation.py:59-63`), whose v1 default `FULL_AND_PIECEWISE` (`:63`) is documented at `:630-632` as a FULL graph for DECODE batches and a piecewise one for prefill/mixed, with `decode_mode()` (`:65-66`) selecting the full half and the runtime reading it at `vllm/v1/worker/gpu/cudagraph_utils.py:185-186`. A decode driver opened `kPiecewise` would have turned a fully graphed decode step into ONE EAGER ATTENTION CALL PER LAYER between graph replays — not vLLM's decode behaviour, and invisible to every token gate here. `GraphBreak` in a `kFull` scope takes the pass-through arm and `AppendBreak` REFUSES a registration in that mode. G2 is `tests/vllm/models/test_qwen3_decode_graph_seam.cpp` (3 cases / 124 assertions), which asserts the SEAM's counters because a driver calling `Backend::ReplayGraph` directly leaves an identical backend log; the mutation restoring the pre-W2 raw pair (18 lines, compiled clean) left `test_breakable_graph` 27/27, `test_qwen3_break_point` 2/2 and `test_qwen3_forward` 10/10 GREEN and reddened only this file. G4 in the same file: capture step vs `Qwen3DenseModel::Forward`, 100 logits, 0 differing. **The async decline at `qwen3.cpp` STANDS and is now GATED in both arms**: migrating the capture does not move the INPUTS, so the depth-2 race is untouched, and the fix is `StepDevInputs` as a SEAM capability, which is W4. **G1 is NOT met by W2** and is recorded owed rather than implied. **W3 DONE 2026-08-19 ([#1291](https://github.com/mudler/vllm.cpp/issues/1291)): the three remaining PLAIN BATCHED drivers migrate — `Qwen3MoeDecodeGraph`, `VoxtralDecodeGraph`, `DeepseekV2DecodeGraph` — one commit each, each with its own RED-first G2 gate.** Four of the nine drivers are now on the seam, and the six batched-driver `VLLM_CPP_CUDAGRAPH` reads `## Our baseline` item 1 counted are down to TWO, both in `qwen3_5.cpp` (W4). Each gate asserts the SEAM's counters and not the backend log, because a driver that kept its raw pair produces identical logits, an identical backend log and an identical `replay_count()`; red-first on four assertions each (`test_qwen3_moe_decode_graph_seam` 222/226, `test_voxtral_decode_graph_seam` 224/228, `test_deepseek_v2_decode_graph_seam` 224/228, all exit 1), green 3/3 each after. The G2 mutation — restoring each pre-W3 driver file, 25/102, 23/92 and 25/94 lines, each compiled clean — reddens ONLY its own gate and leaves `test_breakable_graph` 216/216 and W2's `test_qwen3_decode_graph_seam` 231/231 green. The gate harness is now SHARED (`tests/vllm/models/decode_graph_seam_harness.h`); three more copies inside `tests/` would have reproduced the duplication this row removes from `src/`. **G1 IS DELIVERED and is no longer owed** — the item W1 and W2 both carried. `tests/vllm/models/test_decode_graph_seam_g1_cuda.cpp` runs each driver COLD, CAPTURE and THREE consecutive replays against its own eager arm (selected by `max_num_reqs == 0`, so both arms are one binary on one device rather than two builds, each with its OWN device KV cache) on `thor:gpu0` through an `rc` lease — NVIDIA Thor sm_110, driver 595.78, nvcc 13.0.88, source `c905bb536`, 32 `.cu.o` objects, binary resolving `libcudart.so.13`/`libcublasLt.so.13`: **3 cases, 1600 assertions, exit 0, `5 steps x 100 logits, 0 differing, 4 replays` per driver.** The COUNT carries that claim, not the status line: with no CUDA backend the same file prints `SUCCESS!` over `assertions: 0`. Bounded honestly — synthetic tiny models rather than a checkpoint, and W2's driver shares the seam by argument rather than by measurement. **W3 also found a gate that could not fail.** The three gates' `breaks_registered == 0` mode guard is a TAUTOLOGY for any model with no registered break point, and the one production `vt::GraphBreak` in the tree is W1's in `qwen3.cpp`: flipping `kFull` to `kPiecewise` in `qwen3_moe.cpp`, one token, compiled clean and left that gate GREEN at 226/226. The mode was UNOBSERVABLE from outside a driver, so `vt::GraphBreakStats` gains `full_scopes`/`piecewise_scopes`, counted in `GraphCaptureScope`'s constructor on the ACTIVE path only, with an inert-scope control; the same flip now reds all three gates on exactly those two assertions. **NO break point is registered in these three models, deliberately**: under `kFull` it would be pass-through machinery no gate can exercise, and the break-point set is what the PIECEWISE arm needs (W4/W6). **The async decline, per driver:** Voxtral needs none (its only construction site is `VoxtralGenerateGreedy`, unreachable from the runner); Qwen3-Coder and DeepSeek carry a NEW FINDING instead — `qwen3_moe_registry.cpp:107`, `deepseek_v2_registry.cpp:106` and `glm4_moe_lite_registry.cpp:125` route an async step into a host-vector replay with no `device_token_ids` check at all, filed [#1305](https://github.com/mudler/vllm.cpp/issues/1305) with W4 as owner rather than mitigated on a measurement W3 cannot make. G5's ROCm/Tenstorrent arm is NOT discharged and moves to W5: the fleet carries no such device, so it is blocked on hardware rather than unattempted. **W4 DONE 2026-08-19 ([#1307](https://github.com/mudler/vllm.cpp/issues/1307)): the persistent device input path becomes a SEAM CAPABILITY, and the two Qwen3.5 drivers migrate.** `vt::PersistentStepInput` (`include/vt/persistent_step_input.h`, `src/vt/persistent_step_input.cpp`) binds a capture-stable device destination the DRIVER owns together with its pinned host staging block, and refreshes it in place from a host source or a DEVICE one; it owns the address-stability rule as a REFUSAL, the staging block, and the refreshing ARM as an observable (`last_source()`, `vt::StepInputStats`), and deliberately NOT the device allocation, because `Qwen3_5DecodeGraph` draws its retained inputs from a DEDICATED `DevicePool` so they never pop a block the captured forward's scratch then needs (D3). RED-first against a stub with the declared API and no guarantees: `tests/vt/test_persistent_step_input.cpp` 9 cases / 0 passed / 59 assertions / 32 failed / exit 1, GREEN after at 9/9 and 59/59; three mutations (delete the capacity refusal, make a null device source a silent no-op, collapse the host arm out of staging) each compiled clean and each reds exactly one case. `Qwen3_5DecodeGraph` and `Qwen3_5DenseDecodeGraph` open a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` in `kFull` and replay through it, and their `PinnedStepInputs`/`StageStepInputs` staging now runs THROUGH the capability, which is what makes it reachable rather than a class with a unit test. **Six of the nine drivers are on the seam** and `grep -rn 'std::getenv("VLLM_CPP_CUDAGRAPH")' src/` returns exactly ONE line, `src/vt/breakable_graph.cpp:61` — one switch, at last. Gate `tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp` RED-first on the MoE driver's five seam assertions (3 cases / 62 assertions / 5 failed / exit 1) and GREEN after at 7/7 and 129, G4 reading `40 values, 0 differing` per driver; G2 mutations: the whole pre-W4 file restored reds BOTH drivers (296 lines, 10 assertions), the MoE replay bypassing the container reds ONLY the MoE case (7 lines), the MoE `kFull`->`kPiecewise` flip reds ONLY its mode counters (3 lines), and deleting the `StageStepInputs` call site reds ONLY the reachability case while `test_persistent_step_input` stays 59/59 green — the difference between a class that works and a capability something reaches. **W4 FALSIFIED THIS ROW'S OWN PREMISE, which is its most important result.** This record and the spec both said the fix `qwen3.cpp`'s `DenseDecodeGraphForward`'s decline names already existed as `StepDevInputs`. It does not: `StepDevInputs` has NO token-id member, and its pinned sibling `PinnedStepInputs::token_ids` was allocated at capture, filled every step, zeroed by the poison hook, and NEVER uploaded or read — the embed runs OUTSIDE the captured region from the HOST vector in every batched driver, so **the decode graph carries no token ids to the device in ANY driver**. The dead block is removed. Consequently the DECLINE STANDS and [#1305](https://github.com/mudler/vllm.cpp/issues/1305) STAYS OPEN: W4 also read the decline's recorded cause against the tree at its own parent and found it falsified (the `DeviceTokenIdsScope` WAS live on the graph path, consumed by `EmbedInto` on all three arms at `qwen3.cpp:610,621,644 @ 338cbbfd1^`), so the measured failure is real and its mechanism is unidentified — not a state from which a refactor may retire a mitigation. The async battery was NOT run and W4 says so plainly: it needs `dgx` WITH the Qwen3-0.6B/4B checkpoints, `dgx:gpu0` was held by another session for W4's whole window, and W4's lease was `thor:gpu0`. Still NO throughput claim. W5 DONE 2026-08-19 ([#1335](https://github.com/mudler/vllm.cpp/issues/1335)): the THREE SINGLE-SHAPE drivers migrate — the DFlash draft graph, the DeepSeek V4 decode graph and the Laguna decode graph, whose own note at `laguna.cpp:2116-2119` asked for this seam by name and named V4's as the sibling that moves with it. **NINE OF NINE DRIVERS ARE 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 three per-model rollback switches stay (each an A/B lever for one driver); `VLLM_CPP_CUDAGRAPH` reaches all three for the first time. **D10, the auxiliary-stream fork/join, is DISCHARGED and REACHED** — `GraphCaptureScope` owns the outstanding-fork set and joins it before `EndCaptureGraph` (port of `breakable_cuda_graph.py:353-361` plus the `wait_stream` hook `:101-153`), registered by `vt::GraphNoteFork`/`GraphNoteJoin` from `laguna.cpp:2572-2576,2612`, the only fork inside a captured region by construction. Every prior stage opened `kFull`, which has ONE segment and so no between-segments window, so the rule could not be exercised before W5 and untested machinery was not landed for it. Gated as a COUNTER and an ORDER out of one backend trace, five arms including the control where the model joins first, and two mutations (deleting the join reds only the new case on 5 assertions; making it over-fire reds it on 8). DFlash is the ONE single-shape driver gateable without a GPU, because its admission predicate names neither a device type nor a kernel registry: `test_qwen3_dflash_decode_graph_seam.cpp` RED-first 3 cases/0 passed/16 assertions/7 failed exit 1, GREEN after 3/18, and the G2 mutation reds ONLY that file while seven other suites — the driver's own `test_dflash_propose` included — stay green. **G1 RE-RUN at W5's head on `thor:gpu0`** (sm_110, driver 595.78, nvcc 13.0.88, 32 `.cu.o`, source `79dc6b5bd`) because D10 put a join on the path of EVERY segment close, so the seam changed underneath the five measured drivers: `test_decode_graph_seam_g1_cuda` 5 cases / 2066 assertions / 0 failed, each reading `0 differing, 4 replays`, plus `test_breakable_graph` 265 on the same device. **And the one thing a green build could NOT have told us was measured separately**: Laguna's capture class sits behind `#ifdef VT_MARLIN_NVFP4`, so a passing build is the SAME OBSERVATION as one that compiled the region out. `-DVT_MARLIN_NVFP4=1` is on `laguna.cpp`'s own compile command, and an undeclared identifier injected immediately after its `GraphCaptureScope` line FAILED the object build under `-Werror` (`laguna.cpp:2735`) against an rc-0 baseline, restoring to an empty diff; the identical mutation on V4 failed at `deepseek_v4.cpp:1921`. Both migrated regions are COMPILED, which retires the could-not-even-be-built half. **G1 for all three and G2 for V4 and Laguna are OWED on hardware**, per driver and per reason: V4's `CanRunResidentDecode` refuses `kCPU` and needs the four CUDA-registered kernel families, Laguna's capture class exists only under `VT_MARLIN_NVFP4`. G5's ROCm/Tenstorrent arm stays BLOCKED — the fleet is all NVIDIA — and its owner moves from W5 to the ROW. Still NO throughput claim; analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) W6 DONE 2026-08-19 ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): the eligibility predicate, #1020, and the negative result on the piecewise arm. | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-BREAK-W6`; [#1163](https://github.com/mudler/vllm.cpp/issues/1163), [#1192](https://github.com/mudler/vllm.cpp/issues/1192), [#1261](https://github.com/mudler/vllm.cpp/issues/1261), [#1291](https://github.com/mudler/vllm.cpp/issues/1291), [#1307](https://github.com/mudler/vllm.cpp/issues/1307), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1020](https://github.com/mudler/vllm.cpp/issues/1020), [#1335](https://github.com/mudler/vllm.cpp/issues/1335), [#1374](https://github.com/mudler/vllm.cpp/issues/1374), [#1380](https://github.com/mudler/vllm.cpp/issues/1380), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1390](https://github.com/mudler/vllm.cpp/issues/1390) | | `ENG-CUDAGRAPH-DIFFUSION` | Capture the LTX-2.5 denoise loop (fixed shapes, many identical iterations — the ideal graph target). **BLOCKED, and the blocker is ours:** the render does almost no device compute to capture | T2 | SGLang enabled BCG on this shape AFTER our pin — LTX-2 H200 two-stage 10.75s->6.90s (`d4be483efb`), SANA 1024px -26% (`6c7498113f`), SANA denoise 0.73->0.457s (`56ef810cad`). Dated events, NOT pinned evidence; their win is mostly PyTorch host tax we do not pay | NO capture at all: `grep` for capture across `src/vllm/model_executor/models/ltx2*.cpp` returns nothing | blocked by [#1024](https://github.com/mudler/vllm.cpp/issues/1024) (GPU util **exactly 0 in 321 of 347 samples**, 1.00 core of 20 held for 17+ min after staging), [#1007](https://github.com/mudler/vllm.cpp/issues/1007) (VAE decode has no device arm), [#1087](https://github.com/mudler/vllm.cpp/issues/1087) (**57-66% of wall** is ONE resolution-CONSTANT serial host phase), [#1010](https://github.com/mudler/vllm.cpp/issues/1010) (no phase-boundary log). Decision point is a MEASUREMENT of GPU-busy vs wall once device-resident, not an implementation. **The unblock order now has an owning row:** `LTX25-DEVICE-RESIDENCY` ([#1264](https://github.com/mudler/vllm.cpp/issues/1264), [ltx25-device-residency.md](specs/ltx25-device-residency.md)) stages those defects W0-W6 and carries this decision point as its W7 — if the loop comes back GPU-bound, #1164 closes as a refutation the way [#1161](https://github.com/mudler/vllm.cpp/issues/1161) closed prefill capture | [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | `INVENTORIED` | [#1164](https://github.com/mudler/vllm.cpp/issues/1164) | | `ENG-BATCH-INVARIANT` | Opt-in deterministic execution across scheduler batch sizes (`VLLM_BATCH_INVARIANT=1`): batch-invariant matmul/norm/attention/collectives plus persistent-scheduler NVFP4; production default remains off | T1 | default/env `vllm/envs.py:89,576-578`; initialization `vllm/v1/worker/gpu_worker.py:1262`; NVFP4 dispatch `csrc/libtorch_stable/quantization/fp4/nvfp4_scaled_mm_sm120_kernels.cu:212-220`; suite fixture `tests/v1/determinism/conftest.py:9-12`; operator/e2e `tests/v1/determinism/test_nvfp4_batch_invariant_scaled_mm.py`, `tests/v1/determinism/test_nvfp4_batch_invariant.py` @ `702f481` | - | [W3-C3R executed contract](specs/nvfp4-persistent-plan-cache.md#w3-c3r-batch-shape-localization-and-gate-correction-2026-07-13): production-default ours and vLLM both change outputs across batch shapes; no local opt-in implementation is claimed | `planned: specs/batch-invariant-execution.md` | `INVENTORIED` | - | | `ENG-ASYNC-SCHED` | Async/overlap scheduling (AsyncScheduler placeholders + depth-2 batch-queue step + async D2H on a copy stream); vLLM's DEFAULT at the pin — mirror obligation per B3. **Host-side machinery + runner device-input half + sampler-OUTPUT half LANDED + CPU-gated (2026-07-16):** `AsyncScheduler` placeholder accounting, `step_with_batch_queue` depth-2, `ResolveAsyncScheduling` default-ON-when-compatible + `MaxConcurrentBatches`, `VT_ASYNC_SCHED` rollback; the runner device-input path `combine_sampled_and_draft_tokens`; PLUS the sampler-OUTPUT half — `vt::Backend` event/pinned primitives (`AllocPinned`/events, CUDA cudaHostAlloc+cudaEvent, CPU sync-degeneration), `AsyncGPUModelRunnerOutput` (device sampled-id snapshot → non-blocking D2H on a copy queue + event; `get_output()` waits only that event; MAIN queue never blocked), `Sampler::forward(sampled_ids_out)` device-resident greedy, `GPUModelRunner::sample_tokens_async` + `runner_supports_async`, and the `Executor`+`step_with_batch_queue` seam resolving `get_output()` at CONSUME time. All behind `VT_ASYNC_RUNNER`/`set_async_input_combine`, default OFF. Sync path byte-identical (placeholder sites INERT while count 0; combine off; `sample_tokens_async` degenerates to sync when async off; `sampled_ids_out=nullptr`). **ENABLE-FLIP LANDED + CPU-gated (2026-07-16):** (1) `LoadedEngine` now reorders `runner_` before the scheduler and builds an `AsyncScheduler` + `max_concurrent_batches=2` when `ResolveAsyncScheduling(runner_.runner_supports_async())` resolves ON (else the byte-identical synchronous `Scheduler` + depth-1); the resolved mcb threads into `AsyncLLM`→`EngineCoreProc` (`step_with_batch_queue`) and the "Asynchronous scheduling is enabled/disabled" log mirrors vLLM for A/B audit; (2) the device combine/scatter kernel (`_combine_sampled_and_draft_tokens_kernel` + last_sampled scatter) is ported to CUDA (`src/vt/cuda/cuda_combine_tokens.cu`), main-stream-ordered on the CUDA async path so it DELETES `sample_tokens_async`'s pre-scatter `Synchronize`; the CPU backend keeps the host loop. `VT_ASYNC_RUNNER=1` engages full W3; `VT_ASYNC_SCHED=0` is the same-binary rollback. Production default (no env) stays synchronous byte-identical. **FULL W3 DGX proof RAN twice** — `f086b64` (5/5 gates PASS; c16 TPOT −5.4 ms WIN, tput neutral, TTFT +36 % = Little's-law repayment) and the 2026-07-16 re-proof on the THROUGHPUT-lever fix (persistent pooled sampled-id/pinned buffers + `Sampler` greedy scratch removing ALL per-step `cudaMalloc`/`cudaFree`/`cudaHostAlloc`/event-create from the sampled-id path, incl. the overlap-killing `cudaFree` inside `get_output`; mirrors `gpu_model_runner.py:873-878` + `async_utils.py:12-70`): token-exactness **6/6 PASS**, interleaved c16 **tput −0.32 % (gate ≥+1.5 % FAILS), TPOT −4.95 ms retained, TTFT +34.8 %** — the allocator lever is REFUTED as the tput unlock (≤0.1 % of a ~165 ms c16 step). **DEFAULT FLIPPED ON 2026-07-17** (`VT_ASYNC_RUNNER` default ON via the pure `AsyncRunnerFlagIsOn` predicate, mirroring `vllm/config/vllm.py:992-1044`): the discriminator (`6ea7856`) proved vLLM's own async pays the identical +26–31 % TTFT / −0.7 to −0.9 % tput / −2.6 to −4.3 ms TPOT envelope and W3-ON nets positive (both binding ITL-tail anomalies flip to PASS), so the "needs a throughput lever" ship-gate is RETIRED — W3 is a parity/mirror obligation with a tails+TPOT win. The flip is TOKEN-NEUTRAL (async-ON ≡ async-OFF bit-identical on DGX). `VT_ASYNC_RUNNER=0` = runner-level rollback, `VT_ASYNC_SCHED=0` = scheduler-level rollback. TTFT means rise into vLLM's async envelope BY DESIGN — the next binding grid runs async by default and its TTFT must NOT be misread as a regression. **ROBUSTNESS FIX 2026-07-20 (`discard_request_mask`):** the runner was missing vLLM's `discard_request_mask`, so `GPUModelRunner` emitted a sampled token for prefill-CHUNK requests too; under async this drained a `num_output_placeholders` never reserved (the `is_prefill_chunk` path adds none) → the `async_scheduler.cpp` `num_output_placeholders >= 0` assertion aborted on c8 + short-output (chunked prefill + preemption). FIX mirrors vLLM: `execute_model` computes `exec_state_.discard[i] = seq_len < num_tokens` (`gpu_model_runner.py:2048`); `sample_tokens` clears those rows to empty (`outputs.py:303`), the async path passes `invalid_req_indices` to `AsyncGPUModelRunnerOutput::get_output` (`gpu_model_runner.py:3625` + `outputs.py:303`). Scheduler UNCHANGED (assertion kept — it was correct once the runner honors `scheduler.py:1888-1890`). Sync/non-chunked decode byte-identical (mask all-zero); DGX 27B 235/235 + 35B 315/315, `vllm-bench` c8+short-output+chunked+kv-pressure no longer crashes, memcheck 0. Ledger [parity-ledger.md](parity-ledger.md) 2026-07-20 row | T1 | `vllm/v1/core/sched/async_scheduler.py:12`; `vllm/config/vllm.py:490,990,1038`; `vllm/v1/engine/core.py:519`; `vllm/v1/worker/gpu/input_batch.py:304-406`; `vllm/v1/worker/gpu/async_utils.py:12-70`; `vllm/v1/worker/gpu/gpu_model_runner.py:242-332`; `vllm/v1/outputs.py:298-307` | `src/vllm/v1/core/sched/async_scheduler.cpp:10,45`; placeholder plumbing `src/vllm/v1/core/sched/scheduler.cpp:148,164,605`; `src/vllm/v1/engine/core.cpp:91` (`step_with_batch_queue`, async-output seam); `src/vllm/v1/engine/core_proc.cpp:32,46`; config `include/vllm/config/scheduler.h:117,165,188`, `src/vllm/config/scheduler.cpp:12`; `include/vllm/v1/request.h:187`; runner input leaf `src/vllm/v1/worker/gpu/prepare_inputs.cpp`, `src/vllm/v1/worker/gpu/input_batch.cpp`; runner output leaf `include/vt/backend.h`+`src/vt/backend.cpp`+`src/vt/cuda/cuda_backend.cu` (event/pinned), `include/vllm/v1/worker/gpu/async_output.{h,cpp}` (`AsyncGPUModelRunnerOutput`), `src/vllm/v1/sample/sampler.cpp` (`sampled_ids_out`), `src/vllm/v1/worker/gpu/runner.cpp` (`sample_tokens_async`/`runner_supports_async`), `src/vllm/v1/executor/executor.cpp`+`include/vllm/v1/worker/gpu/model_runner_base.h` (async seam); enable-flip `include/vllm/entrypoints/model_loader.h`+`src/vllm/entrypoints/model_loader.cpp` (`runner_` before scheduler, `ResolveAsyncEnabled`/`MakeScheduler`, `AsyncScheduler`+mcb=2, log), `include/vllm/v1/engine/async_llm.h`+`src/vllm/v1/engine/async_llm.cpp` (mcb param → `EngineCoreProc`); device kernel `include/vt/cuda/combine_tokens.h`+`src/vt/cuda/cuda_combine_tokens.cu`, wired `src/vllm/v1/worker/gpu/runner.cpp` (CUDA combine/scatter branch removes the pre-sync) | `tests/vllm/v1/test_async_scheduler.cpp:1` (6 cases, 54 asserts; RED vs base Scheduler 2/6 fail); depth-2 engine cycle `tests/vllm/v1/test_engine_core_proc.cpp:479` (mcb=2, async-output seam); config resolution `tests/vllm/test_scheduler_config.cpp:75`; enable-flip construction matrix `tests/vllm/entrypoints/test_loaded_engine_dense.cpp` (runner×VT_ASYNC_SCHED → scheduler type + mcb; RED = un-flipped engine, 3/3 ON-arm asserts fail); runner input leaf `test_combine_tokens.cpp` (RED = stale → 5/7 fail), `test_input_batch.cpp`, `test_runner.cpp` (async-ON≡sync); output leaf `tests/vt/test_backend.cpp` (event/pinned contract), `tests/vllm/v1/worker/test_async_output.cpp` (materialize/flush/snapshot; RED = +1 splice), `test_runner.cpp` (`sample_tokens_async` decode ≡ sync); full CPU ctest 111/111, tools 164/164. Prior diagnostic `3812d8` six-leg control: total **1.002153×**, TTFT **0.862159×**, no GPU-time reduction (neutral for speed). **DEFAULT-FLIP (2026-07-17):** new pure CPU flag test [test_async_runner_flag.cpp](../tests/vllm/v1/worker/test_async_runner_flag.cpp) (11 asserts, default-ON/'0'-off); construction matrix [test_loaded_engine_dense.cpp](../tests/vllm/entrypoints/test_loaded_engine_dense.cpp) INVERTED (default → AsyncScheduler+mcb=2; RED verified 5 asserts fail vs un-flipped). CPU clean `-Werror` rebuild, full serial ctest **116/116**, tools **164/164**. **DGX re-confirmation** (evidence `dgx:~/work/vllm.cpp-async-flip`, CUTLASS+FA2 hard-verified, one flock): shipping default (async ON + RMSNorm-fast OFF) → **27B 235/235 + 35B 315/315** with the "Asynchronous scheduling is enabled (mcb=2)" log, and both rollback arms (`VT_ASYNC_RUNNER=0`, `VT_ASYNC_SCHED=0`) 235/235 + 315/315 log "disabled"; async arms BIT-IDENTICAL (token-neutral). Closing record [parity-ledger.md#L502](parity-ledger.md#L502) | [async-serving.md](specs/async-serving.md) | `DONE` | `6ea7856` | diff --git a/.agents/specs/eng-cudagraph-break.md b/.agents/specs/eng-cudagraph-break.md index b6b8be26b..906a09b0b 100644 --- a/.agents/specs/eng-cudagraph-break.md +++ b/.agents/specs/eng-cudagraph-break.md @@ -1877,11 +1877,14 @@ Each item names the stage that owns it. Nothing here is claimed by W1. architectures: three runs each — right host ids and no mirror as the reference, stale host ids and no mirror as the CONTROL that must differ, stale host ids with the truth reaching the model only through `device_token_ids` as the gate. - RED before the fix at 2 cases / 59 assertions / 12 failed / exit 1, with 200 of - 200 logit values differing per step on both architectures; GREEN after at - 59/59. Two mutations: deleting the registry scope line reds it at 6 assertions, - and swapping the seam's DEVICE arm for its HOST arm leaves the logits BIT - IDENTICAL and reds only the counters, which is the arm no token gate can see. + RED before the fix at 2 cases / 65 assertions / 10 failed / exit 1, with 800 of + 800 logit values differing over four steps on BOTH architectures; GREEN after at + 65/65, exit 0. Two mutations, each compiled clean and each restored by sha256: + deleting the registry's scope line — the production call site — reds 4 + assertions and puts all 800 values back, and swapping the seam's DEVICE arm for + its HOST arm leaves the logits BIT IDENTICAL at 0 of 800 differing and reds only + `device_refreshes` and `host_refreshes`, which is the arm no token gate can + see. **WHAT IS STILL OWED, narrowed rather than closed.** The depth-2 four-concurrent battery against these two models on a real device has NOT been @@ -1889,8 +1892,16 @@ Each item names the stage that owns it. Nothing here is claimed by W1. fix is proven to embed the mirror's identifiers and is NOT proven to close the degeneration `qwen3.cpp`'s decline was measured against — whose own cause W4 established is unidentified. `qwen3.cpp`'s decline therefore STANDS, untouched. - Owner: row **`ENG-CUDAGRAPH-BREAK`**, the stage that gets a `dgx` window with - checkpoints; the same window the decline entry above already owes two runs to. + The reason this stage did not run it, stated as a fleet state rather than as an + intention: at 2026-08-19, `rc devices` read `dgx:gpu0 busy` — the only box whose + HuggingFace cache carries Qwen3-Coder-30B-A3B — while `thor:gpu0` and + `orin:gpu0` were ready and carry no such checkpoint, so the battery was not + obtainable in the window rather than skipped. The CPU gate compares all four + steps for exactly this reason: on a device the two replay steps become the + assertion the defect is about, and the file becomes the device gate the moment + it runs on one. Owner: row **`ENG-CUDAGRAPH-BREAK`**, the stage that gets a + `dgx` window with checkpoints; the same window the decline entry above already + owes two runs to. - **`test_qwen3_5_decode_graph_seam` SIGSEGVs on `main`, in W6's own case, and every assertion passes** ([#1390](https://github.com/mudler/vllm.cpp/issues/1390), found while landing diff --git a/tests/vllm/models/test_moe_async_device_ids.cpp b/tests/vllm/models/test_moe_async_device_ids.cpp index 946949c60..738b86faa 100644 --- a/tests/vllm/models/test_moe_async_device_ids.cpp +++ b/tests/vllm/models/test_moe_async_device_ids.cpp @@ -379,6 +379,13 @@ const std::vector>& TrueIds() { return v; } +struct DeviceFree { + vt::Backend* b = nullptr; + void operator()(void* p) const { + if (p != nullptr && b != nullptr) b->Free(p); + } +}; + // Drive `steps` pure-decode steps through ModelRegistry::Forward and return the // downloaded [2, vocab] logits of each step. `mirror` selects run C: the host // vector is replaced by zeros and the true identifiers travel only through @@ -409,9 +416,19 @@ std::vector> Run(const Fixture& fx, bool stale_host, bool mir in.pure_decode = true; in.gdn_state_slots = 8; in.uniform_query_len = 1; - // On CPU a host pointer IS device-addressable, which is what makes the - // mirror's contract directly testable without a GPU. - if (mirror) in.device_token_ids = truth.data(); + // THE MIRROR'S BUFFER IS A REAL BACKEND ALLOCATION, not the host vector's + // address. On CPU the two are the same thing, so this buys nothing today; + // it is what makes the case correct by construction on a device, where + // `device_token_ids` is a pointer the runner's combine wrote and a host + // address would be the wrong kind of pointer. + std::unique_ptr mirror_ids; + if (mirror) { + const size_t bytes = truth.size() * sizeof(int32_t); + mirror_ids.reset(be.Alloc(bytes)); + mirror_ids.get_deleter().b = &be; + be.Copy(q, mirror_ids.get(), truth.data(), bytes); + in.device_token_ids = static_cast(mirror_ids.get()); + } const vllm::ForwardLogits fl = ModelRegistry::Forward(*model, in); REQUIRE(fl.on_device()); std::vector rows(static_cast(2 * kV)); @@ -467,11 +484,15 @@ TEST_CASE( const std::vector> stale = Run(fx, /*stale_host=*/true, /*mirror=*/false, kSteps); CHECK(vt::GetStepInputStats().device_refreshes == 0); - // Only the COLD and CAPTURE steps recompute on this harness; a CPU replay - // returns the slot's persistent logits unchanged, so steps 2 and 3 carry no - // information either way and are not asserted on. - CHECK(Differing(ref[0], stale[0]) > 0); - CHECK(Differing(ref[1], stale[1]) > 0); + // EVERY step is compared, and what each one MEANS depends on the harness. On + // CPU only the cold and capture steps recompute — a CPU "replay" returns the + // slot's persistent logits unchanged — so steps 2 and 3 hold what step 1 + // produced and the comparison is true for that reason there. On a DEVICE a + // replay recomputes, and those steps become the assertion the reported defect + // is actually about: that a REPLAY does not generate from stale identifiers. + // Asserting them costs nothing here and makes this file the device gate the + // moment it runs on one. + for (int t = 0; t < kSteps; ++t) CHECK(Differing(ref[t], stale[t]) > 0); // RUN C — THE GATE. The same stale host vector, with the true identifiers // reaching the model ONLY through `device_token_ids`. @@ -486,11 +507,12 @@ TEST_CASE( CHECK(s.device_refreshes == kSteps); CHECK(s.host_refreshes == kSteps); } - CHECK(Differing(ref[0], via_device[0]) == 0); - CHECK(Differing(ref[1], via_device[1]) == 0); + size_t differing = 0; + for (int t = 0; t < kSteps; ++t) differing += Differing(ref[t], via_device[t]); + CHECK(differing == 0); MESSAGE("registry forward, mirror vs host reference, bit for bit: " - << ref[0].size() << " values per step, " << Differing(ref[0], via_device[0]) - << " and " << Differing(ref[1], via_device[1]) << " differing"); + << kSteps << " steps x " << ref[0].size() << " values, " << differing + << " differing"); } TEST_CASE( @@ -517,8 +539,7 @@ TEST_CASE( const std::vector> stale = Run(fx, /*stale_host=*/true, /*mirror=*/false, kSteps); CHECK(vt::GetStepInputStats().device_refreshes == 0); - CHECK(Differing(ref[0], stale[0]) > 0); - CHECK(Differing(ref[1], stale[1]) > 0); + for (int t = 0; t < kSteps; ++t) CHECK(Differing(ref[t], stale[t]) > 0); vt::ResetStepInputStats(); const std::vector> via_device = @@ -528,9 +549,10 @@ TEST_CASE( CHECK(s.device_refreshes == kSteps); CHECK(s.host_refreshes == kSteps); } - CHECK(Differing(ref[0], via_device[0]) == 0); - CHECK(Differing(ref[1], via_device[1]) == 0); + size_t differing = 0; + for (int t = 0; t < kSteps; ++t) differing += Differing(ref[t], via_device[t]); + CHECK(differing == 0); MESSAGE("registry forward, mirror vs host reference, bit for bit: " - << ref[0].size() << " values per step, " << Differing(ref[0], via_device[0]) - << " and " << Differing(ref[1], via_device[1]) << " differing"); + << kSteps << " steps x " << ref[0].size() << " values, " << differing + << " differing"); } From 919acfa0b88853453b2c023207fd5ccca159ac27 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 19 Aug 2026 21:28:51 +0000 Subject: [PATCH 5/9] test(ENG-CUDAGRAPH-BREAK): the EAGER arms and the THIRD registry, which 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&)` 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] --- .../vllm/models/test_moe_async_device_ids.cpp | 229 ++++++++++++------ 1 file changed, 149 insertions(+), 80 deletions(-) diff --git a/tests/vllm/models/test_moe_async_device_ids.cpp b/tests/vllm/models/test_moe_async_device_ids.cpp index 738b86faa..41e9c36f7 100644 --- a/tests/vllm/models/test_moe_async_device_ids.cpp +++ b/tests/vllm/models/test_moe_async_device_ids.cpp @@ -18,10 +18,30 @@ // `tests/vllm/models/test_kimi_linear_paged.cpp` does for the same contract: // hand the RIGHT identifiers ONLY through `device_token_ids`, make the host // vector deliberately wrong, and require the logits to equal a run that had the -// right host identifiers and no mirror. On CPU a host pointer is -// device-addressable, so the contract is directly testable here. +// right host identifiers and no mirror. // -// THREE RUNS, because two of them cannot separate the cases: +// WHAT CPU CANNOT SHOW, stated the right way round. `vt::Backend::Alloc` returns +// HOST-addressable memory on this backend, so the mirror's buffer and the host +// vector are the same kind of pointer and both refresh arms reduce to the same +// `memcpy` from the same address. That is precisely why the DEVICE half of the +// contract is NOT directly testable here: replacing +// `PersistentStepInput::RefreshFromDevice` with `RefreshFromHost` leaves every +// logit bit-identical and reds only the `device_refreshes`/`host_refreshes` +// counters. Those counters are a legitimate stand-in for WHICH ARM RAN, and they +// are what this file asserts; they gate the instrument, not the behaviour. The +// two behavioural guarantees — that the copy reads DEVICE memory, and that it is +// ordered on the main queue AFTER the runner's combine — are untested on any +// device, and the spec's `## Owed` records that rather than implying otherwise. +// +// BOTH LANES, because the defect had two halves and only one of them could ever +// have been declined. A case that constructs `StaticGraphCpu` gets the decode +// GRAPH driver, because that harness is what makes the CPU platform answer +// `support_static_graph_mode()` true. A case that does NOT construct it gets the +// registry's EAGER arm (`ForwardDevice`), which is the lane every non-CUDA and +// every non-pure-decode step takes and the lane no refusal could have mitigated. +// Each registration owes the guarantee on both, so each is run twice. +// +// THREE RUNS per lane, because two of them cannot separate the cases: // // A right host ids, no mirror -> the reference // B WRONG host ids, no mirror -> must DIFFER from A @@ -258,10 +278,16 @@ struct CachePool { constexpr int64_t kDsQkNope = 16, kDsQkRope = 8, kDsVHead = 16, kDsKvLora = 24; constexpr int64_t kDsHeads = 4, kDsE = 4, kDsMoeI = 16; -std::string DsConfigJson() { +// The SAME geometry serves BOTH registrations that reach `DeepseekV2DecodeGraph`. +// `glm4_moe_lite_registry.cpp` loads `DeepseekV2Weights` through the identical +// loader and dispatches to the identical driver; the only thing that differs is +// which registry forward publishes the scope, which is exactly the call site +// #1305 changed and the one nothing gated before this file. +std::string DsConfigJson(const char* architecture = "DeepseekV2ForCausalLM", + const char* model_type = "deepseek_v2") { nlohmann::json j; - j["architectures"] = std::vector{"DeepseekV2ForCausalLM"}; - j["model_type"] = "deepseek_v2"; + j["architectures"] = std::vector{architecture}; + j["model_type"] = model_type; j["hidden_size"] = kH; j["num_hidden_layers"] = kL; j["num_attention_heads"] = kDsHeads; @@ -447,112 +473,155 @@ size_t Differing(const std::vector& a, const std::vector& b) { return n; } -} // namespace - -TEST_CASE( - "Qwen3MoeForCausalLM embeds the async mirror's DEVICE ids, not the stale host " - "vector") { - Fixture fx(ConfigJson(), BuildTensors()); - REQUIRE(fx.cfg.num_experts == kE); - REQUIRE_MESSAGE(vt::GraphCaptureEnabled(), - "this gate needs the CAPTURING lane; VLLM_CPP_CUDAGRAPH=0 is set"); - // The decode-graph arm of the registry admits itself only where the platform - // reports static-graph mode, which CPU does not. The harness swaps both - // registries so the driver's OWN predicate is what routes, exactly as the - // seam gates do. - StaticGraphCpu harness; - - constexpr int kSteps = 4; +// THE A/B/C TRIPLE, over whichever lane the CALLER has already put the registry +// in. Constructing `StaticGraphCpu` before calling this routes the step into the +// decode-GRAPH driver; not constructing it leaves the CPU platform answering +// `support_static_graph_mode()` false, which is the registry's EAGER arm. +// +// `through_seam` IS AN ASSERTION AND NOT DECORATION, and it is what makes the +// two lanes distinguishable from inside one helper. The graph driver refreshes a +// `vllm::StepTokenIds`, so it 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 at all. Checking the +// counters BOTH ways means a change that quietly moved a case onto the other +// lane could not keep it green. +template +void AbcTriple(const Fixture& fx, bool through_seam, int steps = 4) { + const int64_t want = through_seam ? steps : 0; // RUN A — the reference: the identifiers arrive on the host, no mirror. vt::ResetStepInputStats(); - const std::vector> ref = Run(fx, /*stale_host=*/false, - /*mirror=*/false, kSteps); + const std::vector> ref = + Run(fx, /*stale_host=*/false, /*mirror=*/false, steps); { const vt::StepInputStats s = vt::GetStepInputStats(); - // The slot binds once and refreshes from the HOST every step; with no mirror - // the device arm must never be taken. - CHECK(s.host_refreshes == kSteps); + // On the graph lane the slot binds once and refreshes from the HOST every + // step; with no mirror the device arm must never be taken. On the eager lane + // NOTHING binds, which is how this case proves it is on the other lane. + CHECK(s.host_refreshes == want); CHECK(s.device_refreshes == 0); - CHECK(s.binds >= 1); + if (through_seam) { + CHECK(s.binds >= 1); + } else { + CHECK(s.binds == 0); + } } // RUN B — THE CONTROL. Stale host identifiers and no mirror: the logits must // MOVE. Without this arm a model that ignored its identifiers entirely would // satisfy run C. vt::ResetStepInputStats(); - const std::vector> stale = Run(fx, /*stale_host=*/true, - /*mirror=*/false, kSteps); + const std::vector> stale = + Run(fx, /*stale_host=*/true, /*mirror=*/false, steps); CHECK(vt::GetStepInputStats().device_refreshes == 0); - // EVERY step is compared, and what each one MEANS depends on the harness. On - // CPU only the cold and capture steps recompute — a CPU "replay" returns the - // slot's persistent logits unchanged — so steps 2 and 3 hold what step 1 - // produced and the comparison is true for that reason there. On a DEVICE a - // replay recomputes, and those steps become the assertion the reported defect - // is actually about: that a REPLAY does not generate from stale identifiers. - // Asserting them costs nothing here and makes this file the device gate the - // moment it runs on one. - for (int t = 0; t < kSteps; ++t) CHECK(Differing(ref[t], stale[t]) > 0); + // EVERY step is compared, and what each one MEANS depends on the lane. On the + // eager lane every step recomputes, so every comparison is a live one. On the + // graph lane a CPU "replay" recomputes nothing — it returns the slot's + // persistent logits unchanged — so steps 2 and 3 hold what step 1 produced and + // the comparison is true for that reason there. On a DEVICE a replay + // recomputes, and those steps become the assertion the reported defect is + // actually about: that a REPLAY does not generate from stale identifiers. + for (int t = 0; t < steps; ++t) CHECK(Differing(ref[t], stale[t]) > 0); // RUN C — THE GATE. The same stale host vector, with the true identifiers // reaching the model ONLY through `device_token_ids`. vt::ResetStepInputStats(); - const std::vector> via_device = Run(fx, /*stale_host=*/true, - /*mirror=*/true, kSteps); + const std::vector> via_device = + Run(fx, /*stale_host=*/true, /*mirror=*/true, steps); { const vt::StepInputStats s = vt::GetStepInputStats(); - // THE SEAM, ASSERTED. `device_refreshes` moves only inside - // `vt::PersistentStepInput::RefreshFromDevice`; a hand-rolled copy in the - // driver would produce identical logits and leave this at zero. - CHECK(s.device_refreshes == kSteps); - CHECK(s.host_refreshes == kSteps); + // THE SEAM, ASSERTED on the lane that owes it. `device_refreshes` moves only + // inside `vt::PersistentStepInput::RefreshFromDevice`; a hand-rolled copy in + // the graph driver would produce identical logits and leave this at zero. + CHECK(s.device_refreshes == want); + CHECK(s.host_refreshes == want); } size_t differing = 0; - for (int t = 0; t < kSteps; ++t) differing += Differing(ref[t], via_device[t]); + for (int t = 0; t < steps; ++t) differing += Differing(ref[t], via_device[t]); CHECK(differing == 0); MESSAGE("registry forward, mirror vs host reference, bit for bit: " - << kSteps << " steps x " << ref[0].size() << " values, " << differing + << steps << " steps x " << ref[0].size() << " values, " << differing << " differing"); } +} // namespace + +// ─── Qwen3-MoE: `qwen3_moe_registry.cpp`, both lanes ───────────────────────── + TEST_CASE( - "DeepseekV2ForCausalLM embeds the async mirror's DEVICE ids, not the stale host " - "vector") { - Fixture fx(DsConfigJson(), DsBuildTensors()); + "Qwen3MoeForCausalLM GRAPH arm embeds the async mirror's DEVICE ids, not the " + "stale host vector") { + Fixture fx(ConfigJson(), BuildTensors()); + REQUIRE(fx.cfg.num_experts == kE); REQUIRE_MESSAGE(vt::GraphCaptureEnabled(), "this gate needs the CAPTURING lane; VLLM_CPP_CUDAGRAPH=0 is set"); + // The decode-graph arm of the registry admits itself only where the platform + // reports static-graph mode, which CPU does not. The harness swaps both + // registries so the driver's OWN predicate is what routes, exactly as the + // seam gates do. StaticGraphCpu harness; + AbcTriple(fx, /*through_seam=*/true); +} + +// THE EAGER HALF, which is the half no decline could ever have mitigated and the +// half nothing in this tree gated. `ForwardQwen3MoeForCausalLM` falls through to +// `Qwen3MoeModel::ForwardDevice` on every step that is not pure-decode-on-a +// -static-graph platform, and `ForwardBody` embeds there from the HOST vector. +// Before #1305 that arm never looked at `device_token_ids` either, so on the +// asynchronous serving path it generated from the previous step's identifiers +// exactly like the graph arm did. NO harness here: a plain CPU platform answers +// `support_static_graph_mode()` false, so the registry's own predicate routes. +TEST_CASE( + "Qwen3MoeForCausalLM EAGER arm embeds the async mirror's DEVICE ids, not the " + "stale host vector") { + Fixture fx(ConfigJson(), BuildTensors()); + REQUIRE(fx.cfg.num_experts == kE); + AbcTriple(fx, /*through_seam=*/false); +} - constexpr int kSteps = 4; +// ─── DeepSeek-V2: `deepseek_v2_registry.cpp`, both lanes ───────────────────── - vt::ResetStepInputStats(); - const std::vector> ref = - Run(fx, /*stale_host=*/false, /*mirror=*/false, kSteps); - { - const vt::StepInputStats s = vt::GetStepInputStats(); - CHECK(s.host_refreshes == kSteps); - CHECK(s.device_refreshes == 0); - CHECK(s.binds >= 1); - } +TEST_CASE( + "DeepseekV2ForCausalLM GRAPH arm embeds the async mirror's DEVICE ids, not " + "the stale host vector") { + Fixture fx(DsConfigJson(), DsBuildTensors()); + REQUIRE_MESSAGE(vt::GraphCaptureEnabled(), + "this gate needs the CAPTURING lane; VLLM_CPP_CUDAGRAPH=0 is set"); + StaticGraphCpu harness; + AbcTriple(fx, /*through_seam=*/true); +} - vt::ResetStepInputStats(); - const std::vector> stale = - Run(fx, /*stale_host=*/true, /*mirror=*/false, kSteps); - CHECK(vt::GetStepInputStats().device_refreshes == 0); - for (int t = 0; t < kSteps; ++t) CHECK(Differing(ref[t], stale[t]) > 0); +TEST_CASE( + "DeepseekV2ForCausalLM EAGER arm embeds the async mirror's DEVICE ids, not " + "the stale host vector") { + Fixture fx(DsConfigJson(), DsBuildTensors()); + AbcTriple(fx, /*through_seam=*/false); +} - vt::ResetStepInputStats(); - const std::vector> via_device = - Run(fx, /*stale_host=*/true, /*mirror=*/true, kSteps); - { - const vt::StepInputStats s = vt::GetStepInputStats(); - CHECK(s.device_refreshes == kSteps); - CHECK(s.host_refreshes == kSteps); - } - size_t differing = 0; - for (int t = 0; t < kSteps; ++t) differing += Differing(ref[t], via_device[t]); - CHECK(differing == 0); - MESSAGE("registry forward, mirror vs host reference, bit for bit: " - << kSteps << " steps x " << ref[0].size() << " values, " << differing - << " differing"); +// ─── GLM-4-MoE-Lite: `glm4_moe_lite_registry.cpp`, THE THIRD REGISTRATION ──── +// +// #1305 changed THREE registry forwards and the PR that landed it mutated TWO. +// This one shares `DeepseekV2DecodeGraph` and `DeepseekV2Model` with +// `deepseek_v2_registry.cpp` down to the weights struct, so the only thing it +// owns — and the only thing a gate on the other two can prove nothing about — is +// its OWN `detail::DeviceTokenIdsScope`. Deleting that one scope leaves both +// DeepSeek cases green, which is why this case exists as a separate one rather +// than as a comment claiming coverage. +TEST_CASE( + "Glm4MoeLiteForCausalLM GRAPH arm embeds the async mirror's DEVICE ids, not " + "the stale host vector") { + Fixture fx(DsConfigJson("Glm4MoeLiteForCausalLM", "glm4_moe_lite"), + DsBuildTensors()); + REQUIRE_MESSAGE(vt::GraphCaptureEnabled(), + "this gate needs the CAPTURING lane; VLLM_CPP_CUDAGRAPH=0 is set"); + StaticGraphCpu harness; + AbcTriple(fx, /*through_seam=*/true); +} + +TEST_CASE( + "Glm4MoeLiteForCausalLM EAGER arm embeds the async mirror's DEVICE ids, not " + "the stale host vector") { + Fixture fx(DsConfigJson("Glm4MoeLiteForCausalLM", "glm4_moe_lite"), + DsBuildTensors()); + AbcTriple(fx, /*through_seam=*/false); } From 532cb8c259858ed6dc6c494019cc03056ea9d128 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 19 Aug 2026 21:41:42 +0000 Subject: [PATCH 6/9] refactor(ENG-CUDAGRAPH-BREAK): one consumer for the device-id scope, 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] --- .../model_executor/models/step_token_ids.h | 8 ++++ include/vt/persistent_step_input.h | 30 ++++++++---- .../model_executor/models/deepseek_v2.cpp | 24 ++++------ src/vllm/model_executor/models/qwen3.cpp | 48 ++++++++++--------- src/vllm/model_executor/models/qwen3_5.cpp | 35 ++++++++++---- .../model_executor/models/qwen3_5_internal.h | 38 +++++++++++++++ src/vllm/model_executor/models/qwen3_moe.cpp | 28 ++++------- 7 files changed, 136 insertions(+), 75 deletions(-) diff --git a/include/vllm/model_executor/models/step_token_ids.h b/include/vllm/model_executor/models/step_token_ids.h index bdf2d5d40..756020d19 100644 --- a/include/vllm/model_executor/models/step_token_ids.h +++ b/include/vllm/model_executor/models/step_token_ids.h @@ -111,6 +111,14 @@ class StepTokenIds { // seam rather than writing a fifth private copy: a step that re-read the device // mirror and one that uploaded a stale host vector leave the same bytes-shaped // destination and the same token count, and no token gate can separate them. + // + // WHO READS THESE, stated rather than assumed. Nothing does, yet: `bound()`, + // `capacity()`, `last_source()`, `device_refreshes()` and `host_refreshes()` + // have no caller in `src/` or `tests/` — `t()` is the only accessor a caller + // uses. What `tests/vllm/models/test_moe_async_device_ids.cpp` asserts is the + // PROCESS-WIDE `vt::GetStepInputStats()`, which the same refreshes move. These + // five are the per-slot form of the same answer and are kept because a + // multi-slot driver needs the per-slot one, not because a reader exists. vt::StepInputSource last_source() const { return cell_.last_source(); } int64_t device_refreshes() const { return cell_.device_refreshes(); } int64_t host_refreshes() const { return cell_.host_refreshes(); } diff --git a/include/vt/persistent_step_input.h b/include/vt/persistent_step_input.h index dbb97f233..ef57500a3 100644 --- a/include/vt/persistent_step_input.h +++ b/include/vt/persistent_step_input.h @@ -114,15 +114,27 @@ void ResetStepInputStats(); // real fix: read the identifiers from a stable device buffer instead of // racing a host read against a device write. // -// **THIS ARM LANDS WITH NO PRODUCTION CALLER, named rather than implied.** -// `grep -rn RefreshFromDevice src/ include/` returns its definition and -// nothing else, and `last_source()`/`StepInputSource` have no production -// reader either. It is a staged slice under AGENTS.md's "Nothing lands -// dead": the capability the decline needs is the DESTINATION — a device -// token-id buffer the captured graph reads — and no driver has one (see the -// decline in `qwen3.cpp`'s `DenseDecodeGraphForward`). Writing this arm's -// caller before that destination exists would be the tenth hand-rolled copy -// this row removes. Owner: row `ENG-CUDAGRAPH-BREAK`, the stage that gets a +// **THIS ARM HAS A PRODUCTION CALLER, and it did not when it landed.** The +// paragraph that used to stand here said `grep -rn RefreshFromDevice src/ +// include/` returned its definition and nothing else. #1305 made that false +// and this records the new state rather than leaving the old one to be +// discovered. The grep now also returns +// `include/vllm/model_executor/models/step_token_ids.h`, whose +// `StepTokenIds::Refresh` is called once per decode step by +// `Qwen3MoeDecodeGraph` and `DeepseekV2DecodeGraph` — three shipped +// registrations (`qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp`, +// `glm4_moe_lite_registry.cpp`) reach it. +// `tests/vllm/models/test_moe_async_device_ids.cpp` holds that call site: +// deleting this arm's invocation reds its three GRAPH cases on the token +// comparison AND on `device_refreshes`. +// +// WHAT IS STILL UNREAD, named rather than implied. `last_source()` and +// `StepInputSource` have no production reader — every caller of either is a +// test. And the call site above sits OUTSIDE the captured region, so this arm +// is read once per step and never at REPLAY time. Replay-time reading is what +// the `qwen3.cpp` decline actually needs, and neither that destination nor +// that placement exists in the dense driver the decline guards. Owner: row +// `ENG-CUDAGRAPH-BREAK`, the stage that gets a // `dgx` window WITH the Qwen3-0.6B/4B checkpoints the battery needs; listed // under `## Owed` in `.agents/specs/eng-cudagraph-break.md` and tracked by // #1179 and #323. The HOST arm IS reached, and its own reach is bounded diff --git a/src/vllm/model_executor/models/deepseek_v2.cpp b/src/vllm/model_executor/models/deepseek_v2.cpp index 65621b085..f4bfd1b7a 100644 --- a/src/vllm/model_executor/models/deepseek_v2.cpp +++ b/src/vllm/model_executor/models/deepseek_v2.cpp @@ -532,19 +532,17 @@ void GatherRows(Dev d, void* dst, const Tensor& src, const std::vector& // The EMBED step, hoisted out of the layer region so it can stay OUTSIDE a CUDA // graph capture (the embedding path takes a device flag through a cudaMalloc + // stream sync — the same reason qwen3_moe.cpp:200 keeps `EmbedInto` outside). -// #1305 — CONSUME the registry's scoped device-id override, once per forward. +// #1305 — THE SCOPED DEVICE-ID OVERRIDE is consumed here, once per forward. // Both registrations that reach this TU (`DeepseekV2ForCausalLM` and // `Glm4MoeLiteForCausalLM`) publish `ModelForwardInput::device_token_ids` through // `detail::DeviceTokenIdsScope`; it is null on every path except the asynchronous // CUDA runner, where the combine has already spliced each decode row's sampled // token into the DEVICE identifiers and left the host vector deliberately stale. -// Taking it CLEARS it, so the first embed in a forward is the one that gets it. -detail::DeviceTokenIds TakeDeviceTokenIds() { - const detail::DeviceTokenIds ov = detail::DeviceTokenIdsOverride(); - if (ov.ids != nullptr) detail::DeviceTokenIdsOverride() = detail::DeviceTokenIds{}; - return ov; -} - +// Both readers below — `detail::TakeDeviceTokenIds` on the graph path and +// `detail::ApplyDeviceTokenIds` on the eager one — CLEAR it, so the first embed +// in a forward is the one that gets it. Both live in `qwen3_5_internal.h` beside +// the scope that publishes them, because four models consume it and each used to +// spell the same two bodies out for itself. // EMBED FROM AN ALREADY-RESIDENT ID TENSOR. The decode-graph driver holds its // identifiers in a `vllm::StepTokenIds` whose device address is stable for the // life of the slot, so this arm takes the tensor instead of re-uploading a host @@ -565,13 +563,7 @@ void EmbedInto(Dev d, DBuf& hidden, const std::vector& token_ids, // graph arm — they embedded the host vector and never looked at the device // mirror. The override's copy is enqueued on the main queue, so it is ordered // AFTER the combine that produced it rather than racing it. - const detail::DeviceTokenIds ov = TakeDeviceTokenIds(); - if (ov.ids != nullptr) { - VT_CHECK(ov.count <= T, - "deepseek v2 embed: device input ids longer than the embed input"); - d.b.Copy(d.q, dids.ptr(), ov.ids, - static_cast(ov.count) * sizeof(int32_t)); - } + detail::ApplyDeviceTokenIds(d.b, d.q, dids.ptr(), T, "deepseek v2 embed"); EmbedInto(d, hidden, dids.t(), weights); } @@ -1036,7 +1028,7 @@ ForwardLogits DeepseekV2DecodeGraph::Step( // enqueued on the main queue, so the second is ordered after the combine // instead of racing it, and all three arms below embed from the SAME stable // address. - const detail::DeviceTokenIds ov = TakeDeviceTokenIds(); + const detail::DeviceTokenIds ov = detail::TakeDeviceTokenIds(); s.ids.Ensure(d, S); s.ids.Refresh(d, s.token_ids, ov.ids, ov.count); if (cols_changed && s.graph.captured()) { diff --git a/src/vllm/model_executor/models/qwen3.cpp b/src/vllm/model_executor/models/qwen3.cpp index feb72fee1..d86c4fcf8 100644 --- a/src/vllm/model_executor/models/qwen3.cpp +++ b/src/vllm/model_executor/models/qwen3.cpp @@ -205,17 +205,12 @@ void GatherRows(Dev d, void* dst, const Tensor& src, const std::vector& // The override is published by the registry forward's detail::DeviceTokenIdsScope // and CONSUMED here on first use; null on every path except the CUDA async runner, // so with no override this is byte-identical to the pre-fix host upload. +// #1305: the take-and-clear and the bounds-checked copy this used to spell out +// are `detail::ApplyDeviceTokenIds` (`qwen3_5_internal.h`), one body for the four +// models that consume the scope. Behaviour, ordering and the refusal message are +// unchanged; only the copy count is. static void ApplyDeviceTokenIdsOverride(Dev d, DBuf& dids, int64_t T) { - const detail::DeviceTokenIds ov = detail::DeviceTokenIdsOverride(); - if (ov.ids == nullptr) return; - detail::DeviceTokenIdsOverride() = detail::DeviceTokenIds{}; - // A device buffer LONGER than the embed's input would run past the end. That can - // only mean the runner and the model disagree about this step's shape, so fail - // loudly rather than corrupt the embedding. - VT_CHECK(ov.count <= T, - "qwen3 dense embed: device input ids longer than the embed input"); - d.b.Copy(d.q, dids.ptr(), ov.ids, - static_cast(ov.count) * sizeof(int32_t)); + detail::ApplyDeviceTokenIds(d.b, d.q, dids.ptr(), T, "qwen3 dense embed"); } // Embed: hidden[T,H] bf16 = embed_tokens[token_ids] (device-resident table). KEPT @@ -1099,18 +1094,27 @@ std::optional DenseDecodeGraphForward( // (`tests/parity/test_qwen3_dense_async_serving.cpp`) run twice — as it stands, // and with the decline deleted, because only the second can fail. // - // THE FIX THIS COMMENT NAMES DOES NOT EXIST IN ANY DRIVER, and W4 measured that - // too. No decode graph carries token ids to the device: `StepDevInputs` - // (`qwen3_5.cpp`) has no token-id member, its pinned sibling's `token_ids` block - // was allocated and filled every step and NEVER uploaded and never read (W4 - // removed it), and every batched driver embeds OUTSIDE the captured region from - // the HOST vector. `vt::PersistentStepInput::RefreshFromDevice` - // (`include/vt/persistent_step_input.h`) is the arm that fix needs — reading the - // identifiers at REPLAY time from a stable device buffer, which is this - // comment's own wording — but the DESTINATION it would refresh is still owed. - // W2 (#1261) migrating this driver's capture onto the shared seam did not move - // the inputs, and W4 landing the storage primitive did not create the - // destination. + // THE FIX THIS COMMENT NAMES IS NOW HALF-BUILT, AND NOT IN THIS DRIVER. When W4 + // measured it, no decode graph carried token ids to the device at all: + // `StepDevInputs` (`qwen3_5.cpp`) has no token-id member, its pinned sibling's + // `token_ids` block was allocated and filled every step and NEVER uploaded and + // never read (W4 removed it), and every batched driver embedded OUTSIDE the + // captured region from the HOST vector. #1305 changed the DESTINATION half for + // two of the nine drivers: `Qwen3MoeDecodeGraph` and `DeepseekV2DecodeGraph` + // now hold their step identifiers in a `vllm::StepTokenIds` + // (`include/vllm/model_executor/models/step_token_ids.h`) whose device address + // is stable for the life of the slot, and refresh it through + // `vt::PersistentStepInput::RefreshFromDevice` from the runner's mirror. + // + // THAT BUYS THIS DRIVER NOTHING, and it does not weaken the decline. This + // driver has no such destination: W2 (#1261) migrating its capture onto the + // shared seam did not move the inputs, and #1305 did not touch it. And even in + // the two drivers that now have one, the refresh runs OUTSIDE the capture, once + // per step, because `vt::Embedding` allocates a device bounds-check flag and + // synchronizes the stream and therefore cannot be captured. Reading the + // identifiers at REPLAY time — this comment's own wording for the fix — still + // exists in no driver, which is why #1305 records the graph half of the defect + // as unsettled rather than closing it. // // Declining the graph while the mirror is live falls back to the proven-correct // eager path. This is a MITIGATION, not the end state, and a correct stream diff --git a/src/vllm/model_executor/models/qwen3_5.cpp b/src/vllm/model_executor/models/qwen3_5.cpp index 796110402..811225847 100644 --- a/src/vllm/model_executor/models/qwen3_5.cpp +++ b/src/vllm/model_executor/models/qwen3_5.cpp @@ -545,6 +545,27 @@ detail::DeviceTokenIds& detail::DeviceTokenIdsOverride() { return ids; } +// #1305 — THE CONSUMER SIDE, once, beside the publisher it reads. See +// `qwen3_5_internal.h` for what each argument means and why the copy goes on the +// queue. Four models call these: this one, `qwen3.cpp`, `qwen3_moe.cpp` and +// `deepseek_v2.cpp`, each of which used to carry its own copy of both bodies. +detail::DeviceTokenIds detail::TakeDeviceTokenIds() { + const DeviceTokenIds ov = DeviceTokenIdsOverride(); + if (ov.ids != nullptr) DeviceTokenIdsOverride() = DeviceTokenIds{}; + return ov; +} + +bool detail::ApplyDeviceTokenIds(vt::Backend& backend, vt::Queue& queue, + void* dst, int64_t dst_count, const char* what) { + const DeviceTokenIds ov = TakeDeviceTokenIds(); + if (ov.ids == nullptr) return false; + VT_CHECK(ov.count <= dst_count, + std::string(what) + ": device input ids longer than the embed input"); + backend.Copy(queue, dst, ov.ids, + static_cast(ov.count) * sizeof(int32_t)); + return true; +} + vt::DType detail::ResolveMambaSsmCacheDType(const HfConfig& config, vt::DType conv_dtype) { const std::string& dtype = config.mamba_ssm_dtype; @@ -7698,17 +7719,11 @@ detail::ExpertStreamStepScope::~ExpertStreamStepScope() { // (the multimodal helper embeds a prompt and then single tokens); consuming on // first use means those cannot be handed ids that were never meant for them. // The first embed in a registry forward is always the step's own. +// #1305: the take-and-clear and the bounds-checked copy this used to spell out +// are `detail::ApplyDeviceTokenIds` (defined above, declared in +// `qwen3_5_internal.h`), one body for the four models that consume the scope. static void ApplyDeviceTokenIdsOverride(Dev d, DBuf& dids, int64_t T) { - const detail::DeviceTokenIds ov = detail::DeviceTokenIdsOverride(); - if (ov.ids == nullptr) return; - detail::DeviceTokenIdsOverride() = detail::DeviceTokenIds{}; - // A device buffer LONGER than the embed's input would run past the end. That - // can only mean the runner and the model disagree about this step's shape, so - // fail loudly rather than corrupt the embedding. - VT_CHECK(ov.count <= T, - "qwen3_5 embed: device input ids longer than the embed input"); - d.b.Copy(d.q, dids.ptr(), ov.ids, - static_cast(ov.count) * sizeof(int32_t)); + detail::ApplyDeviceTokenIds(d.b, d.q, dids.ptr(), T, "qwen3_5 embed"); } // Embed: hidden[T,H] bf16 = embed_tokens[token_ids] (device-resident table). diff --git a/src/vllm/model_executor/models/qwen3_5_internal.h b/src/vllm/model_executor/models/qwen3_5_internal.h index 1e0bde29d..3996db20c 100644 --- a/src/vllm/model_executor/models/qwen3_5_internal.h +++ b/src/vllm/model_executor/models/qwen3_5_internal.h @@ -12,6 +12,7 @@ namespace vt { struct Queue; +class Backend; } // namespace vt namespace vllm { @@ -450,6 +451,43 @@ struct DeviceTokenIdsScope { DeviceTokenIds prev; }; +// ─── THE CONSUMER SIDE, ONCE (#1305) ──────────────────────────────────────── +// +// The scope above publishes. Reading it back is four lines of take-and-clear +// plus five of bounds-checked copy, and until #1305 every model that consumed it +// wrote its own pair: `qwen3.cpp`, `qwen3_5.cpp`, and then — in a row whose +// stated purpose is deleting hand-rolled copies — `qwen3_moe.cpp` and +// `deepseek_v2.cpp` as a third and fourth. A fresh review named that, and this +// is the answer: one declaration here beside the publisher, one definition in +// `qwen3_5.cpp` beside `DeviceTokenIdsOverride()`, four call sites. + +// TAKE the published override and CLEAR it, so the FIRST embed in a forward is +// the one that gets it. A forward can reach a second, unrelated embed — the +// multimodal generate helper embeds a prompt and then single tokens — and +// consuming on first use means those cannot be handed a row count that was never +// meant for them. Returns a null `ids` when no override is live, which is every +// path except the asynchronous CUDA runner. +DeviceTokenIds TakeDeviceTokenIds(); + +// TAKE the override and SPLICE it over an embed's device identifier buffer. +// `dst` holds `dst_count` int32 identifiers that a host upload has already +// filled; the override replaces its first `ov.count` rows. That is right for the +// PADDED graph case, where only the real prefix is patched and the inert tail +// must keep the host vector's values, and it degenerates to "replace everything" +// on the eager path where `ov.count == dst_count`. +// +// The copy is enqueued on `queue`, so it is ordered AFTER the runner's combine +// that produced the source rather than racing it — which is the whole point, and +// the reason a host read of `ModelForwardInput::token_ids` cannot substitute. +// +// An override LONGER than `dst_count` can only mean the runner and the model +// disagree about this step's shape, so it throws with `what` naming the caller +// rather than embedding past the end. Returns true when an override was applied, +// false when none was live — in which case nothing is written and the caller is +// byte-identical to its pre-#1305 self. +bool ApplyDeviceTokenIds(vt::Backend& backend, vt::Queue& queue, void* dst, + int64_t dst_count, const char* what); + // ─── ENG-EXPERT-STREAM (#912): the streamed-expert lane, seen from outside ─── // // The lane lives in the anonymous namespace of qwen3_5.cpp because nothing diff --git a/src/vllm/model_executor/models/qwen3_moe.cpp b/src/vllm/model_executor/models/qwen3_moe.cpp index 2ad8d3cbf..84e51d8f6 100644 --- a/src/vllm/model_executor/models/qwen3_moe.cpp +++ b/src/vllm/model_executor/models/qwen3_moe.cpp @@ -117,20 +117,18 @@ void GatherRows(Dev d, void* dst, const Tensor& src, const std::vector& // and it consumes the HOST token_ids. The graph driver runs this per step into // its PERSISTENT hidden buffer, then captures/replays ForwardLayers over that // fixed hidden address. -// #1305 — CONSUME the registry's scoped device-id override, once per forward. +// #1305 — THE SCOPED DEVICE-ID OVERRIDE is consumed here, once per forward. // `ForwardQwen3MoeForCausalLM` publishes `ModelForwardInput::device_token_ids` // through `detail::DeviceTokenIdsScope`; it is null on every path except the // asynchronous CUDA runner, where the combine has already spliced each decode // row's sampled token into the DEVICE identifiers and left the host vector -// deliberately stale. Taking it CLEARS it, so the first embed in a forward is -// the one that gets it and a second, unrelated embed cannot be handed another -// step's rows. -detail::DeviceTokenIds TakeDeviceTokenIds() { - const detail::DeviceTokenIds ov = detail::DeviceTokenIdsOverride(); - if (ov.ids != nullptr) detail::DeviceTokenIdsOverride() = detail::DeviceTokenIds{}; - return ov; -} - +// deliberately stale. Both readers below — +// `detail::TakeDeviceTokenIds` on the graph path and +// `detail::ApplyDeviceTokenIds` on the eager one — CLEAR it, so the first embed +// in a forward is the one that gets it and a second, unrelated embed cannot be +// handed another step's rows. Both live in `qwen3_5_internal.h` beside the scope +// that publishes them, because four models consume it and each used to spell the +// same two bodies out for itself. // EMBED FROM AN ALREADY-RESIDENT ID TENSOR. The decode-graph driver holds its // identifiers in a `vllm::StepTokenIds` whose device address is stable for the // life of the slot, so this arm takes the tensor instead of re-uploading a host @@ -150,13 +148,7 @@ void EmbedInto(Dev d, DBuf& hidden, const std::vector& token_ids, // they embedded the host vector and never looked at the device mirror. The // override's copy is enqueued on the main queue, so it is ordered AFTER the // combine that produced it rather than racing it. - const detail::DeviceTokenIds ov = TakeDeviceTokenIds(); - if (ov.ids != nullptr) { - VT_CHECK(ov.count <= T, - "qwen3 moe embed: device input ids longer than the embed input"); - d.b.Copy(d.q, dids.ptr(), ov.ids, - static_cast(ov.count) * sizeof(int32_t)); - } + detail::ApplyDeviceTokenIds(d.b, d.q, dids.ptr(), T, "qwen3 moe embed"); EmbedInto(d, hidden, dids.t(), weights, config); } @@ -547,7 +539,7 @@ ForwardLogits Qwen3MoeDecodeGraph::Step( // enqueued on the main queue, so the second is ordered after the combine // instead of racing it, and all three arms below embed from the SAME stable // address. - const detail::DeviceTokenIds ov = TakeDeviceTokenIds(); + const detail::DeviceTokenIds ov = detail::TakeDeviceTokenIds(); s.ids.Ensure(d, S); s.ids.Refresh(d, s.token_ids, ov.ids, ov.count); if (cols_changed && s.graph.captured()) { From 91087e670d32e6779d93456b72a06d6ec199657f Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 19 Aug 2026 21:46:27 +0000 Subject: [PATCH 7/9] record(ENG-CUDAGRAPH-BREAK): #1305's eager half closes and its graph 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] --- .agents/engine-matrix.md | 2 +- .agents/specs/eng-cudagraph-break.md | 115 +++++++++++++++++++++------ 2 files changed, 90 insertions(+), 27 deletions(-) diff --git a/.agents/engine-matrix.md b/.agents/engine-matrix.md index 66e18e9ce..8b87d04a0 100644 --- a/.agents/engine-matrix.md +++ b/.agents/engine-matrix.md @@ -61,7 +61,7 @@ forensics: roadmap_v1.md and the parity ledger. | `ENG-PREEMPT-RECOMPUTE` | FCFS tail preemption with recompute | T0 | `vllm/v1/core/sched/scheduler.py:1142`; `tests/v1/core/test_scheduler.py:930` | `src/vllm/v1/core/sched/scheduler.cpp:102,157`; `src/vllm/v1/core/sched/request_queue.cpp:36` | `tests/vllm/v1/test_scheduler.cpp:247,295`; `tests/vllm/v1/test_request_queue.cpp:91` | `planned: specs/preemption.md` | `ANCHOR-BACKFILL` | - | | `ENG-CUDAGRAPH` | Decode graph capture/replay modes (host-cluster cleanup: capture-size set derived from `max_num_seqs` mirroring vLLM `_set_cudagraph_sizes`; 2026-07-18 graph-baked-scratch use-after-free fix — the 35B c2+ online-serving IMA blocker) | T0 | `vllm/config/compilation.py:53,1319,683-684,1438-1444`; `vllm/config/vllm.py:1667-1770`; `vllm/v1/worker/gpu/cudagraph_utils.py:116`; `tests/compile/test_config.py:122,229` | `src/vt/cuda/cuda_backend.cu:76,97,105`; `include/vllm/model_executor/models/decode_graph_sizes.h`; `src/vllm/model_executor/models/qwen3_5.cpp:3754,3952`; `src/vllm/v1/worker/gpu/runner.cpp:577,597`; graph-safe scratch (retire-on-grow so graph-baked scratch pointers stay valid) `src/vt/cuda/graph_safe_scratch.h`, `src/vt/cuda/cuda_moe_marlin.cu:75`, `src/vt/cuda/cuda_matmul_nvfp4.cu:766`, `src/vt/cuda/cuda_matmul_nvfp4_cutlass.cu:105`, `src/vt/cuda/cuda_matmul_fp8_cutlass.cu:95` | `tests/vt/test_cuda_backend.cpp:98`; `tests/vllm/models/test_decode_graph_sizes.cpp`; `tests/vt/test_graph_safe_scratch.cpp`; explicit 35B gate `tests/parity/test_qwen36_paged_engine.cpp:140` | [blocktable-host-cluster-cleanup.md](specs/blocktable-host-cluster-cleanup.md); [decode-graph-scratch-uaf-2026-07-18.md](specs/decode-graph-scratch-uaf-2026-07-18.md) | `PARTIAL` | **PREFILL capture REFUTED as a lever (2026-08-17, [#1161](https://github.com/mudler/vllm.cpp/issues/1161)).** vLLM's v1 default already captures prefill piecewise (`vllm/config/compilation.py:60-63,615,630` @ `555967922`) and it is in our denominator; SGLang reached the same coverage without `torch.compile` via BCG (`SGLANG-BCG` in [sglang-matrix.md](sglang-matrix.md)). Neither helps us: GB10 2026-07-09 measured prefill GPU-idle-between-launches at **3.8%** with GPU-busy >96% on both arms, and the 27B prefill gap at **92.5% non-GEMM glue GPU work** with the dominant GEMM at +0.17% and attention AHEAD. There are no launch bubbles in our prefill to collapse. Row stays `PARTIAL`; the real residuals are exec dedup ([#1162](https://github.com/mudler/vllm.cpp/issues/1162)) and the break-point seam ([#1163](https://github.com/mudler/vllm.cpp/issues/1163)). Spec [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | | `ENG-CUDAGRAPH-DEDUP` | Graph-executable dedup: hash each captured graph's topology and re-point ONE `cudaGraphExec` with `cudaGraphExecUpdate` on a signature hit, instead of instantiating one exec per padded bucket per model. A memory and capture-time change, NOT a throughput change — a deduped replay launches the same nodes, and the load-bearing gate is byte-identity rather than a ratio | T2 | vLLM has no analogue (its execs come from `torch.compile`, `vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); secondary oracle SGLang `python/sglang/srt/model_executor/runner_backend/cuda_graph_dedup_mixin.py:27-37,105-179,219-242,258-275,353-358` @ `f63458b5be` ([oracles/sglang.md](oracles/sglang.md)) | W1+W2 landing here behind `VT_CUDA_GRAPH_DEDUP`, default OFF until the device A/B measures the per-switch update cost: a device-agnostic dedup registry shared by both accelerator backends plus one CUDA/HIP ops table written once, wired into `EndCaptureGraph`/`ReplayGraph`/`DestroyGraph`. Baseline it replaces: `src/vt/cuda/cuda_backend.cu:222-232` instantiates a fresh exec per capture and destroys the raw graph, over the 7 (`max_num_seqs=32`) or 11 (64) buckets of `include/vllm/model_executor/models/decode_graph_sizes.h:32-41`, times NINE drivers (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight, missing the DFlash draft graph `src/vllm/model_executor/models/qwen3_dflash.cpp:771,870,1038,1091,1095,1106`) | `tests/vt/test_graph_dedup.cpp` 13/13 cases, 65 assertions, RED-first (written and run against an absent header, and the four cases added by the fresh review of #1178, three of them run against the unfixed source) and gated on every platform via a fake ops table whose launch log makes "the right nodes ran" an observable sequence over MORE than one replay per shape; 13/13 negative mutations detected (9 at implementation, 4 at review repair). That count covers `src/vt/graph_dedup.h` ONLY. `src/vt/graph_dedup_runtime.h` had NO executable coverage on any tier, and [#1184](https://github.com/mudler/vllm.cpp/issues/1184) is what hid in that gap: the file is DESIGNED to see runtime calls fail — a refused `cudaGraphExecUpdate` probe is the feature working — and never consumed the runtime's latched error, so the next unrelated kernel reported the refusal as its own failure and every `VT_CUDA_GRAPH_DEDUP=1` run died 6/6 on GB10 as `greedy_argmax launch: invalid device function` from a launch that had succeeded. Repaired structurally rather than at twelve sites: the clear lives in `ScopedLatchClear`'s destructor (`src/vt/graph_dedup_latch.h`) installed at the six `GraphDedupOps` entry points by `MakeLatchGuardedOps`, the table's only constructor, so no raw function address reaches a field and an unwired seventh operation leaves a null the registry refuses; one line covers CUDA and HIP. The device-free half of the signature walk moved to `src/vt/graph_dedup_signature.h` and is gated by `tests/vt/test_graph_dedup_runtime.cpp` 13/13 cases, 51 assertions, RED-first against the pre-fix guard (22 failed assertions reproducing the production message), 7/7 negative mutations detected — Kahn ordering, topological re-index, sorted edge emission, the depth-4 child bound and the four graph-level escapes. STILL compile-gated only: the five node-payload cases behind the device policy. **DEVICE A/B DELIVERED 2026-08-18 on `dgx:gpu0` (GB10, driver 580.173.02, nvcc 13.0.88, `rc` job f88d484b), and it SPLIT.** Gated commit `72de552c8`, whose four dedup sources are byte-identical to the merged `2a976eb9f` — the row squashed, so the gated tree is not an ancestor of the merge and that sha equality is what carries the claim. CORRECTNESS PASSES: 12/12 cells exit 0, zero `invalid device function` and zero `engine-fatal` in every cell log where the pre-fix head `e4ce5571a` died after exactly one replay, ON replays as often as OFF (60=60, 33=33, 43=43), and `--output-token-ids` is IDENTICAL over 10/10 comparisons with the three OFF/OFF controls passing FIRST and the three workloads hashing to three DIFFERENT values, so the identity is not vacuous. #1184 is closed by this run, because a CPU suite drives a fake runtime and cannot observe the real latched error. THE BENEFIT IS REFUTED for the case this row was filed for: `N == M` in every ON cell — 3 graphs to 3 execs on sizes [24 16 8], 2 to 2 on [16 8], 2 to 2 on [32 24] — with the registry's count CLIMBING 1→1, 2→2, 3→3, so more than one capture reached it and the 1:1 is a measurement rather than the single-capture artefact the first attempt produced. Cause pre-registered before the run and then confirmed, structural rather than a tuning miss: `AppendKernelPayload` hashes (`func`, `gridDim.{x,y,z}`, `blockDim.{x,y,z}`, `sharedMemBytes`) at `src/vt/graph_dedup_runtime.h:121-128` and the memcpy payload hashes the copy extent, so the padded batch dimension sits in the KEY, no candidate group ever forms and `cudaGraphExecUpdate` is NEVER ATTEMPTED. That contradicts this row's own premise — `graph_dedup.h`'s header says the fold is for "two padded batch sizes … the same node topology with different parameters" — and SGLang keys the same fields (`cuda_graph_dedup_mixin.py:105-114`), so whatever folds upstream is not decode buckets either. NO throughput or memory number is recorded: clocks unpinned AND the ON arm allocated exactly as many executables as OFF. Honest gaps: per-shape replay counts are unavailable (the driver prints a TOTAL, so B's ~30-per-shape is arithmetic); the driver's "N captured size(s)" counts SLOTS not captures (A reports 6, emits 3); the container's own cuBLASLt was never re-tested at CUDA 13.0 because the staged cu130 prefix was probed first and worked; only the Qwen3 dense decode driver was exercised. STILL OWED: the default flip, now NOT JUSTIFIED on this evidence rather than merely ungated; a COARSER key that could group two decode buckets at all, which the probe-before-fold design makes a cost question rather than an obviously unsafe one ([#1226](https://github.com/mudler/vllm.cpp/issues/1226), the next traceable hypothesis, deliberately NOT decided by this record); device-tier signature stability/discrimination tests; probing `current_raw` instead of `raws.front()` to retire the update-transitivity assumption; the ROCm compile; a supporting `orin:gpu0` leg, BLOCKED because the Jetson 540.4.0 driver cannot run a CUDA 13 runtime (`cudaGetDeviceCount err=35`); and reaching the feature from the default serving path at all — the async runner captures no decode graph, **W5, THE SAME DAY, CONFIRMED THE HYPOTHESIS THAT NEGATIVE PRODUCED ([#1226](https://github.com/mudler/vllm.cpp/issues/1226) DELIVERED).** Same box, `rc-worker-4b8lj`, boot_id `3fd9745a-d25a-426c-ba3c-97c958a85515` at both ends, GB10, driver `580.173.02`, `### DONE_AB_KEY 2026-08-18T20:58:46Z`, binary sha256 `ca114abb…c772ad` from `b48b51df1` (tar sha256 asserted before extraction). Drop the launch dimensions and the memcpy extents from the key and every bucket folds: `a_coarse` 3 graphs to 2 execs, `b_coarse` 2 to 1, `c_coarse` 2 to 1, each `probes=1 refused=0`, against `probes=0 refused=0` in every EXACT cell. **`probes=0` in the EXACT cells is the direct process-level proof of W4's source-level diagnosis** — with the launch dimensions in the key no candidate group forms and `cudaGraphExecUpdate` is never asked; drop them and it is asked once per fold and ACCEPTED EVERY TIME. The saving W4 recorded as unreachable is reachable via the key. Byte-identity holds on A (five cells, `59ebff4a…`) and C (four cells, `ff205260…`). **Workload B is VOID rather than a pass, and its cause is a NEW DEFECT that is not this row's:** the two `VT_CUDA_GRAPH_DEDUP`-unset control cells DISAGREED (`5973c5a1…` 2638 bytes vs `4cf79230…` 2650 bytes) on one binary, one workload, greedy `--temperature 0 --seed 777` at `--concurrency 16`, 23 s apart — 672 tokens both, so the byte delta is JSON width and not a length; exactly rows 17 and 18 of 21 differ, both mid-decode, both in the ragged tail `21 % 16` leaves. B's `b_off_a == b_exact` and `b_off_a == b_coarse_a` therefore compare against a baseline that does not reproduce itself and are WORTHLESS; only the OFF/OFF control made that visible, and without it B would have read as three more confirmations. Filed [#1283](https://github.com/mudler/vllm.cpp/issues/1283). **Caveats that bound this result:** nvcc was `13.3.73` here and `13.0.88` for the W4 baseline the recorded dgx gate stack names, so the OFF-vs-ON and EXACT-vs-COARSE comparisons WITHIN this binary are valid while this run and that baseline are NOT directly comparable; clocks unpinned (2405 MHz current, 3003 max, 2418 applications) and nothing measured bytes, so NO throughput and NO memory number is claimed or implied; only the Qwen3 dense decode driver was exercised; `refused=0` is ONE driver on ONE hardware and toolkit pair, which is no more a floor than W4's negative was a ceiling; and the coarse key is behind `VT_CUDA_GRAPH_DEDUP_COARSE_KEY`, default OFF, inside a default-OFF flag, on **PR [#1232](https://github.com/mudler/vllm.cpp/pull/1232) which is STILL A DRAFT — nothing on `main` folds today.** **Row stays `ACTIVE`, argued:** not `DONE`, because the fold is unreachable on every shipping configuration and the row's stated MEMORY saving has never been measured in bytes on either key; not `PARTIAL`, because nothing upstream is omitted — the coarse key is our own extension past SGLang, which keys the fields we started from; not `BLOCKED`, because nothing external stops the next step. What is owed is now a DECISION about the default plus the byte measurement and the probe-cost-at-real-churn measurement it needs, and landing #1232 first **W6, 2026-08-19, THE DEVICE-BYTE MEASUREMENT — THE BENEFIT QUESTION IS NOW CLOSED AND THE ANSWER IS NEGATIVE.** Tested `origin/main` `2c8f53d93`, which is PR #1232 LANDED, so the "nothing on `main` folds today" caveat every earlier record carried is RETIRED and this measures a configuration that ships. Same box, `rc` job `93f783de`, pod `rc-worker-4b8lj`, boot_id `3fd9745a-…` at BOTH ends, GB10, driver `580.173.02`, nvcc **13.0.88** (the W4 baseline toolkit; W5 ran 13.3.73, so W6 and W5 are NOT directly comparable while comparisons WITHIN this one binary are valid), binary sha256 `be697268…0ce657a7`, `### DONE_BYTES 2026-08-19T04:57:19Z`, 12/12 cells exit 0, zero VOID markers. **THE FOLD ENGAGES AT THE SHIPPED BUCKET SET**, which is the churn W5 could not produce: `vllm-bench` sets `max_num_seqs = concurrency`, so W32 captured `[1 2 4 8 16 24 32]` 7-of-7 and W64 captured `[1 … 64]` 11-of-11, exactly `decode_graph_sizes.h:32-41`, against the 2-3 buckets every earlier conclusion was drawn from. COARSE folds 7 graphs to 3 execs (`probes=7 refused=3`) and 11 to 5 (`probes=22 refused=16`); EXACT folds NOTHING at `probes=0`, reproducing W4 at four times the bucket count. Token ids byte-identical across every cell of a workload INCLUDING both OFF/OFF controls (`ff0db6c6…be9d` 11720 B; `e1cbf5fc…e5d0` 57620 B) — neither workload has #1283's ragged-tail shape and neither hit it. **THE SAVING DOES NOT SURVIVE ITS OWN NULL CONTROL.** `nvidia-smi --query-compute-apps` tail median (the `--query-gpu=memory.used` axis returns `[N/A]` on this box) shows W64 IDENTICAL to the megabyte in all five cells (9737) and W32's coarse arm reading 10-23 MiB HIGHER than OFF (3252/3262 vs 3262/3275). A `cudaMemGetInfo` shim summed over every instantiate gives a nominal 13.83 MiB at 7 buckets — **0.42% of a 3.25 GiB process** — and **−0.75 MiB, i.e. NOTHING, at 11**. That nominal effect is NOT ESTABLISHED on four independent grounds: `EXACT` is a TRUE NULL (same 7 and 11 retained execs, `probes=0`, so it allocates what OFF allocates) and disagrees with OFF by 10.6-13.1 MiB against a 13.83 MiB candidate; the W64 OFF/OFF pair disagrees with ITSELF by 18.2 MiB; one instantiate recorded a NEGATIVE delta (`-5,165,056` B); and `cudaGraphExecDestroy` reclaimed `0` in EVERY cell. Per-instantiate deltas for byte-identical 404-node graphs span 0 to 10,514,432 B and 17 of 27 instantiates in one cell read exactly zero, so these are POOL-GRANULAR readings and the coarse arm's throwaway probes grow that pool exactly like retained execs do. What CAN be priced: one ~390-node executable at **2.08-4.35 MiB**, 10.0-10.6 KB per node — the figure to re-run on a deep checkpoint. **THE MECHANISM INVERTS THIS ROW'S PREMISE.** The driver refuses **43% of probes at 7 buckets and 73% at 11**, every one of them `probe refused a fold (err=910 result=2)` = `cudaErrorGraphExecUpdateFailure` / `cudaGraphExecUpdateErrorTopologyChanged`. The shim's `cudaGraphGetNodes` reading says why false candidates form: the decode graphs are **TWO topologies, 376 and 404 nodes**, mixed across the buckets (`w32_off_a` captured `404 404 376 376 404 404 404`). Every refusal is about TOPOLOGY, never a parameter, so a COARSER key produces MORE false hits rather than more folds — the opposite of what W5's 2-bucket A/B suggested, and W5's `refused=0` is now explained as an artefact of workloads whose buckets only ever SHRANK, so exactly one pair was ever presented. **COST:** W32 OFF 7 instantiates / 0 updates vs COARSE 10 (3 retained + 7 probes) / 11 updates; W64 OFF 11 / 0 vs COARSE **27** (5 retained + 22 probes) / 28 updates — **2.45x the instantiate calls** to retain 6 fewer executables. **Peak transient did NOT double** — in every ON cell live-bytes peak == end, because `Register` destroys the probe before returning, so the feared "double the peak to save the steady state" trade did not occur. **A replay-time re-point DID occur** — 4 and 6 non-probe updates over 88 and 244 replays, ARITHMETIC over two printed totals and not a counter — with every cell exiting 0 and byte-identical, so `Replay`'s transitivity assumption neither aborted nor changed a token; W5 recorded that case as untested. **CAVEATS THAT BOUND THIS RESULT:** the clock pin was **REFUSED inside the lease** (`The current user does not have permission to change clocks for GPU 0000000F:01:00.0`, `clocks_pinned=0`), so **NO time-based figure is attributable** and the instantiate-wall and update-wall figures in `bytes.log` are diagnostics quoted nowhere as a result; `result=2` is ONE driver, ONE GB10, ONE toolkit; only the Qwen3 dense decode driver was exercised, as in W4 and W5; `VT_ASYNC_RUNNER=0` throughout, so the feature is STILL unreachable on the DEFAULT serving path (#1179); and `cudaMemGetInfo` cannot separate an executable's own cost from the pool chunk that satisfied it. **VERDICT, DELIVERED AND NEGATIVE:** `VT_CUDA_GRAPH_DEDUP` stays default OFF, now on MEASUREMENT rather than on silence; `VT_CUDA_GRAPH_DEDUP_COARSE_KEY` alone is a **NO-OP, not merely unsupported** — `GraphDedupCoarseKeyEnabled()` (`src/vt/graph_dedup.h:114`) is read only by the signature builder (`src/vt/graph_dedup_runtime.h:177`), only from `Register`, only under `GraphDedupEnabled()` (`src/vt/cuda/cuda_backend.cu:237`), so with dedup off its sole observable is one stderr line; both on is unsupported. **NOT A CEILING.** Three things would change it and each is traceable: find where the 376/404 split comes from (the FA-2 split-KV grid is the first suspect — a capture that fixes the node set across buckets removes every refusal); an instrument that resolves a single 2-4 MiB executable against driver pool granularity (`cuMemGetAllocationGranularity` or a pool-statistics query); and the same measurement on a 60-80 layer checkpoint, where bytes scale with node count. **Row STAYS `ACTIVE`, argued, and the argument is now narrow.** The MEASUREMENT obligations are discharged and the DECISION is delivered, which is the `DONE` case and it is a real one. Three things stop the flip and none is a checker technicality: the feature is unreachable on the DEFAULT serving path, owned by `ENG-CUDAGRAPH-BREAK` (#1179) and the "nothing lands dead" half of this row; two items still sit under #1162 itself — the device-tier signature stability/discrimination tests and probing `group.current_raw` instead of `raws.front()` to retire the transitivity assumption; and the `DONE` record surface owes a `.agents/parity-ledger.md` entry, a closing-commit owner in place of the claim, an exact test anchor and the RELEASE of `CLAIM-ENG-CUDAGRAPH-DEDUP`, which is an operator act and which this record-only branch does not own. Not `PARTIAL` — nothing upstream is omitted. Not `BLOCKED` — nothing external stops the next step. Full evidence: [benchmark-record.md](benchmark-record.md) entry `ENG-CUDAGRAPH-DEDUP W6`, raw at `/mnt/nas_share/rc/dedup-bytes/` | [eng-cudagraph-dedup.md](specs/eng-cudagraph-dedup.md); analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-DEDUP` ([#1162](https://github.com/mudler/vllm.cpp/issues/1162)) | -| `ENG-CUDAGRAPH-BREAK` | One shared `vt` capture seam that accepts BREAK POINTS, so a forward containing a host-dependent op is still graphed instead of falling out entirely — and so the NINE hand-rolled drivers become one (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight). **Coverage AND CORRECTNESS row, not a throughput row** | T1 | mirror vLLM `CUDAGraphMode.PIECEWISE` splitting at `splitting_ops` (`vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); construction from SGLang BCG `python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/breakable_cuda_graph.py:204-243,246-274,309-333,335-367` @ `f63458b5be` (decorator + runtime stream capture, no compiler); its unit suite `test/registered/cuda_graph/breakable/test_breakable_cuda_graph.py:30,172,230` (305 lines, 11 unit cases) is mapped case for case in the spec's `## Tests to port` | **W6 MOVED THE PREDICATE** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374), 2026-08-19): `GPUModelRunner::execute_model` names the step's ACTUAL uniform query length once through `v1::GraphEligibleQueryLen` (`src/vllm/v1/worker/gpu/cudagraph_dispatch.h`, INERT with no caller since #442 and now called from production) and ships it on `ModelForwardInput::uniform_query_len`; the two Qwen3.5 registrations stop re-deriving that test in twenty duplicated lines each, and both key their slot ring on `(S, q, spec)`. [#1020](https://github.com/mudler/vllm.cpp/issues/1020) CLOSES on the pair, and the key half was a LIVE collision rather than the enabler #1020 called it: `S = spec_step ? B : PadToCaptureSize(B)` puts a 4-request spec step at 1+1 tokens and an 8-request padded decode on the same `S == 8` at the base commit. The widening is BOUNDED by `VT_SPEC_GRAPH_MAX_QLENS` (default 2), because reading the actual length multiplies the spec shape ceiling by `1 + k`. Seven of the nine drivers still read `pure_decode` and are byte-identical. **What did NOT move is 'except at the break points'**: no driver in this tree serves a prefill or a mixed batch under any predicate, so that needs a prefill capture driver nobody has written and whose benefit D5 already refutes on this hardware — a publishable negative, recorded in the spec's `## Owed` as a row-level item. The pre-W6 baseline it replaces: all-or-nothing, `src/vllm/v1/worker/gpu/runner.cpp:1338-1341` routing only `pure_decode`; drivers `qwen3_5.h:275`, `qwen3_5_dense.h:391`, `qwen3_moe.h:117`, `qwen3.h:243`, `deepseek_v2.h:324`, `voxtral.h:126`, plus `deepseek_v4.cpp`, `laguna.cpp` — and the spike found the NINTH already written, `src/vllm/model_executor/models/qwen3_dflash.cpp:771,1091`. The re-derivation is measured, not asserted: `StepDevInputs` (`src/vllm/model_executor/models/qwen3_5.cpp:3894`, the persistent DEVICE input path) exists in ONE driver and `grep -c` returns 0 in `qwen3_moe.cpp`, `qwen3.cpp`, `deepseek_v2.cpp` and `voxtral.cpp`, which is why `src/vllm/model_executor/models/qwen3.cpp`'s `DenseDecodeGraphForward` DECLINES the graph outright when the async device-token mirror is live. **That decline is why this is also a CORRECTNESS row** ([#1179](https://github.com/mudler/vllm.cpp/issues/1179)): a SHIPPED model has already lost its decode graph to the duplication, on the driver's own measurement (`depth-1, graph ON PASS 78/78`; `depth-2, graph OFF PASS 82/82`; `depth-2, graph ON FAIL, slots 1-3 degenerate`), and the fix its comment names is the sibling's `StepDevInputs`. The row still makes NO throughput claim: the prefill refutation on the `ENG-CUDAGRAPH` row (3.8% host idle, >96% GPU-busy, 92.5% glue) stands unchanged; **#1305 CLOSED, and reading the tree found a larger defect than the issue described** (2026-08-19): `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp` never constructed a `detail::DeviceTokenIdsScope` and neither `qwen3_moe.cpp`'s nor `deepseek_v2.cpp`'s `EmbedInto` ever consulted one, so `ModelForwardInput::device_token_ids` reached NOTHING in either translation unit — the decode graph AND both eager arms embedded the host vector the runner's mirror arm deliberately leaves stale for decode rows. The three registries now publish the scope (the mechanism `qwen3.cpp`, `qwen3_5.cpp`, `mistral_registry.cpp`, `internlm2_registry.cpp` and `llama_registry.cpp` already use), and each decode-graph size slot holds a `vllm::StepTokenIds` (`include/vllm/model_executor/models/step_token_ids.h`) whose destination is a device buffer with a stable address, refreshed through `vt::PersistentStepInput` — host arm for the padded vector, DEVICE arm over the real prefix, both on the main queue so the second is ordered after the combine rather than racing it. That is `vt::PersistentStepInput::RefreshFromDevice`'s FIRST production caller, retiring the staged slice W4 landed with none, and it is the fix `qwen3.cpp`'s own decline comment names rather than a fifth private copy. `qwen3.cpp`'s decline is UNTOUCHED: W4 measured its recorded cause false and its real one is unidentified. | owed: bit-exactness vs eager on every migrated model over MORE than one replay, on a real GPU — **W2 did NOT meet it and says so**: no `rc` lease was obtainable in its window and a CPU harness cannot replay a captured segment, so it moves to W3 with the three drivers of the same shape (G1); the host-lifetime contract of `decode-graph-scratch-uaf-2026-07-18.md` enforced AT the seam — D1's INPUT half, making the intermediates a segment reads unavailable to the `DevicePool` free list, which becomes live only for the first PIECEWISE production capture (W4); the auxiliary-stream auto-join before every segment close (`:353-361`, spec D10), live at `src/vllm/model_executor/models/qwen3_5.cpp:6254-6255,6384` and `src/vllm/model_executor/models/laguna.cpp:2572-2576,2612` (W4, W5). **Delivered by W1** ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the reachability mutation (performed; deleting the call site reds `tests/vllm/models/test_qwen3_break_point.cpp` and leaves the unit suite green); the ported SGLang unit cases with their arithmetic chains and post-replay assertions; and the break-function OUTPUT writeback (`replay_fn`/`_copy_output` `breakable_cuda_graph.py:231-235,172-201`, spec D9), whose destination is a `vt::BreakSlot` the seam owns rather than a caller reference it cannot outlive **W6 gates** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): G2 at THREE levels because the claim has three parts — the engine (`tests/vllm/v1/spec_decode/test_mtp_depth.cpp`, a real LoadedEngine/EngineCore/Scheduler/runner stack, asserting `clamped_spec_steps`, measured 0/0/1/2/4 at k=1/2/3/4/6), the driver (`tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp`, two spec shapes of equal S and different q getting two rings and two captures), and the arithmetic (`tests/vllm/v1/worker/gpu/test_cudagraph_dispatch.cpp`). Five detecting mutations, each reddening ONE level and leaving the others green, plus an over-fire control. A SIXTH mutation was NOT detected and forced a repair: the per-request verify conjunct is redundant on every model that reads the field (both are GDN hybrids whose prefill trips the first conjunct), so it moved into `GraphEligibleQueryLen` where a mutation reds 4 assertions, and the spec records it as unreached defence in depth. **G1 re-run on `thor:gpu0` (sm_110, driver 595.78, nvcc 13.0.88): 2066 assertions, 0 failed, 0 differing on all five migrated drivers — W6 moves no logit.** The ring key's own device case is BLOCKED by [#1380](https://github.com/mudler/vllm.cpp/issues/1380), a pre-existing `cudaMalloc` inside a capturing stream on the spec arm that W6 neither caused nor regressed; the case PINS that refusal and is written to fail when #1380 is fixed.; **#1305 (2026-08-19)**: `tests/vllm/models/test_moe_async_device_ids.cpp`, entered at `ModelRegistry::Forward` over a synthetic safetensors checkpoint for `Qwen3MoeForCausalLM` and `DeepseekV2ForCausalLM` — the production entry point, not the driver type. Three runs each: right host ids and no mirror as the reference, stale host ids and no mirror as the CONTROL that must differ, stale host ids with the truth reaching the model ONLY through `device_token_ids` as the gate. RED first at 2 cases / 65 assertions / 10 failed / exit 1, with 800 of 800 logit values differing over four steps on both architectures and every counter at 0; GREEN after at 65 of 65, exit 0. TWO mutations, each compiled clean and each restored by sha256: deleting the registry's scope line — the production call site — reds 4 assertions across both cases and puts all 800 values back, and swapping the seam's DEVICE arm for its HOST arm leaves the logits BIT IDENTICAL at 0 of 800 differing and reds only `device_refreshes` and `host_refreshes`, which is the arm no token gate can see. Neighbours green on the same binary: `test_qwen3_moe_decode_graph_seam` 228 of 228, `test_deepseek_v2_decode_graph_seam` 230 of 230, `test_qwen3_decode_graph_seam` 231 of 231, `test_voxtral_decode_graph_seam` 230 of 230, `test_breakable_graph` 265 of 265, `test_persistent_step_input` 66 of 66, `test_model_registry` 924 of 924, `test_qwen3_moe_forward` 504 of 504, `test_deepseek_v2_forward` 1052 of 1052. **NOT measured:** the depth-2 four-concurrent battery on a device, which needs a GPU and a real checkpoint; owed. **Found red on `main` and NOT caused here:** `test_qwen3_5_decode_graph_seam` exits 139 while its assertion line reads 135 of 135 passed ([#1390](https://github.com/mudler/vllm.cpp/issues/1390)). | spec [eng-cudagraph-break.md](specs/eng-cudagraph-break.md) (W0 spike DONE 2026-08-18: the existing `vt` capture vocabulary `include/vt/backend.h:208-222` expresses a SEGMENTED capture with NO new virtual, because `EndCaptureGraph` stores nothing (`src/vt/cuda/cuda_backend.cu:225-232`); a break point is expressible with one `thread_local` capture pointer plus a free function, no compiler and no decorator); **W1 DONE 2026-08-18 ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the seam LANDS** — `vt::BreakableGraph`, `vt::GraphCaptureScope` and `vt::GraphBreak` (`include/vt/breakable_graph.h`, `src/vt/breakable_graph.cpp`), the SGLang unit suite ported case for case (`tests/vt/test_breakable_graph.cpp`, 24 cases / 163 assertions, re-derived 2026-08-18 by `ninja test_breakable_graph && ./build/tests/test_breakable_graph`; the recorded 14/81 never re-derived at any head of this branch), and ONE break point registered at the DENSE ATTENTION ENTRY of `Qwen3ForCausalLM` (`src/vllm/model_executor/models/qwen3.cpp`, inside `RunLayer`). **The exit criterion W0 deliberately left open is ANSWERED on a leased GPU:** `cudaStreamEndCapture` then `cudaStreamBeginCapture` on the SAME stream mid-forward with EAGER work between is LEGAL under `cudaStreamCaptureModeThreadLocal` (`src/vt/cuda/cuda_backend.cu:204-206`) — `orin:gpu0` via an `rc` lease, driver 12060, 3 replays with fresh inputs, 0 mismatches, bare zero-work re-begin legal too. G2 reachability is `tests/vllm/models/test_qwen3_break_point.cpp`, which drives the production `Qwen3DenseModel::Forward` with a scope open and counts `num_hidden_layers + 1` segments (mutation: delete the call site ⇒ 1 segment ⇒ RED), and holds G4 in the same case at 500 logits / 0 differing bit for bit. STAGED SLICE, named: the scope and the container are not yet ENTERED from a production step — no driver opens a scope until W2 migrates `Qwen3DenseDecodeGraph` — and the spec's `## Owed` lists it with W2 as owner, alongside the D10 auxiliary-stream auto-join (W4/W5), G5's ROCm/Tenstorrent arms (W3) and G1 on a real GPU (W2). **The capture-failure drain is NOT among them: it landed HERE**, as behaviour (`std::uncaught_exceptions()` compared against the depth recorded at scope entry, so a break function or ordinary model code throwing mid-capture destroys the partial container instead of handing back a forward that reports `captured() == true`) and as three gated arms (tests 13a, 13b, 13c). The spec's `## Owed` strikes the item through and reads DELIVERED in W1; this cell said the opposite until 2026-08-18 because `cba969857` re-derived field 6 alone. **W2 DONE 2026-08-18 ([#1261](https://github.com/mudler/vllm.cpp/issues/1261)): `Qwen3DenseDecodeGraph` MIGRATED and the seam is ENTERED from a production step**, which retires W1's staged slice. `Qwen3DenseDecodeGraph::Step` opens a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` and replays through `BreakableGraph::Replay`; the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle, the `bool captured` flag, the `DestroyGraph` loop and the driver's own `VLLM_CPP_CUDAGRAPH` read are gone (re-derivation items 1, 2, 5, 6). The migration ADDED `vt::GraphCaptureMode`, mirroring vLLM's `CUDAGraphMode` (`vllm/config/compilation.py:59-63`), whose v1 default `FULL_AND_PIECEWISE` (`:63`) is documented at `:630-632` as a FULL graph for DECODE batches and a piecewise one for prefill/mixed, with `decode_mode()` (`:65-66`) selecting the full half and the runtime reading it at `vllm/v1/worker/gpu/cudagraph_utils.py:185-186`. A decode driver opened `kPiecewise` would have turned a fully graphed decode step into ONE EAGER ATTENTION CALL PER LAYER between graph replays — not vLLM's decode behaviour, and invisible to every token gate here. `GraphBreak` in a `kFull` scope takes the pass-through arm and `AppendBreak` REFUSES a registration in that mode. G2 is `tests/vllm/models/test_qwen3_decode_graph_seam.cpp` (3 cases / 124 assertions), which asserts the SEAM's counters because a driver calling `Backend::ReplayGraph` directly leaves an identical backend log; the mutation restoring the pre-W2 raw pair (18 lines, compiled clean) left `test_breakable_graph` 27/27, `test_qwen3_break_point` 2/2 and `test_qwen3_forward` 10/10 GREEN and reddened only this file. G4 in the same file: capture step vs `Qwen3DenseModel::Forward`, 100 logits, 0 differing. **The async decline at `qwen3.cpp` STANDS and is now GATED in both arms**: migrating the capture does not move the INPUTS, so the depth-2 race is untouched, and the fix is `StepDevInputs` as a SEAM capability, which is W4. **G1 is NOT met by W2** and is recorded owed rather than implied. **W3 DONE 2026-08-19 ([#1291](https://github.com/mudler/vllm.cpp/issues/1291)): the three remaining PLAIN BATCHED drivers migrate — `Qwen3MoeDecodeGraph`, `VoxtralDecodeGraph`, `DeepseekV2DecodeGraph` — one commit each, each with its own RED-first G2 gate.** Four of the nine drivers are now on the seam, and the six batched-driver `VLLM_CPP_CUDAGRAPH` reads `## Our baseline` item 1 counted are down to TWO, both in `qwen3_5.cpp` (W4). Each gate asserts the SEAM's counters and not the backend log, because a driver that kept its raw pair produces identical logits, an identical backend log and an identical `replay_count()`; red-first on four assertions each (`test_qwen3_moe_decode_graph_seam` 222/226, `test_voxtral_decode_graph_seam` 224/228, `test_deepseek_v2_decode_graph_seam` 224/228, all exit 1), green 3/3 each after. The G2 mutation — restoring each pre-W3 driver file, 25/102, 23/92 and 25/94 lines, each compiled clean — reddens ONLY its own gate and leaves `test_breakable_graph` 216/216 and W2's `test_qwen3_decode_graph_seam` 231/231 green. The gate harness is now SHARED (`tests/vllm/models/decode_graph_seam_harness.h`); three more copies inside `tests/` would have reproduced the duplication this row removes from `src/`. **G1 IS DELIVERED and is no longer owed** — the item W1 and W2 both carried. `tests/vllm/models/test_decode_graph_seam_g1_cuda.cpp` runs each driver COLD, CAPTURE and THREE consecutive replays against its own eager arm (selected by `max_num_reqs == 0`, so both arms are one binary on one device rather than two builds, each with its OWN device KV cache) on `thor:gpu0` through an `rc` lease — NVIDIA Thor sm_110, driver 595.78, nvcc 13.0.88, source `c905bb536`, 32 `.cu.o` objects, binary resolving `libcudart.so.13`/`libcublasLt.so.13`: **3 cases, 1600 assertions, exit 0, `5 steps x 100 logits, 0 differing, 4 replays` per driver.** The COUNT carries that claim, not the status line: with no CUDA backend the same file prints `SUCCESS!` over `assertions: 0`. Bounded honestly — synthetic tiny models rather than a checkpoint, and W2's driver shares the seam by argument rather than by measurement. **W3 also found a gate that could not fail.** The three gates' `breaks_registered == 0` mode guard is a TAUTOLOGY for any model with no registered break point, and the one production `vt::GraphBreak` in the tree is W1's in `qwen3.cpp`: flipping `kFull` to `kPiecewise` in `qwen3_moe.cpp`, one token, compiled clean and left that gate GREEN at 226/226. The mode was UNOBSERVABLE from outside a driver, so `vt::GraphBreakStats` gains `full_scopes`/`piecewise_scopes`, counted in `GraphCaptureScope`'s constructor on the ACTIVE path only, with an inert-scope control; the same flip now reds all three gates on exactly those two assertions. **NO break point is registered in these three models, deliberately**: under `kFull` it would be pass-through machinery no gate can exercise, and the break-point set is what the PIECEWISE arm needs (W4/W6). **The async decline, per driver:** Voxtral needs none (its only construction site is `VoxtralGenerateGreedy`, unreachable from the runner); Qwen3-Coder and DeepSeek carry a NEW FINDING instead — `qwen3_moe_registry.cpp:107`, `deepseek_v2_registry.cpp:106` and `glm4_moe_lite_registry.cpp:125` route an async step into a host-vector replay with no `device_token_ids` check at all, filed [#1305](https://github.com/mudler/vllm.cpp/issues/1305) with W4 as owner rather than mitigated on a measurement W3 cannot make. G5's ROCm/Tenstorrent arm is NOT discharged and moves to W5: the fleet carries no such device, so it is blocked on hardware rather than unattempted. **W4 DONE 2026-08-19 ([#1307](https://github.com/mudler/vllm.cpp/issues/1307)): the persistent device input path becomes a SEAM CAPABILITY, and the two Qwen3.5 drivers migrate.** `vt::PersistentStepInput` (`include/vt/persistent_step_input.h`, `src/vt/persistent_step_input.cpp`) binds a capture-stable device destination the DRIVER owns together with its pinned host staging block, and refreshes it in place from a host source or a DEVICE one; it owns the address-stability rule as a REFUSAL, the staging block, and the refreshing ARM as an observable (`last_source()`, `vt::StepInputStats`), and deliberately NOT the device allocation, because `Qwen3_5DecodeGraph` draws its retained inputs from a DEDICATED `DevicePool` so they never pop a block the captured forward's scratch then needs (D3). RED-first against a stub with the declared API and no guarantees: `tests/vt/test_persistent_step_input.cpp` 9 cases / 0 passed / 59 assertions / 32 failed / exit 1, GREEN after at 9/9 and 59/59; three mutations (delete the capacity refusal, make a null device source a silent no-op, collapse the host arm out of staging) each compiled clean and each reds exactly one case. `Qwen3_5DecodeGraph` and `Qwen3_5DenseDecodeGraph` open a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` in `kFull` and replay through it, and their `PinnedStepInputs`/`StageStepInputs` staging now runs THROUGH the capability, which is what makes it reachable rather than a class with a unit test. **Six of the nine drivers are on the seam** and `grep -rn 'std::getenv("VLLM_CPP_CUDAGRAPH")' src/` returns exactly ONE line, `src/vt/breakable_graph.cpp:61` — one switch, at last. Gate `tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp` RED-first on the MoE driver's five seam assertions (3 cases / 62 assertions / 5 failed / exit 1) and GREEN after at 7/7 and 129, G4 reading `40 values, 0 differing` per driver; G2 mutations: the whole pre-W4 file restored reds BOTH drivers (296 lines, 10 assertions), the MoE replay bypassing the container reds ONLY the MoE case (7 lines), the MoE `kFull`->`kPiecewise` flip reds ONLY its mode counters (3 lines), and deleting the `StageStepInputs` call site reds ONLY the reachability case while `test_persistent_step_input` stays 59/59 green — the difference between a class that works and a capability something reaches. **W4 FALSIFIED THIS ROW'S OWN PREMISE, which is its most important result.** This record and the spec both said the fix `qwen3.cpp`'s `DenseDecodeGraphForward`'s decline names already existed as `StepDevInputs`. It does not: `StepDevInputs` has NO token-id member, and its pinned sibling `PinnedStepInputs::token_ids` was allocated at capture, filled every step, zeroed by the poison hook, and NEVER uploaded or read — the embed runs OUTSIDE the captured region from the HOST vector in every batched driver, so **the decode graph carries no token ids to the device in ANY driver**. The dead block is removed. Consequently the DECLINE STANDS and [#1305](https://github.com/mudler/vllm.cpp/issues/1305) STAYS OPEN: W4 also read the decline's recorded cause against the tree at its own parent and found it falsified (the `DeviceTokenIdsScope` WAS live on the graph path, consumed by `EmbedInto` on all three arms at `qwen3.cpp:610,621,644 @ 338cbbfd1^`), so the measured failure is real and its mechanism is unidentified — not a state from which a refactor may retire a mitigation. The async battery was NOT run and W4 says so plainly: it needs `dgx` WITH the Qwen3-0.6B/4B checkpoints, `dgx:gpu0` was held by another session for W4's whole window, and W4's lease was `thor:gpu0`. Still NO throughput claim. W5 DONE 2026-08-19 ([#1335](https://github.com/mudler/vllm.cpp/issues/1335)): the THREE SINGLE-SHAPE drivers migrate — the DFlash draft graph, the DeepSeek V4 decode graph and the Laguna decode graph, whose own note at `laguna.cpp:2116-2119` asked for this seam by name and named V4's as the sibling that moves with it. **NINE OF NINE DRIVERS ARE 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 three per-model rollback switches stay (each an A/B lever for one driver); `VLLM_CPP_CUDAGRAPH` reaches all three for the first time. **D10, the auxiliary-stream fork/join, is DISCHARGED and REACHED** — `GraphCaptureScope` owns the outstanding-fork set and joins it before `EndCaptureGraph` (port of `breakable_cuda_graph.py:353-361` plus the `wait_stream` hook `:101-153`), registered by `vt::GraphNoteFork`/`GraphNoteJoin` from `laguna.cpp:2572-2576,2612`, the only fork inside a captured region by construction. Every prior stage opened `kFull`, which has ONE segment and so no between-segments window, so the rule could not be exercised before W5 and untested machinery was not landed for it. Gated as a COUNTER and an ORDER out of one backend trace, five arms including the control where the model joins first, and two mutations (deleting the join reds only the new case on 5 assertions; making it over-fire reds it on 8). DFlash is the ONE single-shape driver gateable without a GPU, because its admission predicate names neither a device type nor a kernel registry: `test_qwen3_dflash_decode_graph_seam.cpp` RED-first 3 cases/0 passed/16 assertions/7 failed exit 1, GREEN after 3/18, and the G2 mutation reds ONLY that file while seven other suites — the driver's own `test_dflash_propose` included — stay green. **G1 RE-RUN at W5's head on `thor:gpu0`** (sm_110, driver 595.78, nvcc 13.0.88, 32 `.cu.o`, source `79dc6b5bd`) because D10 put a join on the path of EVERY segment close, so the seam changed underneath the five measured drivers: `test_decode_graph_seam_g1_cuda` 5 cases / 2066 assertions / 0 failed, each reading `0 differing, 4 replays`, plus `test_breakable_graph` 265 on the same device. **And the one thing a green build could NOT have told us was measured separately**: Laguna's capture class sits behind `#ifdef VT_MARLIN_NVFP4`, so a passing build is the SAME OBSERVATION as one that compiled the region out. `-DVT_MARLIN_NVFP4=1` is on `laguna.cpp`'s own compile command, and an undeclared identifier injected immediately after its `GraphCaptureScope` line FAILED the object build under `-Werror` (`laguna.cpp:2735`) against an rc-0 baseline, restoring to an empty diff; the identical mutation on V4 failed at `deepseek_v4.cpp:1921`. Both migrated regions are COMPILED, which retires the could-not-even-be-built half. **G1 for all three and G2 for V4 and Laguna are OWED on hardware**, per driver and per reason: V4's `CanRunResidentDecode` refuses `kCPU` and needs the four CUDA-registered kernel families, Laguna's capture class exists only under `VT_MARLIN_NVFP4`. G5's ROCm/Tenstorrent arm stays BLOCKED — the fleet is all NVIDIA — and its owner moves from W5 to the ROW. Still NO throughput claim; analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) W6 DONE 2026-08-19 ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): the eligibility predicate, #1020, and the negative result on the piecewise arm. | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-BREAK-W6`; [#1163](https://github.com/mudler/vllm.cpp/issues/1163), [#1192](https://github.com/mudler/vllm.cpp/issues/1192), [#1261](https://github.com/mudler/vllm.cpp/issues/1261), [#1291](https://github.com/mudler/vllm.cpp/issues/1291), [#1307](https://github.com/mudler/vllm.cpp/issues/1307), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1020](https://github.com/mudler/vllm.cpp/issues/1020), [#1335](https://github.com/mudler/vllm.cpp/issues/1335), [#1374](https://github.com/mudler/vllm.cpp/issues/1374), [#1380](https://github.com/mudler/vllm.cpp/issues/1380), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1390](https://github.com/mudler/vllm.cpp/issues/1390) | +| `ENG-CUDAGRAPH-BREAK` | One shared `vt` capture seam that accepts BREAK POINTS, so a forward containing a host-dependent op is still graphed instead of falling out entirely — and so the NINE hand-rolled drivers become one (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight). **Coverage AND CORRECTNESS row, not a throughput row** | T1 | mirror vLLM `CUDAGraphMode.PIECEWISE` splitting at `splitting_ops` (`vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); construction from SGLang BCG `python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/breakable_cuda_graph.py:204-243,246-274,309-333,335-367` @ `f63458b5be` (decorator + runtime stream capture, no compiler); its unit suite `test/registered/cuda_graph/breakable/test_breakable_cuda_graph.py:30,172,230` (305 lines, 11 unit cases) is mapped case for case in the spec's `## Tests to port` | **W6 MOVED THE PREDICATE** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374), 2026-08-19): `GPUModelRunner::execute_model` names the step's ACTUAL uniform query length once through `v1::GraphEligibleQueryLen` (`src/vllm/v1/worker/gpu/cudagraph_dispatch.h`, INERT with no caller since #442 and now called from production) and ships it on `ModelForwardInput::uniform_query_len`; the two Qwen3.5 registrations stop re-deriving that test in twenty duplicated lines each, and both key their slot ring on `(S, q, spec)`. [#1020](https://github.com/mudler/vllm.cpp/issues/1020) CLOSES on the pair, and the key half was a LIVE collision rather than the enabler #1020 called it: `S = spec_step ? B : PadToCaptureSize(B)` puts a 4-request spec step at 1+1 tokens and an 8-request padded decode on the same `S == 8` at the base commit. The widening is BOUNDED by `VT_SPEC_GRAPH_MAX_QLENS` (default 2), because reading the actual length multiplies the spec shape ceiling by `1 + k`. Seven of the nine drivers still read `pure_decode` and are byte-identical. **What did NOT move is 'except at the break points'**: no driver in this tree serves a prefill or a mixed batch under any predicate, so that needs a prefill capture driver nobody has written and whose benefit D5 already refutes on this hardware — a publishable negative, recorded in the spec's `## Owed` as a row-level item. The pre-W6 baseline it replaces: all-or-nothing, `src/vllm/v1/worker/gpu/runner.cpp:1338-1341` routing only `pure_decode`; drivers `qwen3_5.h:275`, `qwen3_5_dense.h:391`, `qwen3_moe.h:117`, `qwen3.h:243`, `deepseek_v2.h:324`, `voxtral.h:126`, plus `deepseek_v4.cpp`, `laguna.cpp` — and the spike found the NINTH already written, `src/vllm/model_executor/models/qwen3_dflash.cpp:771,1091`. The re-derivation is measured, not asserted: `StepDevInputs` (`src/vllm/model_executor/models/qwen3_5.cpp:3894`, the persistent DEVICE input path) exists in ONE driver and `grep -c` returns 0 in `qwen3_moe.cpp`, `qwen3.cpp`, `deepseek_v2.cpp` and `voxtral.cpp`, which is why `src/vllm/model_executor/models/qwen3.cpp`'s `DenseDecodeGraphForward` DECLINES the graph outright when the async device-token mirror is live. **That decline is why this is also a CORRECTNESS row** ([#1179](https://github.com/mudler/vllm.cpp/issues/1179)): a SHIPPED model has already lost its decode graph to the duplication, on the driver's own measurement (`depth-1, graph ON PASS 78/78`; `depth-2, graph OFF PASS 82/82`; `depth-2, graph ON FAIL, slots 1-3 degenerate`), and the fix its comment names is the sibling's `StepDevInputs`. The row still makes NO throughput claim: the prefill refutation on the `ENG-CUDAGRAPH` row (3.8% host idle, >96% GPU-busy, 92.5% glue) stands unchanged; **#1305 ADVANCED AND EXPLICITLY NOT CLOSED, and reading the tree found a larger defect than the issue described** (2026-08-19): `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp` never constructed a `detail::DeviceTokenIdsScope` and neither `qwen3_moe.cpp`'s nor `deepseek_v2.cpp`'s `EmbedInto` ever consulted one, so `ModelForwardInput::device_token_ids` reached NOTHING in either translation unit — the decode graph AND both eager arms embedded the host vector the runner's mirror arm deliberately leaves stale for decode rows. The three registries now publish the scope (the mechanism `qwen3.cpp`, `qwen3_5.cpp`, `mistral_registry.cpp`, `internlm2_registry.cpp` and `llama_registry.cpp` already use), and each decode-graph size slot holds a `vllm::StepTokenIds` (`include/vllm/model_executor/models/step_token_ids.h`) whose destination is a device buffer with a stable address, refreshed through `vt::PersistentStepInput` — host arm for the padded vector, DEVICE arm over the real prefix, both on the main queue so the second is ordered after the combine rather than racing it. That is `vt::PersistentStepInput::RefreshFromDevice`'s FIRST production caller, retiring the staged slice W4 landed with none, and it is the fix `qwen3.cpp`'s own decline comment names rather than a fifth private copy. `qwen3.cpp`'s decline is UNTOUCHED: W4 measured its recorded cause false and its real one is unidentified. **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: 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 ids 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 change itself concedes. #1305's own settlement condition is that battery, it did not run, and the issue stays OPEN with the `ENG-CUDAGRAPH-BREAK` row as owner. | owed: bit-exactness vs eager on every migrated model over MORE than one replay, on a real GPU — **W2 did NOT meet it and says so**: no `rc` lease was obtainable in its window and a CPU harness cannot replay a captured segment, so it moves to W3 with the three drivers of the same shape (G1); the host-lifetime contract of `decode-graph-scratch-uaf-2026-07-18.md` enforced AT the seam — D1's INPUT half, making the intermediates a segment reads unavailable to the `DevicePool` free list, which becomes live only for the first PIECEWISE production capture (W4); the auxiliary-stream auto-join before every segment close (`:353-361`, spec D10), live at `src/vllm/model_executor/models/qwen3_5.cpp:6254-6255,6384` and `src/vllm/model_executor/models/laguna.cpp:2572-2576,2612` (W4, W5). **Delivered by W1** ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the reachability mutation (performed; deleting the call site reds `tests/vllm/models/test_qwen3_break_point.cpp` and leaves the unit suite green); the ported SGLang unit cases with their arithmetic chains and post-replay assertions; and the break-function OUTPUT writeback (`replay_fn`/`_copy_output` `breakable_cuda_graph.py:231-235,172-201`, spec D9), whose destination is a `vt::BreakSlot` the seam owns rather than a caller reference it cannot outlive **W6 gates** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): G2 at THREE levels because the claim has three parts — the engine (`tests/vllm/v1/spec_decode/test_mtp_depth.cpp`, a real LoadedEngine/EngineCore/Scheduler/runner stack, asserting `clamped_spec_steps`, measured 0/0/1/2/4 at k=1/2/3/4/6), the driver (`tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp`, two spec shapes of equal S and different q getting two rings and two captures), and the arithmetic (`tests/vllm/v1/worker/gpu/test_cudagraph_dispatch.cpp`). Five detecting mutations, each reddening ONE level and leaving the others green, plus an over-fire control. A SIXTH mutation was NOT detected and forced a repair: the per-request verify conjunct is redundant on every model that reads the field (both are GDN hybrids whose prefill trips the first conjunct), so it moved into `GraphEligibleQueryLen` where a mutation reds 4 assertions, and the spec records it as unreached defence in depth. **G1 re-run on `thor:gpu0` (sm_110, driver 595.78, nvcc 13.0.88): 2066 assertions, 0 failed, 0 differing on all five migrated drivers — W6 moves no logit.** The ring key's own device case is BLOCKED by [#1380](https://github.com/mudler/vllm.cpp/issues/1380), a pre-existing `cudaMalloc` inside a capturing stream on the spec arm that W6 neither caused nor regressed; the case PINS that refusal and is written to fail when #1380 is fixed.; **#1305 (2026-08-19)**: `tests/vllm/models/test_moe_async_device_ids.cpp`, entered at `ModelRegistry::Forward` over a synthetic safetensors checkpoint for `Qwen3MoeForCausalLM` and `DeepseekV2ForCausalLM` — the production entry point, not the driver type. Three runs each: right host ids and no mirror as the reference, stale host ids and no mirror as the CONTROL that must differ, stale host ids with the truth reaching the model ONLY through `device_token_ids` as the gate. RED first at 2 cases / 65 assertions / 10 failed / exit 1, with 800 of 800 logit values differing over four steps on both architectures and every counter at 0; GREEN after at 65 of 65, exit 0. TWO mutations, each compiled clean and each restored by sha256: deleting the registry's scope line — the production call site — reds 4 assertions across both cases and puts all 800 values back, and swapping the seam's DEVICE arm for its HOST arm leaves the logits BIT IDENTICAL at 0 of 800 differing and reds only `device_refreshes` and `host_refreshes`, which is the arm no token gate can see. Neighbours green on the same binary: `test_qwen3_moe_decode_graph_seam` 228 of 228, `test_deepseek_v2_decode_graph_seam` 230 of 230, `test_qwen3_decode_graph_seam` 231 of 231, `test_voxtral_decode_graph_seam` 230 of 230, `test_breakable_graph` 265 of 265, `test_persistent_step_input` 66 of 66, `test_model_registry` 924 of 924, `test_qwen3_moe_forward` 504 of 504, `test_deepseek_v2_forward` 1052 of 1052. **NOT measured:** the depth-2 four-concurrent battery on a device, which needs a GPU and a real checkpoint; owed. **Found red on `main` and NOT caused here:** `test_qwen3_5_decode_graph_seam` exits 139 while its assertion line reads 135 of 135 passed ([#1390](https://github.com/mudler/vllm.cpp/issues/1390)); re-measured on this branch at exit 139 with 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 run to run on ONE unchanged binary — three consecutive runs of the same baseline binary gave 6 passed, 2 failed and 141 assertions, then no summary at all, then no summary at all. The exit code is the only stable observation, so no assertion count from that file carries a verdict. **THE FRESH REVIEW FOUND THE GATE ABOVE COVERED HALF OF WHAT THE CHANGE CLAIMS** and the repair widened it to 6 cases / 191 assertions / exit 0. What was ungated: the EAGER arms of both models — the half no graph refusal could have mitigated — and the THIRD registration, `glm4_moe_lite_registry.cpp`. Deleting the `TakeDeviceTokenIds` + `d.b.Copy` block from BOTH `EmbedInto` overloads left the old gate green at 2/2 and 65/65; deleting the GLM registry's two-line scope did too. The lane is now selected by the registry's OWN predicate: 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 so a case cannot drift onto the other lane and stay green. Three detecting mutations, each compiled clean and each restored: the two `EmbedInto` call sites reds the 3 EAGER cases only (exit 1, 3/6); the GLM scope reds the 2 GLM cases only (exit 1, 4/6); the seam's `RefreshFromDevice` call reds the 3 GRAPH cases only (exit 1, 3/6). A fourth mutation FAILED TO BUILD under `-Wunused-parameter` and its verdict was DISCARDED rather than read as a pass. **Still owed, and not implied:** the behavioural half of the device contract — that the copy reads DEVICE memory, and that it is main-queue-ordered after the combine — is untestable on the CPU backend, where `Backend::Alloc` returns host-addressable memory and both refresh arms reduce to 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, which gate the instrument rather than the behaviour. | spec [eng-cudagraph-break.md](specs/eng-cudagraph-break.md) (W0 spike DONE 2026-08-18: the existing `vt` capture vocabulary `include/vt/backend.h:208-222` expresses a SEGMENTED capture with NO new virtual, because `EndCaptureGraph` stores nothing (`src/vt/cuda/cuda_backend.cu:225-232`); a break point is expressible with one `thread_local` capture pointer plus a free function, no compiler and no decorator); **W1 DONE 2026-08-18 ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the seam LANDS** — `vt::BreakableGraph`, `vt::GraphCaptureScope` and `vt::GraphBreak` (`include/vt/breakable_graph.h`, `src/vt/breakable_graph.cpp`), the SGLang unit suite ported case for case (`tests/vt/test_breakable_graph.cpp`, 24 cases / 163 assertions, re-derived 2026-08-18 by `ninja test_breakable_graph && ./build/tests/test_breakable_graph`; the recorded 14/81 never re-derived at any head of this branch), and ONE break point registered at the DENSE ATTENTION ENTRY of `Qwen3ForCausalLM` (`src/vllm/model_executor/models/qwen3.cpp`, inside `RunLayer`). **The exit criterion W0 deliberately left open is ANSWERED on a leased GPU:** `cudaStreamEndCapture` then `cudaStreamBeginCapture` on the SAME stream mid-forward with EAGER work between is LEGAL under `cudaStreamCaptureModeThreadLocal` (`src/vt/cuda/cuda_backend.cu:204-206`) — `orin:gpu0` via an `rc` lease, driver 12060, 3 replays with fresh inputs, 0 mismatches, bare zero-work re-begin legal too. G2 reachability is `tests/vllm/models/test_qwen3_break_point.cpp`, which drives the production `Qwen3DenseModel::Forward` with a scope open and counts `num_hidden_layers + 1` segments (mutation: delete the call site ⇒ 1 segment ⇒ RED), and holds G4 in the same case at 500 logits / 0 differing bit for bit. STAGED SLICE, named: the scope and the container are not yet ENTERED from a production step — no driver opens a scope until W2 migrates `Qwen3DenseDecodeGraph` — and the spec's `## Owed` lists it with W2 as owner, alongside the D10 auxiliary-stream auto-join (W4/W5), G5's ROCm/Tenstorrent arms (W3) and G1 on a real GPU (W2). **The capture-failure drain is NOT among them: it landed HERE**, as behaviour (`std::uncaught_exceptions()` compared against the depth recorded at scope entry, so a break function or ordinary model code throwing mid-capture destroys the partial container instead of handing back a forward that reports `captured() == true`) and as three gated arms (tests 13a, 13b, 13c). The spec's `## Owed` strikes the item through and reads DELIVERED in W1; this cell said the opposite until 2026-08-18 because `cba969857` re-derived field 6 alone. **W2 DONE 2026-08-18 ([#1261](https://github.com/mudler/vllm.cpp/issues/1261)): `Qwen3DenseDecodeGraph` MIGRATED and the seam is ENTERED from a production step**, which retires W1's staged slice. `Qwen3DenseDecodeGraph::Step` opens a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` and replays through `BreakableGraph::Replay`; the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle, the `bool captured` flag, the `DestroyGraph` loop and the driver's own `VLLM_CPP_CUDAGRAPH` read are gone (re-derivation items 1, 2, 5, 6). The migration ADDED `vt::GraphCaptureMode`, mirroring vLLM's `CUDAGraphMode` (`vllm/config/compilation.py:59-63`), whose v1 default `FULL_AND_PIECEWISE` (`:63`) is documented at `:630-632` as a FULL graph for DECODE batches and a piecewise one for prefill/mixed, with `decode_mode()` (`:65-66`) selecting the full half and the runtime reading it at `vllm/v1/worker/gpu/cudagraph_utils.py:185-186`. A decode driver opened `kPiecewise` would have turned a fully graphed decode step into ONE EAGER ATTENTION CALL PER LAYER between graph replays — not vLLM's decode behaviour, and invisible to every token gate here. `GraphBreak` in a `kFull` scope takes the pass-through arm and `AppendBreak` REFUSES a registration in that mode. G2 is `tests/vllm/models/test_qwen3_decode_graph_seam.cpp` (3 cases / 124 assertions), which asserts the SEAM's counters because a driver calling `Backend::ReplayGraph` directly leaves an identical backend log; the mutation restoring the pre-W2 raw pair (18 lines, compiled clean) left `test_breakable_graph` 27/27, `test_qwen3_break_point` 2/2 and `test_qwen3_forward` 10/10 GREEN and reddened only this file. G4 in the same file: capture step vs `Qwen3DenseModel::Forward`, 100 logits, 0 differing. **The async decline at `qwen3.cpp` STANDS and is now GATED in both arms**: migrating the capture does not move the INPUTS, so the depth-2 race is untouched, and the fix is `StepDevInputs` as a SEAM capability, which is W4. **G1 is NOT met by W2** and is recorded owed rather than implied. **W3 DONE 2026-08-19 ([#1291](https://github.com/mudler/vllm.cpp/issues/1291)): the three remaining PLAIN BATCHED drivers migrate — `Qwen3MoeDecodeGraph`, `VoxtralDecodeGraph`, `DeepseekV2DecodeGraph` — one commit each, each with its own RED-first G2 gate.** Four of the nine drivers are now on the seam, and the six batched-driver `VLLM_CPP_CUDAGRAPH` reads `## Our baseline` item 1 counted are down to TWO, both in `qwen3_5.cpp` (W4). Each gate asserts the SEAM's counters and not the backend log, because a driver that kept its raw pair produces identical logits, an identical backend log and an identical `replay_count()`; red-first on four assertions each (`test_qwen3_moe_decode_graph_seam` 222/226, `test_voxtral_decode_graph_seam` 224/228, `test_deepseek_v2_decode_graph_seam` 224/228, all exit 1), green 3/3 each after. The G2 mutation — restoring each pre-W3 driver file, 25/102, 23/92 and 25/94 lines, each compiled clean — reddens ONLY its own gate and leaves `test_breakable_graph` 216/216 and W2's `test_qwen3_decode_graph_seam` 231/231 green. The gate harness is now SHARED (`tests/vllm/models/decode_graph_seam_harness.h`); three more copies inside `tests/` would have reproduced the duplication this row removes from `src/`. **G1 IS DELIVERED and is no longer owed** — the item W1 and W2 both carried. `tests/vllm/models/test_decode_graph_seam_g1_cuda.cpp` runs each driver COLD, CAPTURE and THREE consecutive replays against its own eager arm (selected by `max_num_reqs == 0`, so both arms are one binary on one device rather than two builds, each with its OWN device KV cache) on `thor:gpu0` through an `rc` lease — NVIDIA Thor sm_110, driver 595.78, nvcc 13.0.88, source `c905bb536`, 32 `.cu.o` objects, binary resolving `libcudart.so.13`/`libcublasLt.so.13`: **3 cases, 1600 assertions, exit 0, `5 steps x 100 logits, 0 differing, 4 replays` per driver.** The COUNT carries that claim, not the status line: with no CUDA backend the same file prints `SUCCESS!` over `assertions: 0`. Bounded honestly — synthetic tiny models rather than a checkpoint, and W2's driver shares the seam by argument rather than by measurement. **W3 also found a gate that could not fail.** The three gates' `breaks_registered == 0` mode guard is a TAUTOLOGY for any model with no registered break point, and the one production `vt::GraphBreak` in the tree is W1's in `qwen3.cpp`: flipping `kFull` to `kPiecewise` in `qwen3_moe.cpp`, one token, compiled clean and left that gate GREEN at 226/226. The mode was UNOBSERVABLE from outside a driver, so `vt::GraphBreakStats` gains `full_scopes`/`piecewise_scopes`, counted in `GraphCaptureScope`'s constructor on the ACTIVE path only, with an inert-scope control; the same flip now reds all three gates on exactly those two assertions. **NO break point is registered in these three models, deliberately**: under `kFull` it would be pass-through machinery no gate can exercise, and the break-point set is what the PIECEWISE arm needs (W4/W6). **The async decline, per driver:** Voxtral needs none (its only construction site is `VoxtralGenerateGreedy`, unreachable from the runner); Qwen3-Coder and DeepSeek carry a NEW FINDING instead — `qwen3_moe_registry.cpp:107`, `deepseek_v2_registry.cpp:106` and `glm4_moe_lite_registry.cpp:125` route an async step into a host-vector replay with no `device_token_ids` check at all, filed [#1305](https://github.com/mudler/vllm.cpp/issues/1305) with W4 as owner rather than mitigated on a measurement W3 cannot make. G5's ROCm/Tenstorrent arm is NOT discharged and moves to W5: the fleet carries no such device, so it is blocked on hardware rather than unattempted. **W4 DONE 2026-08-19 ([#1307](https://github.com/mudler/vllm.cpp/issues/1307)): the persistent device input path becomes a SEAM CAPABILITY, and the two Qwen3.5 drivers migrate.** `vt::PersistentStepInput` (`include/vt/persistent_step_input.h`, `src/vt/persistent_step_input.cpp`) binds a capture-stable device destination the DRIVER owns together with its pinned host staging block, and refreshes it in place from a host source or a DEVICE one; it owns the address-stability rule as a REFUSAL, the staging block, and the refreshing ARM as an observable (`last_source()`, `vt::StepInputStats`), and deliberately NOT the device allocation, because `Qwen3_5DecodeGraph` draws its retained inputs from a DEDICATED `DevicePool` so they never pop a block the captured forward's scratch then needs (D3). RED-first against a stub with the declared API and no guarantees: `tests/vt/test_persistent_step_input.cpp` 9 cases / 0 passed / 59 assertions / 32 failed / exit 1, GREEN after at 9/9 and 59/59; three mutations (delete the capacity refusal, make a null device source a silent no-op, collapse the host arm out of staging) each compiled clean and each reds exactly one case. `Qwen3_5DecodeGraph` and `Qwen3_5DenseDecodeGraph` open a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` in `kFull` and replay through it, and their `PinnedStepInputs`/`StageStepInputs` staging now runs THROUGH the capability, which is what makes it reachable rather than a class with a unit test. **Six of the nine drivers are on the seam** and `grep -rn 'std::getenv("VLLM_CPP_CUDAGRAPH")' src/` returns exactly ONE line, `src/vt/breakable_graph.cpp:61` — one switch, at last. Gate `tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp` RED-first on the MoE driver's five seam assertions (3 cases / 62 assertions / 5 failed / exit 1) and GREEN after at 7/7 and 129, G4 reading `40 values, 0 differing` per driver; G2 mutations: the whole pre-W4 file restored reds BOTH drivers (296 lines, 10 assertions), the MoE replay bypassing the container reds ONLY the MoE case (7 lines), the MoE `kFull`->`kPiecewise` flip reds ONLY its mode counters (3 lines), and deleting the `StageStepInputs` call site reds ONLY the reachability case while `test_persistent_step_input` stays 59/59 green — the difference between a class that works and a capability something reaches. **W4 FALSIFIED THIS ROW'S OWN PREMISE, which is its most important result.** This record and the spec both said the fix `qwen3.cpp`'s `DenseDecodeGraphForward`'s decline names already existed as `StepDevInputs`. It does not: `StepDevInputs` has NO token-id member, and its pinned sibling `PinnedStepInputs::token_ids` was allocated at capture, filled every step, zeroed by the poison hook, and NEVER uploaded or read — the embed runs OUTSIDE the captured region from the HOST vector in every batched driver, so **the decode graph carries no token ids to the device in ANY driver**. The dead block is removed. Consequently the DECLINE STANDS and [#1305](https://github.com/mudler/vllm.cpp/issues/1305) STAYS OPEN: W4 also read the decline's recorded cause against the tree at its own parent and found it falsified (the `DeviceTokenIdsScope` WAS live on the graph path, consumed by `EmbedInto` on all three arms at `qwen3.cpp:610,621,644 @ 338cbbfd1^`), so the measured failure is real and its mechanism is unidentified — not a state from which a refactor may retire a mitigation. The async battery was NOT run and W4 says so plainly: it needs `dgx` WITH the Qwen3-0.6B/4B checkpoints, `dgx:gpu0` was held by another session for W4's whole window, and W4's lease was `thor:gpu0`. Still NO throughput claim. W5 DONE 2026-08-19 ([#1335](https://github.com/mudler/vllm.cpp/issues/1335)): the THREE SINGLE-SHAPE drivers migrate — the DFlash draft graph, the DeepSeek V4 decode graph and the Laguna decode graph, whose own note at `laguna.cpp:2116-2119` asked for this seam by name and named V4's as the sibling that moves with it. **NINE OF NINE DRIVERS ARE 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 three per-model rollback switches stay (each an A/B lever for one driver); `VLLM_CPP_CUDAGRAPH` reaches all three for the first time. **D10, the auxiliary-stream fork/join, is DISCHARGED and REACHED** — `GraphCaptureScope` owns the outstanding-fork set and joins it before `EndCaptureGraph` (port of `breakable_cuda_graph.py:353-361` plus the `wait_stream` hook `:101-153`), registered by `vt::GraphNoteFork`/`GraphNoteJoin` from `laguna.cpp:2572-2576,2612`, the only fork inside a captured region by construction. Every prior stage opened `kFull`, which has ONE segment and so no between-segments window, so the rule could not be exercised before W5 and untested machinery was not landed for it. Gated as a COUNTER and an ORDER out of one backend trace, five arms including the control where the model joins first, and two mutations (deleting the join reds only the new case on 5 assertions; making it over-fire reds it on 8). DFlash is the ONE single-shape driver gateable without a GPU, because its admission predicate names neither a device type nor a kernel registry: `test_qwen3_dflash_decode_graph_seam.cpp` RED-first 3 cases/0 passed/16 assertions/7 failed exit 1, GREEN after 3/18, and the G2 mutation reds ONLY that file while seven other suites — the driver's own `test_dflash_propose` included — stay green. **G1 RE-RUN at W5's head on `thor:gpu0`** (sm_110, driver 595.78, nvcc 13.0.88, 32 `.cu.o`, source `79dc6b5bd`) because D10 put a join on the path of EVERY segment close, so the seam changed underneath the five measured drivers: `test_decode_graph_seam_g1_cuda` 5 cases / 2066 assertions / 0 failed, each reading `0 differing, 4 replays`, plus `test_breakable_graph` 265 on the same device. **And the one thing a green build could NOT have told us was measured separately**: Laguna's capture class sits behind `#ifdef VT_MARLIN_NVFP4`, so a passing build is the SAME OBSERVATION as one that compiled the region out. `-DVT_MARLIN_NVFP4=1` is on `laguna.cpp`'s own compile command, and an undeclared identifier injected immediately after its `GraphCaptureScope` line FAILED the object build under `-Werror` (`laguna.cpp:2735`) against an rc-0 baseline, restoring to an empty diff; the identical mutation on V4 failed at `deepseek_v4.cpp:1921`. Both migrated regions are COMPILED, which retires the could-not-even-be-built half. **G1 for all three and G2 for V4 and Laguna are OWED on hardware**, per driver and per reason: V4's `CanRunResidentDecode` refuses `kCPU` and needs the four CUDA-registered kernel families, Laguna's capture class exists only under `VT_MARLIN_NVFP4`. G5's ROCm/Tenstorrent arm stays BLOCKED — the fleet is all NVIDIA — and its owner moves from W5 to the ROW. Still NO throughput claim; analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) W6 DONE 2026-08-19 ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): the eligibility predicate, #1020, and the negative result on the piecewise arm. | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-BREAK-W6`; [#1163](https://github.com/mudler/vllm.cpp/issues/1163), [#1192](https://github.com/mudler/vllm.cpp/issues/1192), [#1261](https://github.com/mudler/vllm.cpp/issues/1261), [#1291](https://github.com/mudler/vllm.cpp/issues/1291), [#1307](https://github.com/mudler/vllm.cpp/issues/1307), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1020](https://github.com/mudler/vllm.cpp/issues/1020), [#1335](https://github.com/mudler/vllm.cpp/issues/1335), [#1374](https://github.com/mudler/vllm.cpp/issues/1374), [#1380](https://github.com/mudler/vllm.cpp/issues/1380), [#1390](https://github.com/mudler/vllm.cpp/issues/1390) | | `ENG-CUDAGRAPH-DIFFUSION` | Capture the LTX-2.5 denoise loop (fixed shapes, many identical iterations — the ideal graph target). **BLOCKED, and the blocker is ours:** the render does almost no device compute to capture | T2 | SGLang enabled BCG on this shape AFTER our pin — LTX-2 H200 two-stage 10.75s->6.90s (`d4be483efb`), SANA 1024px -26% (`6c7498113f`), SANA denoise 0.73->0.457s (`56ef810cad`). Dated events, NOT pinned evidence; their win is mostly PyTorch host tax we do not pay | NO capture at all: `grep` for capture across `src/vllm/model_executor/models/ltx2*.cpp` returns nothing | blocked by [#1024](https://github.com/mudler/vllm.cpp/issues/1024) (GPU util **exactly 0 in 321 of 347 samples**, 1.00 core of 20 held for 17+ min after staging), [#1007](https://github.com/mudler/vllm.cpp/issues/1007) (VAE decode has no device arm), [#1087](https://github.com/mudler/vllm.cpp/issues/1087) (**57-66% of wall** is ONE resolution-CONSTANT serial host phase), [#1010](https://github.com/mudler/vllm.cpp/issues/1010) (no phase-boundary log). Decision point is a MEASUREMENT of GPU-busy vs wall once device-resident, not an implementation. **The unblock order now has an owning row:** `LTX25-DEVICE-RESIDENCY` ([#1264](https://github.com/mudler/vllm.cpp/issues/1264), [ltx25-device-residency.md](specs/ltx25-device-residency.md)) stages those defects W0-W6 and carries this decision point as its W7 — if the loop comes back GPU-bound, #1164 closes as a refutation the way [#1161](https://github.com/mudler/vllm.cpp/issues/1161) closed prefill capture | [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | `INVENTORIED` | [#1164](https://github.com/mudler/vllm.cpp/issues/1164) | | `ENG-BATCH-INVARIANT` | Opt-in deterministic execution across scheduler batch sizes (`VLLM_BATCH_INVARIANT=1`): batch-invariant matmul/norm/attention/collectives plus persistent-scheduler NVFP4; production default remains off | T1 | default/env `vllm/envs.py:89,576-578`; initialization `vllm/v1/worker/gpu_worker.py:1262`; NVFP4 dispatch `csrc/libtorch_stable/quantization/fp4/nvfp4_scaled_mm_sm120_kernels.cu:212-220`; suite fixture `tests/v1/determinism/conftest.py:9-12`; operator/e2e `tests/v1/determinism/test_nvfp4_batch_invariant_scaled_mm.py`, `tests/v1/determinism/test_nvfp4_batch_invariant.py` @ `702f481` | - | [W3-C3R executed contract](specs/nvfp4-persistent-plan-cache.md#w3-c3r-batch-shape-localization-and-gate-correction-2026-07-13): production-default ours and vLLM both change outputs across batch shapes; no local opt-in implementation is claimed | `planned: specs/batch-invariant-execution.md` | `INVENTORIED` | - | | `ENG-ASYNC-SCHED` | Async/overlap scheduling (AsyncScheduler placeholders + depth-2 batch-queue step + async D2H on a copy stream); vLLM's DEFAULT at the pin — mirror obligation per B3. **Host-side machinery + runner device-input half + sampler-OUTPUT half LANDED + CPU-gated (2026-07-16):** `AsyncScheduler` placeholder accounting, `step_with_batch_queue` depth-2, `ResolveAsyncScheduling` default-ON-when-compatible + `MaxConcurrentBatches`, `VT_ASYNC_SCHED` rollback; the runner device-input path `combine_sampled_and_draft_tokens`; PLUS the sampler-OUTPUT half — `vt::Backend` event/pinned primitives (`AllocPinned`/events, CUDA cudaHostAlloc+cudaEvent, CPU sync-degeneration), `AsyncGPUModelRunnerOutput` (device sampled-id snapshot → non-blocking D2H on a copy queue + event; `get_output()` waits only that event; MAIN queue never blocked), `Sampler::forward(sampled_ids_out)` device-resident greedy, `GPUModelRunner::sample_tokens_async` + `runner_supports_async`, and the `Executor`+`step_with_batch_queue` seam resolving `get_output()` at CONSUME time. All behind `VT_ASYNC_RUNNER`/`set_async_input_combine`, default OFF. Sync path byte-identical (placeholder sites INERT while count 0; combine off; `sample_tokens_async` degenerates to sync when async off; `sampled_ids_out=nullptr`). **ENABLE-FLIP LANDED + CPU-gated (2026-07-16):** (1) `LoadedEngine` now reorders `runner_` before the scheduler and builds an `AsyncScheduler` + `max_concurrent_batches=2` when `ResolveAsyncScheduling(runner_.runner_supports_async())` resolves ON (else the byte-identical synchronous `Scheduler` + depth-1); the resolved mcb threads into `AsyncLLM`→`EngineCoreProc` (`step_with_batch_queue`) and the "Asynchronous scheduling is enabled/disabled" log mirrors vLLM for A/B audit; (2) the device combine/scatter kernel (`_combine_sampled_and_draft_tokens_kernel` + last_sampled scatter) is ported to CUDA (`src/vt/cuda/cuda_combine_tokens.cu`), main-stream-ordered on the CUDA async path so it DELETES `sample_tokens_async`'s pre-scatter `Synchronize`; the CPU backend keeps the host loop. `VT_ASYNC_RUNNER=1` engages full W3; `VT_ASYNC_SCHED=0` is the same-binary rollback. Production default (no env) stays synchronous byte-identical. **FULL W3 DGX proof RAN twice** — `f086b64` (5/5 gates PASS; c16 TPOT −5.4 ms WIN, tput neutral, TTFT +36 % = Little's-law repayment) and the 2026-07-16 re-proof on the THROUGHPUT-lever fix (persistent pooled sampled-id/pinned buffers + `Sampler` greedy scratch removing ALL per-step `cudaMalloc`/`cudaFree`/`cudaHostAlloc`/event-create from the sampled-id path, incl. the overlap-killing `cudaFree` inside `get_output`; mirrors `gpu_model_runner.py:873-878` + `async_utils.py:12-70`): token-exactness **6/6 PASS**, interleaved c16 **tput −0.32 % (gate ≥+1.5 % FAILS), TPOT −4.95 ms retained, TTFT +34.8 %** — the allocator lever is REFUTED as the tput unlock (≤0.1 % of a ~165 ms c16 step). **DEFAULT FLIPPED ON 2026-07-17** (`VT_ASYNC_RUNNER` default ON via the pure `AsyncRunnerFlagIsOn` predicate, mirroring `vllm/config/vllm.py:992-1044`): the discriminator (`6ea7856`) proved vLLM's own async pays the identical +26–31 % TTFT / −0.7 to −0.9 % tput / −2.6 to −4.3 ms TPOT envelope and W3-ON nets positive (both binding ITL-tail anomalies flip to PASS), so the "needs a throughput lever" ship-gate is RETIRED — W3 is a parity/mirror obligation with a tails+TPOT win. The flip is TOKEN-NEUTRAL (async-ON ≡ async-OFF bit-identical on DGX). `VT_ASYNC_RUNNER=0` = runner-level rollback, `VT_ASYNC_SCHED=0` = scheduler-level rollback. TTFT means rise into vLLM's async envelope BY DESIGN — the next binding grid runs async by default and its TTFT must NOT be misread as a regression. **ROBUSTNESS FIX 2026-07-20 (`discard_request_mask`):** the runner was missing vLLM's `discard_request_mask`, so `GPUModelRunner` emitted a sampled token for prefill-CHUNK requests too; under async this drained a `num_output_placeholders` never reserved (the `is_prefill_chunk` path adds none) → the `async_scheduler.cpp` `num_output_placeholders >= 0` assertion aborted on c8 + short-output (chunked prefill + preemption). FIX mirrors vLLM: `execute_model` computes `exec_state_.discard[i] = seq_len < num_tokens` (`gpu_model_runner.py:2048`); `sample_tokens` clears those rows to empty (`outputs.py:303`), the async path passes `invalid_req_indices` to `AsyncGPUModelRunnerOutput::get_output` (`gpu_model_runner.py:3625` + `outputs.py:303`). Scheduler UNCHANGED (assertion kept — it was correct once the runner honors `scheduler.py:1888-1890`). Sync/non-chunked decode byte-identical (mask all-zero); DGX 27B 235/235 + 35B 315/315, `vllm-bench` c8+short-output+chunked+kv-pressure no longer crashes, memcheck 0. Ledger [parity-ledger.md](parity-ledger.md) 2026-07-20 row | T1 | `vllm/v1/core/sched/async_scheduler.py:12`; `vllm/config/vllm.py:490,990,1038`; `vllm/v1/engine/core.py:519`; `vllm/v1/worker/gpu/input_batch.py:304-406`; `vllm/v1/worker/gpu/async_utils.py:12-70`; `vllm/v1/worker/gpu/gpu_model_runner.py:242-332`; `vllm/v1/outputs.py:298-307` | `src/vllm/v1/core/sched/async_scheduler.cpp:10,45`; placeholder plumbing `src/vllm/v1/core/sched/scheduler.cpp:148,164,605`; `src/vllm/v1/engine/core.cpp:91` (`step_with_batch_queue`, async-output seam); `src/vllm/v1/engine/core_proc.cpp:32,46`; config `include/vllm/config/scheduler.h:117,165,188`, `src/vllm/config/scheduler.cpp:12`; `include/vllm/v1/request.h:187`; runner input leaf `src/vllm/v1/worker/gpu/prepare_inputs.cpp`, `src/vllm/v1/worker/gpu/input_batch.cpp`; runner output leaf `include/vt/backend.h`+`src/vt/backend.cpp`+`src/vt/cuda/cuda_backend.cu` (event/pinned), `include/vllm/v1/worker/gpu/async_output.{h,cpp}` (`AsyncGPUModelRunnerOutput`), `src/vllm/v1/sample/sampler.cpp` (`sampled_ids_out`), `src/vllm/v1/worker/gpu/runner.cpp` (`sample_tokens_async`/`runner_supports_async`), `src/vllm/v1/executor/executor.cpp`+`include/vllm/v1/worker/gpu/model_runner_base.h` (async seam); enable-flip `include/vllm/entrypoints/model_loader.h`+`src/vllm/entrypoints/model_loader.cpp` (`runner_` before scheduler, `ResolveAsyncEnabled`/`MakeScheduler`, `AsyncScheduler`+mcb=2, log), `include/vllm/v1/engine/async_llm.h`+`src/vllm/v1/engine/async_llm.cpp` (mcb param → `EngineCoreProc`); device kernel `include/vt/cuda/combine_tokens.h`+`src/vt/cuda/cuda_combine_tokens.cu`, wired `src/vllm/v1/worker/gpu/runner.cpp` (CUDA combine/scatter branch removes the pre-sync) | `tests/vllm/v1/test_async_scheduler.cpp:1` (6 cases, 54 asserts; RED vs base Scheduler 2/6 fail); depth-2 engine cycle `tests/vllm/v1/test_engine_core_proc.cpp:479` (mcb=2, async-output seam); config resolution `tests/vllm/test_scheduler_config.cpp:75`; enable-flip construction matrix `tests/vllm/entrypoints/test_loaded_engine_dense.cpp` (runner×VT_ASYNC_SCHED → scheduler type + mcb; RED = un-flipped engine, 3/3 ON-arm asserts fail); runner input leaf `test_combine_tokens.cpp` (RED = stale → 5/7 fail), `test_input_batch.cpp`, `test_runner.cpp` (async-ON≡sync); output leaf `tests/vt/test_backend.cpp` (event/pinned contract), `tests/vllm/v1/worker/test_async_output.cpp` (materialize/flush/snapshot; RED = +1 splice), `test_runner.cpp` (`sample_tokens_async` decode ≡ sync); full CPU ctest 111/111, tools 164/164. Prior diagnostic `3812d8` six-leg control: total **1.002153×**, TTFT **0.862159×**, no GPU-time reduction (neutral for speed). **DEFAULT-FLIP (2026-07-17):** new pure CPU flag test [test_async_runner_flag.cpp](../tests/vllm/v1/worker/test_async_runner_flag.cpp) (11 asserts, default-ON/'0'-off); construction matrix [test_loaded_engine_dense.cpp](../tests/vllm/entrypoints/test_loaded_engine_dense.cpp) INVERTED (default → AsyncScheduler+mcb=2; RED verified 5 asserts fail vs un-flipped). CPU clean `-Werror` rebuild, full serial ctest **116/116**, tools **164/164**. **DGX re-confirmation** (evidence `dgx:~/work/vllm.cpp-async-flip`, CUTLASS+FA2 hard-verified, one flock): shipping default (async ON + RMSNorm-fast OFF) → **27B 235/235 + 35B 315/315** with the "Asynchronous scheduling is enabled (mcb=2)" log, and both rollback arms (`VT_ASYNC_RUNNER=0`, `VT_ASYNC_SCHED=0`) 235/235 + 315/315 log "disabled"; async arms BIT-IDENTICAL (token-neutral). Closing record [parity-ledger.md#L502](parity-ledger.md#L502) | [async-serving.md](specs/async-serving.md) | `DONE` | `6ea7856` | diff --git a/.agents/specs/eng-cudagraph-break.md b/.agents/specs/eng-cudagraph-break.md index 906a09b0b..bf29aa851 100644 --- a/.agents/specs/eng-cudagraph-break.md +++ b/.agents/specs/eng-cudagraph-break.md @@ -1517,15 +1517,24 @@ region by construction, and that driver is the production caller. Gated as a counter and an ORDER out of one backend trace, with two mutations proving neither the rule nor its control arm is vacuous. -**#1305 IS FIXED, and reading the tree made it a bigger defect than the issue -described.** The three registrations it names never published +**#1305's EAGER HALF IS FIXED AND GATED; its GRAPH half is not settled, and the +issue stays OPEN.** Reading the tree made it a bigger defect than the issue +described: the three registrations it names never published `detail::DeviceTokenIdsScope` and neither model's `EmbedInto` ever consulted one, so `device_token_ids` reached nothing in either translation unit — the eager arms as well as the decode graph. Both now consume it, and each decode-graph slot holds a `vllm::StepTokenIds` on `vt::PersistentStepInput`, which is -`RefreshFromDevice`'s first production caller. What is NOT closed is the depth-2 -battery on a device; `## Owed` carries it, and `qwen3.cpp`'s decline is -untouched. +`RefreshFromDevice`'s first production caller. Gated at six cases and 191 +assertions across both lanes of all three registrations, after a fresh review +proved by mutation that the first gate saw neither the eager arms nor the third +registry. + +The graph half does not close on that. The mechanism these drivers now have is +functionally what `qwen3.cpp` already had at `338cbbfd1^`, and W4 measured the +depth-2 graph-ON battery FAILING with it in place; a stable device address buys +nothing while the embed stays outside the capture. #1305's settlement condition +is that battery, it did not run, `## Owed` carries it together with the untested +device half of the refresh contract, and `qwen3.cpp`'s decline is untouched. **W4 corrected a premise this spec had asserted three times.** The decode graph carries NO token ids to the device in any driver, `StepDevInputs` included, so @@ -1850,8 +1859,8 @@ Each item names the stage that owns it. Nothing here is claimed by W1. and find out whether they degenerate at depth 2 at all. Owner: row **`ENG-CUDAGRAPH-BREAK`**, the stage that gets that window. - **RESOLVED, AND NOT THE WAY EITHER W3 OR W4 EXPECTED, because reading the tree - found a LARGER defect than the one #1305 describes and a fix that needs no + **HALF RESOLVED, AND NOT THE WAY EITHER W3 OR W4 EXPECTED, because reading the + tree found a LARGER defect than the one #1305 describes and a fix that needs no decline at all.** #1305 reads as a graph-arm hazard. It is not: those three registrations never constructed a `detail::DeviceTokenIdsScope` and neither `qwen3_moe.cpp`'s nor `deepseek_v2.cpp`'s `EmbedInto` ever consulted one, so @@ -1873,25 +1882,79 @@ Each item names the stage that owns it. Nothing here is claimed by W1. of a fifth private copy. Gated at `tests/vllm/models/test_moe_async_device_ids.cpp`, entered at - `ModelRegistry::Forward` over a synthetic safetensors checkpoint for both - architectures: three runs each — right host ids and no mirror as the reference, - stale host ids and no mirror as the CONTROL that must differ, stale host ids - with the truth reaching the model only through `device_token_ids` as the gate. - RED before the fix at 2 cases / 65 assertions / 10 failed / exit 1, with 800 of - 800 logit values differing over four steps on BOTH architectures; GREEN after at - 65/65, exit 0. Two mutations, each compiled clean and each restored by sha256: - deleting the registry's scope line — the production call site — reds 4 - assertions and puts all 800 values back, and swapping the seam's DEVICE arm for - its HOST arm leaves the logits BIT IDENTICAL at 0 of 800 differing and reds only - `device_refreshes` and `host_refreshes`, which is the arm no token gate can - see. - - **WHAT IS STILL OWED, narrowed rather than closed.** The depth-2 - four-concurrent battery against these two models on a real device has NOT been - run: it needs a GPU and a real checkpoint, and this stage had neither. So the - fix is proven to embed the mirror's identifiers and is NOT proven to close the - degeneration `qwen3.cpp`'s decline was measured against — whose own cause W4 - established is unidentified. `qwen3.cpp`'s decline therefore STANDS, untouched. + `ModelRegistry::Forward` over a synthetic safetensors checkpoint: three runs per + case — right host ids and no mirror as the reference, stale host ids and no + mirror as the CONTROL that must differ, stale host ids with the truth reaching + the model only through `device_token_ids` as the gate. RED before the fix at 2 + cases / 65 assertions / 10 failed / exit 1, with 800 of 800 logit values + differing over four steps on BOTH architectures; GREEN after at 65/65, exit 0. + + **THE GATE THAT LANDED COVERED HALF OF WHAT THE CHANGE CLAIMS, and a fresh + review proved it by mutation rather than by reading.** Two gaps, each shown with + a mutation that compiled and ran. Deleting the `TakeDeviceTokenIds` + + `d.b.Copy` block from BOTH `EmbedInto(const std::vector&)` overloads — + restoring the pre-fix EAGER behaviour, which is the half this entry calls its + most important finding — 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 registrations this entry + says publish a scope, did too; the landing change's own reachability mutation + had covered only two. + + Repaired to SIX cases / 191 assertions / exit 0, both lanes for all three + registrations, routed 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, 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. + + Three detecting mutations, each compiled clean (`compile_rc=0`) and each + restored: + + | mutation | exit | cases | what reds | + |---|---|---|---| + | delete both `EmbedInto` override consumers | 1 | 3 of 6 pass | the 3 EAGER cases, on `differing == 0` | + | delete `glm4_moe_lite_registry.cpp`'s scope | 1 | 4 of 6 pass | the 2 GLM cases only | + | delete `StepTokenIds::Refresh`'s `RefreshFromDevice` | 1 | 3 of 6 pass | the 3 GRAPH cases, on `device_refreshes` AND `differing == 0` | + + A FOURTH mutation deleted the shared copy outright and FAILED TO BUILD under + `-Wunused-parameter`; its verdict was discarded rather than read as a pass, + which is the failure mode a mutation harness has to print `compile_rc` to avoid. + + **WHAT IS STILL OWED, narrowed rather than closed, and why #1305 does NOT close + here.** The issue SPLITS. 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 the change + itself concedes. So landing it is not evidence that the graph-arm degeneration + is gone. + + 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: it needs a + GPU and a real checkpoint, and this stage had neither. The fix is proven to + embed the mirror's identifiers and is NOT proven to close the degeneration + `qwen3.cpp`'s decline was measured against, whose own cause W4 established is + unidentified. **#1305 therefore stays OPEN**, the pull request references it + without a closing keyword, and `qwen3.cpp`'s decline STANDS, untouched. + + **AND THE DEVICE HALF OF THE CONTRACT IS UNTESTED ON ANY DEVICE.** On the CPU + backend `vt::Backend::Alloc` returns HOST-addressable memory, so the mirror's + buffer and the host vector are the same kind of pointer and both refresh arms + reduce to the same memcpy from the same address. Swapping + `PersistentStepInput::RefreshFromDevice` for `RefreshFromHost` leaves every + logit bit-identical — 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. The two behavioural guarantees — that the copy + reads DEVICE memory, and that it is main-queue-ordered AFTER the runner's + combine — have no gate on any device. Owed with the battery, same window, same + owner. The reason this stage did not run it, stated as a fleet state rather than as an intention: at 2026-08-19, `rc devices` read `dgx:gpu0 busy` — the only box whose HuggingFace cache carries Qwen3-Coder-30B-A3B — while `thor:gpu0` and From 48317737a775fc691f2c774eb695957f5df79527 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 19 Aug 2026 22:44:22 +0000 Subject: [PATCH 8/9] record(ENG-CUDAGRAPH-BREAK): the Owed entry claimed a reader this change does not give (#1305, #1390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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] --- .agents/engine-matrix.md | 2 +- .agents/specs/eng-cudagraph-break.md | 74 +++++++++++++++++++++++++--- include/vt/persistent_step_input.h | 9 ++-- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/.agents/engine-matrix.md b/.agents/engine-matrix.md index aefaf283c..57092b47c 100644 --- a/.agents/engine-matrix.md +++ b/.agents/engine-matrix.md @@ -61,7 +61,7 @@ forensics: roadmap_v1.md and the parity ledger. | `ENG-PREEMPT-RECOMPUTE` | FCFS tail preemption with recompute | T0 | `vllm/v1/core/sched/scheduler.py:1142`; `tests/v1/core/test_scheduler.py:930` | `src/vllm/v1/core/sched/scheduler.cpp:102,157`; `src/vllm/v1/core/sched/request_queue.cpp:36` | `tests/vllm/v1/test_scheduler.cpp:247,295`; `tests/vllm/v1/test_request_queue.cpp:91` | `planned: specs/preemption.md` | `ANCHOR-BACKFILL` | - | | `ENG-CUDAGRAPH` | Decode graph capture/replay modes (host-cluster cleanup: capture-size set derived from `max_num_seqs` mirroring vLLM `_set_cudagraph_sizes`; 2026-07-18 graph-baked-scratch use-after-free fix — the 35B c2+ online-serving IMA blocker) | T0 | `vllm/config/compilation.py:53,1319,683-684,1438-1444`; `vllm/config/vllm.py:1667-1770`; `vllm/v1/worker/gpu/cudagraph_utils.py:116`; `tests/compile/test_config.py:122,229` | `src/vt/cuda/cuda_backend.cu:76,97,105`; `include/vllm/model_executor/models/decode_graph_sizes.h`; `src/vllm/model_executor/models/qwen3_5.cpp:3754,3952`; `src/vllm/v1/worker/gpu/runner.cpp:577,597`; graph-safe scratch (retire-on-grow so graph-baked scratch pointers stay valid) `src/vt/cuda/graph_safe_scratch.h`, `src/vt/cuda/cuda_moe_marlin.cu:75`, `src/vt/cuda/cuda_matmul_nvfp4.cu:766`, `src/vt/cuda/cuda_matmul_nvfp4_cutlass.cu:105`, `src/vt/cuda/cuda_matmul_fp8_cutlass.cu:95` | `tests/vt/test_cuda_backend.cpp:98`; `tests/vllm/models/test_decode_graph_sizes.cpp`; `tests/vt/test_graph_safe_scratch.cpp`; explicit 35B gate `tests/parity/test_qwen36_paged_engine.cpp:140` | [blocktable-host-cluster-cleanup.md](specs/blocktable-host-cluster-cleanup.md); [decode-graph-scratch-uaf-2026-07-18.md](specs/decode-graph-scratch-uaf-2026-07-18.md) | `PARTIAL` | **PREFILL capture REFUTED as a lever (2026-08-17, [#1161](https://github.com/mudler/vllm.cpp/issues/1161)).** vLLM's v1 default already captures prefill piecewise (`vllm/config/compilation.py:60-63,615,630` @ `555967922`) and it is in our denominator; SGLang reached the same coverage without `torch.compile` via BCG (`SGLANG-BCG` in [sglang-matrix.md](sglang-matrix.md)). Neither helps us: GB10 2026-07-09 measured prefill GPU-idle-between-launches at **3.8%** with GPU-busy >96% on both arms, and the 27B prefill gap at **92.5% non-GEMM glue GPU work** with the dominant GEMM at +0.17% and attention AHEAD. There are no launch bubbles in our prefill to collapse. Row stays `PARTIAL`; the real residuals are exec dedup ([#1162](https://github.com/mudler/vllm.cpp/issues/1162)) and the break-point seam ([#1163](https://github.com/mudler/vllm.cpp/issues/1163)). Spec [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | | `ENG-CUDAGRAPH-DEDUP` | Graph-executable dedup: hash each captured graph's topology and re-point ONE `cudaGraphExec` with `cudaGraphExecUpdate` on a signature hit, instead of instantiating one exec per padded bucket per model. A memory and capture-time change, NOT a throughput change — a deduped replay launches the same nodes, and the load-bearing gate is byte-identity rather than a ratio | T2 | vLLM has no analogue (its execs come from `torch.compile`, `vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); secondary oracle SGLang `python/sglang/srt/model_executor/runner_backend/cuda_graph_dedup_mixin.py:27-37,105-179,219-242,258-275,353-358` @ `f63458b5be` ([oracles/sglang.md](oracles/sglang.md)) | W1+W2 landing here behind `VT_CUDA_GRAPH_DEDUP`, default OFF until the device A/B measures the per-switch update cost: a device-agnostic dedup registry shared by both accelerator backends plus one CUDA/HIP ops table written once, wired into `EndCaptureGraph`/`ReplayGraph`/`DestroyGraph`. Baseline it replaces: `src/vt/cuda/cuda_backend.cu:222-232` instantiates a fresh exec per capture and destroys the raw graph, over the 7 (`max_num_seqs=32`) or 11 (64) buckets of `include/vllm/model_executor/models/decode_graph_sizes.h:32-41`, times NINE drivers (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight, missing the DFlash draft graph `src/vllm/model_executor/models/qwen3_dflash.cpp:771,870,1038,1091,1095,1106`) | `tests/vt/test_graph_dedup.cpp` 13/13 cases, 65 assertions, RED-first (written and run against an absent header, and the four cases added by the fresh review of #1178, three of them run against the unfixed source) and gated on every platform via a fake ops table whose launch log makes "the right nodes ran" an observable sequence over MORE than one replay per shape; 13/13 negative mutations detected (9 at implementation, 4 at review repair). That count covers `src/vt/graph_dedup.h` ONLY. `src/vt/graph_dedup_runtime.h` had NO executable coverage on any tier, and [#1184](https://github.com/mudler/vllm.cpp/issues/1184) is what hid in that gap: the file is DESIGNED to see runtime calls fail — a refused `cudaGraphExecUpdate` probe is the feature working — and never consumed the runtime's latched error, so the next unrelated kernel reported the refusal as its own failure and every `VT_CUDA_GRAPH_DEDUP=1` run died 6/6 on GB10 as `greedy_argmax launch: invalid device function` from a launch that had succeeded. Repaired structurally rather than at twelve sites: the clear lives in `ScopedLatchClear`'s destructor (`src/vt/graph_dedup_latch.h`) installed at the six `GraphDedupOps` entry points by `MakeLatchGuardedOps`, the table's only constructor, so no raw function address reaches a field and an unwired seventh operation leaves a null the registry refuses; one line covers CUDA and HIP. The device-free half of the signature walk moved to `src/vt/graph_dedup_signature.h` and is gated by `tests/vt/test_graph_dedup_runtime.cpp` 13/13 cases, 51 assertions, RED-first against the pre-fix guard (22 failed assertions reproducing the production message), 7/7 negative mutations detected — Kahn ordering, topological re-index, sorted edge emission, the depth-4 child bound and the four graph-level escapes. STILL compile-gated only: the five node-payload cases behind the device policy. **DEVICE A/B DELIVERED 2026-08-18 on `dgx:gpu0` (GB10, driver 580.173.02, nvcc 13.0.88, `rc` job f88d484b), and it SPLIT.** Gated commit `72de552c8`, whose four dedup sources are byte-identical to the merged `2a976eb9f` — the row squashed, so the gated tree is not an ancestor of the merge and that sha equality is what carries the claim. CORRECTNESS PASSES: 12/12 cells exit 0, zero `invalid device function` and zero `engine-fatal` in every cell log where the pre-fix head `e4ce5571a` died after exactly one replay, ON replays as often as OFF (60=60, 33=33, 43=43), and `--output-token-ids` is IDENTICAL over 10/10 comparisons with the three OFF/OFF controls passing FIRST and the three workloads hashing to three DIFFERENT values, so the identity is not vacuous. #1184 is closed by this run, because a CPU suite drives a fake runtime and cannot observe the real latched error. THE BENEFIT IS REFUTED for the case this row was filed for: `N == M` in every ON cell — 3 graphs to 3 execs on sizes [24 16 8], 2 to 2 on [16 8], 2 to 2 on [32 24] — with the registry's count CLIMBING 1→1, 2→2, 3→3, so more than one capture reached it and the 1:1 is a measurement rather than the single-capture artefact the first attempt produced. Cause pre-registered before the run and then confirmed, structural rather than a tuning miss: `AppendKernelPayload` hashes (`func`, `gridDim.{x,y,z}`, `blockDim.{x,y,z}`, `sharedMemBytes`) at `src/vt/graph_dedup_runtime.h:121-128` and the memcpy payload hashes the copy extent, so the padded batch dimension sits in the KEY, no candidate group ever forms and `cudaGraphExecUpdate` is NEVER ATTEMPTED. That contradicts this row's own premise — `graph_dedup.h`'s header says the fold is for "two padded batch sizes … the same node topology with different parameters" — and SGLang keys the same fields (`cuda_graph_dedup_mixin.py:105-114`), so whatever folds upstream is not decode buckets either. NO throughput or memory number is recorded: clocks unpinned AND the ON arm allocated exactly as many executables as OFF. Honest gaps: per-shape replay counts are unavailable (the driver prints a TOTAL, so B's ~30-per-shape is arithmetic); the driver's "N captured size(s)" counts SLOTS not captures (A reports 6, emits 3); the container's own cuBLASLt was never re-tested at CUDA 13.0 because the staged cu130 prefix was probed first and worked; only the Qwen3 dense decode driver was exercised. STILL OWED: the default flip, now NOT JUSTIFIED on this evidence rather than merely ungated; a COARSER key that could group two decode buckets at all, which the probe-before-fold design makes a cost question rather than an obviously unsafe one ([#1226](https://github.com/mudler/vllm.cpp/issues/1226), the next traceable hypothesis, deliberately NOT decided by this record); device-tier signature stability/discrimination tests; probing `current_raw` instead of `raws.front()` to retire the update-transitivity assumption; the ROCm compile; a supporting `orin:gpu0` leg, BLOCKED because the Jetson 540.4.0 driver cannot run a CUDA 13 runtime (`cudaGetDeviceCount err=35`); and reaching the feature from the default serving path at all — the async runner captures no decode graph, **W5, THE SAME DAY, CONFIRMED THE HYPOTHESIS THAT NEGATIVE PRODUCED ([#1226](https://github.com/mudler/vllm.cpp/issues/1226) DELIVERED).** Same box, `rc-worker-4b8lj`, boot_id `3fd9745a-d25a-426c-ba3c-97c958a85515` at both ends, GB10, driver `580.173.02`, `### DONE_AB_KEY 2026-08-18T20:58:46Z`, binary sha256 `ca114abb…c772ad` from `b48b51df1` (tar sha256 asserted before extraction). Drop the launch dimensions and the memcpy extents from the key and every bucket folds: `a_coarse` 3 graphs to 2 execs, `b_coarse` 2 to 1, `c_coarse` 2 to 1, each `probes=1 refused=0`, against `probes=0 refused=0` in every EXACT cell. **`probes=0` in the EXACT cells is the direct process-level proof of W4's source-level diagnosis** — with the launch dimensions in the key no candidate group forms and `cudaGraphExecUpdate` is never asked; drop them and it is asked once per fold and ACCEPTED EVERY TIME. The saving W4 recorded as unreachable is reachable via the key. Byte-identity holds on A (five cells, `59ebff4a…`) and C (four cells, `ff205260…`). **Workload B is VOID rather than a pass, and its cause is a NEW DEFECT that is not this row's:** the two `VT_CUDA_GRAPH_DEDUP`-unset control cells DISAGREED (`5973c5a1…` 2638 bytes vs `4cf79230…` 2650 bytes) on one binary, one workload, greedy `--temperature 0 --seed 777` at `--concurrency 16`, 23 s apart — 672 tokens both, so the byte delta is JSON width and not a length; exactly rows 17 and 18 of 21 differ, both mid-decode, both in the ragged tail `21 % 16` leaves. B's `b_off_a == b_exact` and `b_off_a == b_coarse_a` therefore compare against a baseline that does not reproduce itself and are WORTHLESS; only the OFF/OFF control made that visible, and without it B would have read as three more confirmations. Filed [#1283](https://github.com/mudler/vllm.cpp/issues/1283). **Caveats that bound this result:** nvcc was `13.3.73` here and `13.0.88` for the W4 baseline the recorded dgx gate stack names, so the OFF-vs-ON and EXACT-vs-COARSE comparisons WITHIN this binary are valid while this run and that baseline are NOT directly comparable; clocks unpinned (2405 MHz current, 3003 max, 2418 applications) and nothing measured bytes, so NO throughput and NO memory number is claimed or implied; only the Qwen3 dense decode driver was exercised; `refused=0` is ONE driver on ONE hardware and toolkit pair, which is no more a floor than W4's negative was a ceiling; and the coarse key is behind `VT_CUDA_GRAPH_DEDUP_COARSE_KEY`, default OFF, inside a default-OFF flag, on **PR [#1232](https://github.com/mudler/vllm.cpp/pull/1232) which is STILL A DRAFT — nothing on `main` folds today.** **Row stays `ACTIVE`, argued:** not `DONE`, because the fold is unreachable on every shipping configuration and the row's stated MEMORY saving has never been measured in bytes on either key; not `PARTIAL`, because nothing upstream is omitted — the coarse key is our own extension past SGLang, which keys the fields we started from; not `BLOCKED`, because nothing external stops the next step. What is owed is now a DECISION about the default plus the byte measurement and the probe-cost-at-real-churn measurement it needs, and landing #1232 first **W6, 2026-08-19, THE DEVICE-BYTE MEASUREMENT — THE BENEFIT QUESTION IS NOW CLOSED AND THE ANSWER IS NEGATIVE.** Tested `origin/main` `2c8f53d93`, which is PR #1232 LANDED, so the "nothing on `main` folds today" caveat every earlier record carried is RETIRED and this measures a configuration that ships. Same box, `rc` job `93f783de`, pod `rc-worker-4b8lj`, boot_id `3fd9745a-…` at BOTH ends, GB10, driver `580.173.02`, nvcc **13.0.88** (the W4 baseline toolkit; W5 ran 13.3.73, so W6 and W5 are NOT directly comparable while comparisons WITHIN this one binary are valid), binary sha256 `be697268…0ce657a7`, `### DONE_BYTES 2026-08-19T04:57:19Z`, 12/12 cells exit 0, zero VOID markers. **THE FOLD ENGAGES AT THE SHIPPED BUCKET SET**, which is the churn W5 could not produce: `vllm-bench` sets `max_num_seqs = concurrency`, so W32 captured `[1 2 4 8 16 24 32]` 7-of-7 and W64 captured `[1 … 64]` 11-of-11, exactly `decode_graph_sizes.h:32-41`, against the 2-3 buckets every earlier conclusion was drawn from. COARSE folds 7 graphs to 3 execs (`probes=7 refused=3`) and 11 to 5 (`probes=22 refused=16`); EXACT folds NOTHING at `probes=0`, reproducing W4 at four times the bucket count. Token ids byte-identical across every cell of a workload INCLUDING both OFF/OFF controls (`ff0db6c6…be9d` 11720 B; `e1cbf5fc…e5d0` 57620 B) — neither workload has #1283's ragged-tail shape and neither hit it. **THE SAVING DOES NOT SURVIVE ITS OWN NULL CONTROL.** `nvidia-smi --query-compute-apps` tail median (the `--query-gpu=memory.used` axis returns `[N/A]` on this box) shows W64 IDENTICAL to the megabyte in all five cells (9737) and W32's coarse arm reading 10-23 MiB HIGHER than OFF (3252/3262 vs 3262/3275). A `cudaMemGetInfo` shim summed over every instantiate gives a nominal 13.83 MiB at 7 buckets — **0.42% of a 3.25 GiB process** — and **−0.75 MiB, i.e. NOTHING, at 11**. That nominal effect is NOT ESTABLISHED on four independent grounds: `EXACT` is a TRUE NULL (same 7 and 11 retained execs, `probes=0`, so it allocates what OFF allocates) and disagrees with OFF by 10.6-13.1 MiB against a 13.83 MiB candidate; the W64 OFF/OFF pair disagrees with ITSELF by 18.2 MiB; one instantiate recorded a NEGATIVE delta (`-5,165,056` B); and `cudaGraphExecDestroy` reclaimed `0` in EVERY cell. Per-instantiate deltas for byte-identical 404-node graphs span 0 to 10,514,432 B and 17 of 27 instantiates in one cell read exactly zero, so these are POOL-GRANULAR readings and the coarse arm's throwaway probes grow that pool exactly like retained execs do. What CAN be priced: one ~390-node executable at **2.08-4.35 MiB**, 10.0-10.6 KB per node — the figure to re-run on a deep checkpoint. **THE MECHANISM INVERTS THIS ROW'S PREMISE.** The driver refuses **43% of probes at 7 buckets and 73% at 11**, every one of them `probe refused a fold (err=910 result=2)` = `cudaErrorGraphExecUpdateFailure` / `cudaGraphExecUpdateErrorTopologyChanged`. The shim's `cudaGraphGetNodes` reading says why false candidates form: the decode graphs are **TWO topologies, 376 and 404 nodes**, mixed across the buckets (`w32_off_a` captured `404 404 376 376 404 404 404`). Every refusal is about TOPOLOGY, never a parameter, so a COARSER key produces MORE false hits rather than more folds — the opposite of what W5's 2-bucket A/B suggested, and W5's `refused=0` is now explained as an artefact of workloads whose buckets only ever SHRANK, so exactly one pair was ever presented. **COST:** W32 OFF 7 instantiates / 0 updates vs COARSE 10 (3 retained + 7 probes) / 11 updates; W64 OFF 11 / 0 vs COARSE **27** (5 retained + 22 probes) / 28 updates — **2.45x the instantiate calls** to retain 6 fewer executables. **Peak transient did NOT double** — in every ON cell live-bytes peak == end, because `Register` destroys the probe before returning, so the feared "double the peak to save the steady state" trade did not occur. **A replay-time re-point DID occur** — 4 and 6 non-probe updates over 88 and 244 replays, ARITHMETIC over two printed totals and not a counter — with every cell exiting 0 and byte-identical, so `Replay`'s transitivity assumption neither aborted nor changed a token; W5 recorded that case as untested. **CAVEATS THAT BOUND THIS RESULT:** the clock pin was **REFUSED inside the lease** (`The current user does not have permission to change clocks for GPU 0000000F:01:00.0`, `clocks_pinned=0`), so **NO time-based figure is attributable** and the instantiate-wall and update-wall figures in `bytes.log` are diagnostics quoted nowhere as a result; `result=2` is ONE driver, ONE GB10, ONE toolkit; only the Qwen3 dense decode driver was exercised, as in W4 and W5; `VT_ASYNC_RUNNER=0` throughout, so the feature is STILL unreachable on the DEFAULT serving path (#1179); and `cudaMemGetInfo` cannot separate an executable's own cost from the pool chunk that satisfied it. **VERDICT, DELIVERED AND NEGATIVE:** `VT_CUDA_GRAPH_DEDUP` stays default OFF, now on MEASUREMENT rather than on silence; `VT_CUDA_GRAPH_DEDUP_COARSE_KEY` alone is a **NO-OP, not merely unsupported** — `GraphDedupCoarseKeyEnabled()` (`src/vt/graph_dedup.h:114`) is read only by the signature builder (`src/vt/graph_dedup_runtime.h:177`), only from `Register`, only under `GraphDedupEnabled()` (`src/vt/cuda/cuda_backend.cu:237`), so with dedup off its sole observable is one stderr line; both on is unsupported. **NOT A CEILING.** Three things would change it and each is traceable: find where the 376/404 split comes from (the FA-2 split-KV grid is the first suspect — a capture that fixes the node set across buckets removes every refusal); an instrument that resolves a single 2-4 MiB executable against driver pool granularity (`cuMemGetAllocationGranularity` or a pool-statistics query); and the same measurement on a 60-80 layer checkpoint, where bytes scale with node count. **Row STAYS `ACTIVE`, argued, and the argument is now narrow.** The MEASUREMENT obligations are discharged and the DECISION is delivered, which is the `DONE` case and it is a real one. Three things stop the flip and none is a checker technicality: the feature is unreachable on the DEFAULT serving path, owned by `ENG-CUDAGRAPH-BREAK` (#1179) and the "nothing lands dead" half of this row; two items still sit under #1162 itself — the device-tier signature stability/discrimination tests and probing `group.current_raw` instead of `raws.front()` to retire the transitivity assumption; and the `DONE` record surface owes a `.agents/parity-ledger.md` entry, a closing-commit owner in place of the claim, an exact test anchor and the RELEASE of `CLAIM-ENG-CUDAGRAPH-DEDUP`, which is an operator act and which this record-only branch does not own. Not `PARTIAL` — nothing upstream is omitted. Not `BLOCKED` — nothing external stops the next step. Full evidence: [benchmark-record.md](benchmark-record.md) entry `ENG-CUDAGRAPH-DEDUP W6`, raw at `/mnt/nas_share/rc/dedup-bytes/` | [eng-cudagraph-dedup.md](specs/eng-cudagraph-dedup.md); analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-DEDUP` ([#1162](https://github.com/mudler/vllm.cpp/issues/1162)) | -| `ENG-CUDAGRAPH-BREAK` | One shared `vt` capture seam that accepts BREAK POINTS, so a forward containing a host-dependent op is still graphed instead of falling out entirely — and so the NINE hand-rolled drivers become one (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight). **Coverage AND CORRECTNESS row, not a throughput row** | T1 | mirror vLLM `CUDAGraphMode.PIECEWISE` splitting at `splitting_ops` (`vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); construction from SGLang BCG `python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/breakable_cuda_graph.py:204-243,246-274,309-333,335-367` @ `f63458b5be` (decorator + runtime stream capture, no compiler); its unit suite `test/registered/cuda_graph/breakable/test_breakable_cuda_graph.py:30,172,230` (305 lines, 11 unit cases) is mapped case for case in the spec's `## Tests to port` | **W6 MOVED THE PREDICATE** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374), 2026-08-19): `GPUModelRunner::execute_model` names the step's ACTUAL uniform query length once through `v1::GraphEligibleQueryLen` (`src/vllm/v1/worker/gpu/cudagraph_dispatch.h`, INERT with no caller since #442 and now called from production) and ships it on `ModelForwardInput::uniform_query_len`; the two Qwen3.5 registrations stop re-deriving that test in twenty duplicated lines each, and both key their slot ring on `(S, q, spec)`. [#1020](https://github.com/mudler/vllm.cpp/issues/1020) CLOSES on the pair, and the key half was a LIVE collision rather than the enabler #1020 called it: `S = spec_step ? B : PadToCaptureSize(B)` puts a 4-request spec step at 1+1 tokens and an 8-request padded decode on the same `S == 8` at the base commit. The widening is BOUNDED by `VT_SPEC_GRAPH_MAX_QLENS` (default 2), because reading the actual length multiplies the spec shape ceiling by `1 + k`. Seven of the nine drivers still read `pure_decode` and are byte-identical. **What did NOT move is 'except at the break points'**: no driver in this tree serves a prefill or a mixed batch under any predicate, so that needs a prefill capture driver nobody has written and whose benefit D5 already refutes on this hardware — a publishable negative, recorded in the spec's `## Owed` as a row-level item. The pre-W6 baseline it replaces: all-or-nothing, `src/vllm/v1/worker/gpu/runner.cpp:1338-1341` routing only `pure_decode`; drivers `qwen3_5.h:275`, `qwen3_5_dense.h:391`, `qwen3_moe.h:117`, `qwen3.h:243`, `deepseek_v2.h:324`, `voxtral.h:126`, plus `deepseek_v4.cpp`, `laguna.cpp` — and the spike found the NINTH already written, `src/vllm/model_executor/models/qwen3_dflash.cpp:771,1091`. The re-derivation is measured, not asserted: `StepDevInputs` (`src/vllm/model_executor/models/qwen3_5.cpp:3894`, the persistent DEVICE input path) exists in ONE driver and `grep -c` returns 0 in `qwen3_moe.cpp`, `qwen3.cpp`, `deepseek_v2.cpp` and `voxtral.cpp`, which is why `src/vllm/model_executor/models/qwen3.cpp`'s `DenseDecodeGraphForward` DECLINES the graph outright when the async device-token mirror is live. **That decline is why this is also a CORRECTNESS row** ([#1179](https://github.com/mudler/vllm.cpp/issues/1179)): a SHIPPED model has already lost its decode graph to the duplication, on the driver's own measurement (`depth-1, graph ON PASS 78/78`; `depth-2, graph OFF PASS 82/82`; `depth-2, graph ON FAIL, slots 1-3 degenerate`), and the fix its comment names is the sibling's `StepDevInputs`. The row still makes NO throughput claim: the prefill refutation on the `ENG-CUDAGRAPH` row (3.8% host idle, >96% GPU-busy, 92.5% glue) stands unchanged; **#1305 ADVANCED AND EXPLICITLY NOT CLOSED, and reading the tree found a larger defect than the issue described** (2026-08-19): `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp` never constructed a `detail::DeviceTokenIdsScope` and neither `qwen3_moe.cpp`'s nor `deepseek_v2.cpp`'s `EmbedInto` ever consulted one, so `ModelForwardInput::device_token_ids` reached NOTHING in either translation unit — the decode graph AND both eager arms embedded the host vector the runner's mirror arm deliberately leaves stale for decode rows. The three registries now publish the scope (the mechanism `qwen3.cpp`, `qwen3_5.cpp`, `mistral_registry.cpp`, `internlm2_registry.cpp` and `llama_registry.cpp` already use), and each decode-graph size slot holds a `vllm::StepTokenIds` (`include/vllm/model_executor/models/step_token_ids.h`) whose destination is a device buffer with a stable address, refreshed through `vt::PersistentStepInput` — host arm for the padded vector, DEVICE arm over the real prefix, both on the main queue so the second is ordered after the combine rather than racing it. That is `vt::PersistentStepInput::RefreshFromDevice`'s FIRST production caller, retiring the staged slice W4 landed with none, and it is the fix `qwen3.cpp`'s own decline comment names rather than a fifth private copy. `qwen3.cpp`'s decline is UNTOUCHED: W4 measured its recorded cause false and its real one is unidentified. **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: 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 ids 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 change itself concedes. #1305's own settlement condition is that battery, it did not run, and the issue stays OPEN with the `ENG-CUDAGRAPH-BREAK` row as owner. | owed: bit-exactness vs eager on every migrated model over MORE than one replay, on a real GPU — **W2 did NOT meet it and says so**: no `rc` lease was obtainable in its window and a CPU harness cannot replay a captured segment, so it moves to W3 with the three drivers of the same shape (G1); the host-lifetime contract of `decode-graph-scratch-uaf-2026-07-18.md` enforced AT the seam — D1's INPUT half, making the intermediates a segment reads unavailable to the `DevicePool` free list, which becomes live only for the first PIECEWISE production capture (W4); the auxiliary-stream auto-join before every segment close (`:353-361`, spec D10), live at `src/vllm/model_executor/models/qwen3_5.cpp:6254-6255,6384` and `src/vllm/model_executor/models/laguna.cpp:2572-2576,2612` (W4, W5). **Delivered by W1** ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the reachability mutation (performed; deleting the call site reds `tests/vllm/models/test_qwen3_break_point.cpp` and leaves the unit suite green); the ported SGLang unit cases with their arithmetic chains and post-replay assertions; and the break-function OUTPUT writeback (`replay_fn`/`_copy_output` `breakable_cuda_graph.py:231-235,172-201`, spec D9), whose destination is a `vt::BreakSlot` the seam owns rather than a caller reference it cannot outlive **W6 gates** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): G2 at THREE levels because the claim has three parts — the engine (`tests/vllm/v1/spec_decode/test_mtp_depth.cpp`, a real LoadedEngine/EngineCore/Scheduler/runner stack, asserting `clamped_spec_steps`, measured 0/0/1/2/4 at k=1/2/3/4/6), the driver (`tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp`, two spec shapes of equal S and different q getting two rings and two captures), and the arithmetic (`tests/vllm/v1/worker/gpu/test_cudagraph_dispatch.cpp`). Five detecting mutations, each reddening ONE level and leaving the others green, plus an over-fire control. A SIXTH mutation was NOT detected and forced a repair: the per-request verify conjunct is redundant on every model that reads the field (both are GDN hybrids whose prefill trips the first conjunct), so it moved into `GraphEligibleQueryLen` where a mutation reds 4 assertions, and the spec records it as unreached defence in depth. **G1 re-run on `thor:gpu0` (sm_110, driver 595.78, nvcc 13.0.88): 2066 assertions, 0 failed, 0 differing on all five migrated drivers — W6 moves no logit.** The ring key's own device case is BLOCKED by [#1380](https://github.com/mudler/vllm.cpp/issues/1380), a pre-existing `cudaMalloc` inside a capturing stream on the spec arm that W6 neither caused nor regressed; the case PINS that refusal and is written to fail when #1380 is fixed.; **#1305 (2026-08-19)**: `tests/vllm/models/test_moe_async_device_ids.cpp`, entered at `ModelRegistry::Forward` over a synthetic safetensors checkpoint for `Qwen3MoeForCausalLM` and `DeepseekV2ForCausalLM` — the production entry point, not the driver type. Three runs each: right host ids and no mirror as the reference, stale host ids and no mirror as the CONTROL that must differ, stale host ids with the truth reaching the model ONLY through `device_token_ids` as the gate. RED first at 2 cases / 65 assertions / 10 failed / exit 1, with 800 of 800 logit values differing over four steps on both architectures and every counter at 0; GREEN after at 65 of 65, exit 0. TWO mutations, each compiled clean and each restored by sha256: deleting the registry's scope line — the production call site — reds 4 assertions across both cases and puts all 800 values back, and swapping the seam's DEVICE arm for its HOST arm leaves the logits BIT IDENTICAL at 0 of 800 differing and reds only `device_refreshes` and `host_refreshes`, which is the arm no token gate can see. Neighbours green on the same binary: `test_qwen3_moe_decode_graph_seam` 228 of 228, `test_deepseek_v2_decode_graph_seam` 230 of 230, `test_qwen3_decode_graph_seam` 231 of 231, `test_voxtral_decode_graph_seam` 230 of 230, `test_breakable_graph` 265 of 265, `test_persistent_step_input` 66 of 66, `test_model_registry` 924 of 924, `test_qwen3_moe_forward` 504 of 504, `test_deepseek_v2_forward` 1052 of 1052. **NOT measured:** the depth-2 four-concurrent battery on a device, which needs a GPU and a real checkpoint; owed. **Found red on `main` and NOT caused here:** `test_qwen3_5_decode_graph_seam` exits 139 while its assertion line reads 135 of 135 passed ([#1390](https://github.com/mudler/vllm.cpp/issues/1390)); re-measured on this branch at exit 139 with 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 run to run on ONE unchanged binary — three consecutive runs of the same baseline binary gave 6 passed, 2 failed and 141 assertions, then no summary at all, then no summary at all. The exit code is the only stable observation, so no assertion count from that file carries a verdict. **THE FRESH REVIEW FOUND THE GATE ABOVE COVERED HALF OF WHAT THE CHANGE CLAIMS** and the repair widened it to 6 cases / 191 assertions / exit 0. What was ungated: the EAGER arms of both models — the half no graph refusal could have mitigated — and the THIRD registration, `glm4_moe_lite_registry.cpp`. Deleting the `TakeDeviceTokenIds` + `d.b.Copy` block from BOTH `EmbedInto` overloads left the old gate green at 2/2 and 65/65; deleting the GLM registry's two-line scope did too. The lane is now selected by the registry's OWN predicate: 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 so a case cannot drift onto the other lane and stay green. Three detecting mutations, each compiled clean and each restored: the two `EmbedInto` call sites reds the 3 EAGER cases only (exit 1, 3/6); the GLM scope reds the 2 GLM cases only (exit 1, 4/6); the seam's `RefreshFromDevice` call reds the 3 GRAPH cases only (exit 1, 3/6). A fourth mutation FAILED TO BUILD under `-Wunused-parameter` and its verdict was DISCARDED rather than read as a pass. **Still owed, and not implied:** the behavioural half of the device contract — that the copy reads DEVICE memory, and that it is main-queue-ordered after the combine — is untestable on the CPU backend, where `Backend::Alloc` returns host-addressable memory and both refresh arms reduce to 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, which gate the instrument rather than the behaviour. | spec [eng-cudagraph-break.md](specs/eng-cudagraph-break.md) (W0 spike DONE 2026-08-18: the existing `vt` capture vocabulary `include/vt/backend.h:208-222` expresses a SEGMENTED capture with NO new virtual, because `EndCaptureGraph` stores nothing (`src/vt/cuda/cuda_backend.cu:225-232`); a break point is expressible with one `thread_local` capture pointer plus a free function, no compiler and no decorator); **W1 DONE 2026-08-18 ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the seam LANDS** — `vt::BreakableGraph`, `vt::GraphCaptureScope` and `vt::GraphBreak` (`include/vt/breakable_graph.h`, `src/vt/breakable_graph.cpp`), the SGLang unit suite ported case for case (`tests/vt/test_breakable_graph.cpp`, 24 cases / 163 assertions, re-derived 2026-08-18 by `ninja test_breakable_graph && ./build/tests/test_breakable_graph`; the recorded 14/81 never re-derived at any head of this branch), and ONE break point registered at the DENSE ATTENTION ENTRY of `Qwen3ForCausalLM` (`src/vllm/model_executor/models/qwen3.cpp`, inside `RunLayer`). **The exit criterion W0 deliberately left open is ANSWERED on a leased GPU:** `cudaStreamEndCapture` then `cudaStreamBeginCapture` on the SAME stream mid-forward with EAGER work between is LEGAL under `cudaStreamCaptureModeThreadLocal` (`src/vt/cuda/cuda_backend.cu:204-206`) — `orin:gpu0` via an `rc` lease, driver 12060, 3 replays with fresh inputs, 0 mismatches, bare zero-work re-begin legal too. G2 reachability is `tests/vllm/models/test_qwen3_break_point.cpp`, which drives the production `Qwen3DenseModel::Forward` with a scope open and counts `num_hidden_layers + 1` segments (mutation: delete the call site ⇒ 1 segment ⇒ RED), and holds G4 in the same case at 500 logits / 0 differing bit for bit. STAGED SLICE, named: the scope and the container are not yet ENTERED from a production step — no driver opens a scope until W2 migrates `Qwen3DenseDecodeGraph` — and the spec's `## Owed` lists it with W2 as owner, alongside the D10 auxiliary-stream auto-join (W4/W5), G5's ROCm/Tenstorrent arms (W3) and G1 on a real GPU (W2). **The capture-failure drain is NOT among them: it landed HERE**, as behaviour (`std::uncaught_exceptions()` compared against the depth recorded at scope entry, so a break function or ordinary model code throwing mid-capture destroys the partial container instead of handing back a forward that reports `captured() == true`) and as three gated arms (tests 13a, 13b, 13c). The spec's `## Owed` strikes the item through and reads DELIVERED in W1; this cell said the opposite until 2026-08-18 because `cba969857` re-derived field 6 alone. **W2 DONE 2026-08-18 ([#1261](https://github.com/mudler/vllm.cpp/issues/1261)): `Qwen3DenseDecodeGraph` MIGRATED and the seam is ENTERED from a production step**, which retires W1's staged slice. `Qwen3DenseDecodeGraph::Step` opens a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` and replays through `BreakableGraph::Replay`; the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle, the `bool captured` flag, the `DestroyGraph` loop and the driver's own `VLLM_CPP_CUDAGRAPH` read are gone (re-derivation items 1, 2, 5, 6). The migration ADDED `vt::GraphCaptureMode`, mirroring vLLM's `CUDAGraphMode` (`vllm/config/compilation.py:59-63`), whose v1 default `FULL_AND_PIECEWISE` (`:63`) is documented at `:630-632` as a FULL graph for DECODE batches and a piecewise one for prefill/mixed, with `decode_mode()` (`:65-66`) selecting the full half and the runtime reading it at `vllm/v1/worker/gpu/cudagraph_utils.py:185-186`. A decode driver opened `kPiecewise` would have turned a fully graphed decode step into ONE EAGER ATTENTION CALL PER LAYER between graph replays — not vLLM's decode behaviour, and invisible to every token gate here. `GraphBreak` in a `kFull` scope takes the pass-through arm and `AppendBreak` REFUSES a registration in that mode. G2 is `tests/vllm/models/test_qwen3_decode_graph_seam.cpp` (3 cases / 124 assertions), which asserts the SEAM's counters because a driver calling `Backend::ReplayGraph` directly leaves an identical backend log; the mutation restoring the pre-W2 raw pair (18 lines, compiled clean) left `test_breakable_graph` 27/27, `test_qwen3_break_point` 2/2 and `test_qwen3_forward` 10/10 GREEN and reddened only this file. G4 in the same file: capture step vs `Qwen3DenseModel::Forward`, 100 logits, 0 differing. **The async decline at `qwen3.cpp` STANDS and is now GATED in both arms**: migrating the capture does not move the INPUTS, so the depth-2 race is untouched, and the fix is `StepDevInputs` as a SEAM capability, which is W4. **G1 is NOT met by W2** and is recorded owed rather than implied. **W3 DONE 2026-08-19 ([#1291](https://github.com/mudler/vllm.cpp/issues/1291)): the three remaining PLAIN BATCHED drivers migrate — `Qwen3MoeDecodeGraph`, `VoxtralDecodeGraph`, `DeepseekV2DecodeGraph` — one commit each, each with its own RED-first G2 gate.** Four of the nine drivers are now on the seam, and the six batched-driver `VLLM_CPP_CUDAGRAPH` reads `## Our baseline` item 1 counted are down to TWO, both in `qwen3_5.cpp` (W4). Each gate asserts the SEAM's counters and not the backend log, because a driver that kept its raw pair produces identical logits, an identical backend log and an identical `replay_count()`; red-first on four assertions each (`test_qwen3_moe_decode_graph_seam` 222/226, `test_voxtral_decode_graph_seam` 224/228, `test_deepseek_v2_decode_graph_seam` 224/228, all exit 1), green 3/3 each after. The G2 mutation — restoring each pre-W3 driver file, 25/102, 23/92 and 25/94 lines, each compiled clean — reddens ONLY its own gate and leaves `test_breakable_graph` 216/216 and W2's `test_qwen3_decode_graph_seam` 231/231 green. The gate harness is now SHARED (`tests/vllm/models/decode_graph_seam_harness.h`); three more copies inside `tests/` would have reproduced the duplication this row removes from `src/`. **G1 IS DELIVERED and is no longer owed** — the item W1 and W2 both carried. `tests/vllm/models/test_decode_graph_seam_g1_cuda.cpp` runs each driver COLD, CAPTURE and THREE consecutive replays against its own eager arm (selected by `max_num_reqs == 0`, so both arms are one binary on one device rather than two builds, each with its OWN device KV cache) on `thor:gpu0` through an `rc` lease — NVIDIA Thor sm_110, driver 595.78, nvcc 13.0.88, source `c905bb536`, 32 `.cu.o` objects, binary resolving `libcudart.so.13`/`libcublasLt.so.13`: **3 cases, 1600 assertions, exit 0, `5 steps x 100 logits, 0 differing, 4 replays` per driver.** The COUNT carries that claim, not the status line: with no CUDA backend the same file prints `SUCCESS!` over `assertions: 0`. Bounded honestly — synthetic tiny models rather than a checkpoint, and W2's driver shares the seam by argument rather than by measurement. **W3 also found a gate that could not fail.** The three gates' `breaks_registered == 0` mode guard is a TAUTOLOGY for any model with no registered break point, and the one production `vt::GraphBreak` in the tree is W1's in `qwen3.cpp`: flipping `kFull` to `kPiecewise` in `qwen3_moe.cpp`, one token, compiled clean and left that gate GREEN at 226/226. The mode was UNOBSERVABLE from outside a driver, so `vt::GraphBreakStats` gains `full_scopes`/`piecewise_scopes`, counted in `GraphCaptureScope`'s constructor on the ACTIVE path only, with an inert-scope control; the same flip now reds all three gates on exactly those two assertions. **NO break point is registered in these three models, deliberately**: under `kFull` it would be pass-through machinery no gate can exercise, and the break-point set is what the PIECEWISE arm needs (W4/W6). **The async decline, per driver:** Voxtral needs none (its only construction site is `VoxtralGenerateGreedy`, unreachable from the runner); Qwen3-Coder and DeepSeek carry a NEW FINDING instead — `qwen3_moe_registry.cpp:107`, `deepseek_v2_registry.cpp:106` and `glm4_moe_lite_registry.cpp:125` route an async step into a host-vector replay with no `device_token_ids` check at all, filed [#1305](https://github.com/mudler/vllm.cpp/issues/1305) with W4 as owner rather than mitigated on a measurement W3 cannot make. G5's ROCm/Tenstorrent arm is NOT discharged and moves to W5: the fleet carries no such device, so it is blocked on hardware rather than unattempted. **W4 DONE 2026-08-19 ([#1307](https://github.com/mudler/vllm.cpp/issues/1307)): the persistent device input path becomes a SEAM CAPABILITY, and the two Qwen3.5 drivers migrate.** `vt::PersistentStepInput` (`include/vt/persistent_step_input.h`, `src/vt/persistent_step_input.cpp`) binds a capture-stable device destination the DRIVER owns together with its pinned host staging block, and refreshes it in place from a host source or a DEVICE one; it owns the address-stability rule as a REFUSAL, the staging block, and the refreshing ARM as an observable (`last_source()`, `vt::StepInputStats`), and deliberately NOT the device allocation, because `Qwen3_5DecodeGraph` draws its retained inputs from a DEDICATED `DevicePool` so they never pop a block the captured forward's scratch then needs (D3). RED-first against a stub with the declared API and no guarantees: `tests/vt/test_persistent_step_input.cpp` 9 cases / 0 passed / 59 assertions / 32 failed / exit 1, GREEN after at 9/9 and 59/59; three mutations (delete the capacity refusal, make a null device source a silent no-op, collapse the host arm out of staging) each compiled clean and each reds exactly one case. `Qwen3_5DecodeGraph` and `Qwen3_5DenseDecodeGraph` open a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` in `kFull` and replay through it, and their `PinnedStepInputs`/`StageStepInputs` staging now runs THROUGH the capability, which is what makes it reachable rather than a class with a unit test. **Six of the nine drivers are on the seam** and `grep -rn 'std::getenv("VLLM_CPP_CUDAGRAPH")' src/` returns exactly ONE line, `src/vt/breakable_graph.cpp:61` — one switch, at last. Gate `tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp` RED-first on the MoE driver's five seam assertions (3 cases / 62 assertions / 5 failed / exit 1) and GREEN after at 7/7 and 129, G4 reading `40 values, 0 differing` per driver; G2 mutations: the whole pre-W4 file restored reds BOTH drivers (296 lines, 10 assertions), the MoE replay bypassing the container reds ONLY the MoE case (7 lines), the MoE `kFull`->`kPiecewise` flip reds ONLY its mode counters (3 lines), and deleting the `StageStepInputs` call site reds ONLY the reachability case while `test_persistent_step_input` stays 59/59 green — the difference between a class that works and a capability something reaches. **W4 FALSIFIED THIS ROW'S OWN PREMISE, which is its most important result.** This record and the spec both said the fix `qwen3.cpp`'s `DenseDecodeGraphForward`'s decline names already existed as `StepDevInputs`. It does not: `StepDevInputs` has NO token-id member, and its pinned sibling `PinnedStepInputs::token_ids` was allocated at capture, filled every step, zeroed by the poison hook, and NEVER uploaded or read — the embed runs OUTSIDE the captured region from the HOST vector in every batched driver, so **the decode graph carries no token ids to the device in ANY driver**. The dead block is removed. Consequently the DECLINE STANDS and [#1305](https://github.com/mudler/vllm.cpp/issues/1305) STAYS OPEN: W4 also read the decline's recorded cause against the tree at its own parent and found it falsified (the `DeviceTokenIdsScope` WAS live on the graph path, consumed by `EmbedInto` on all three arms at `qwen3.cpp:610,621,644 @ 338cbbfd1^`), so the measured failure is real and its mechanism is unidentified — not a state from which a refactor may retire a mitigation. The async battery was NOT run and W4 says so plainly: it needs `dgx` WITH the Qwen3-0.6B/4B checkpoints, `dgx:gpu0` was held by another session for W4's whole window, and W4's lease was `thor:gpu0`. Still NO throughput claim. W5 DONE 2026-08-19 ([#1335](https://github.com/mudler/vllm.cpp/issues/1335)): the THREE SINGLE-SHAPE drivers migrate — the DFlash draft graph, the DeepSeek V4 decode graph and the Laguna decode graph, whose own note at `laguna.cpp:2116-2119` asked for this seam by name and named V4's as the sibling that moves with it. **NINE OF NINE DRIVERS ARE 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 three per-model rollback switches stay (each an A/B lever for one driver); `VLLM_CPP_CUDAGRAPH` reaches all three for the first time. **D10, the auxiliary-stream fork/join, is DISCHARGED and REACHED** — `GraphCaptureScope` owns the outstanding-fork set and joins it before `EndCaptureGraph` (port of `breakable_cuda_graph.py:353-361` plus the `wait_stream` hook `:101-153`), registered by `vt::GraphNoteFork`/`GraphNoteJoin` from `laguna.cpp:2572-2576,2612`, the only fork inside a captured region by construction. Every prior stage opened `kFull`, which has ONE segment and so no between-segments window, so the rule could not be exercised before W5 and untested machinery was not landed for it. Gated as a COUNTER and an ORDER out of one backend trace, five arms including the control where the model joins first, and two mutations (deleting the join reds only the new case on 5 assertions; making it over-fire reds it on 8). DFlash is the ONE single-shape driver gateable without a GPU, because its admission predicate names neither a device type nor a kernel registry: `test_qwen3_dflash_decode_graph_seam.cpp` RED-first 3 cases/0 passed/16 assertions/7 failed exit 1, GREEN after 3/18, and the G2 mutation reds ONLY that file while seven other suites — the driver's own `test_dflash_propose` included — stay green. **G1 RE-RUN at W5's head on `thor:gpu0`** (sm_110, driver 595.78, nvcc 13.0.88, 32 `.cu.o`, source `79dc6b5bd`) because D10 put a join on the path of EVERY segment close, so the seam changed underneath the five measured drivers: `test_decode_graph_seam_g1_cuda` 5 cases / 2066 assertions / 0 failed, each reading `0 differing, 4 replays`, plus `test_breakable_graph` 265 on the same device. **And the one thing a green build could NOT have told us was measured separately**: Laguna's capture class sits behind `#ifdef VT_MARLIN_NVFP4`, so a passing build is the SAME OBSERVATION as one that compiled the region out. `-DVT_MARLIN_NVFP4=1` is on `laguna.cpp`'s own compile command, and an undeclared identifier injected immediately after its `GraphCaptureScope` line FAILED the object build under `-Werror` (`laguna.cpp:2735`) against an rc-0 baseline, restoring to an empty diff; the identical mutation on V4 failed at `deepseek_v4.cpp:1921`. Both migrated regions are COMPILED, which retires the could-not-even-be-built half. **G1 for all three and G2 for V4 and Laguna are OWED on hardware**, per driver and per reason: V4's `CanRunResidentDecode` refuses `kCPU` and needs the four CUDA-registered kernel families, Laguna's capture class exists only under `VT_MARLIN_NVFP4`. G5's ROCm/Tenstorrent arm stays BLOCKED — the fleet is all NVIDIA — and its owner moves from W5 to the ROW. Still NO throughput claim; analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) W6 DONE 2026-08-19 ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): the eligibility predicate, #1020, and the negative result on the piecewise arm. | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-BREAK-W6`; [#1163](https://github.com/mudler/vllm.cpp/issues/1163), [#1192](https://github.com/mudler/vllm.cpp/issues/1192), [#1261](https://github.com/mudler/vllm.cpp/issues/1261), [#1291](https://github.com/mudler/vllm.cpp/issues/1291), [#1307](https://github.com/mudler/vllm.cpp/issues/1307), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1020](https://github.com/mudler/vllm.cpp/issues/1020), [#1335](https://github.com/mudler/vllm.cpp/issues/1335), [#1374](https://github.com/mudler/vllm.cpp/issues/1374), [#1380](https://github.com/mudler/vllm.cpp/issues/1380), [#1390](https://github.com/mudler/vllm.cpp/issues/1390) | +| `ENG-CUDAGRAPH-BREAK` | One shared `vt` capture seam that accepts BREAK POINTS, so a forward containing a host-dependent op is still graphed instead of falling out entirely — and so the NINE hand-rolled drivers become one (count corrected 2026-08-18, [#1179](https://github.com/mudler/vllm.cpp/issues/1179); `9bc4d7f44` recorded eight). **Coverage AND CORRECTNESS row, not a throughput row** | T1 | mirror vLLM `CUDAGraphMode.PIECEWISE` splitting at `splitting_ops` (`vllm/config/compilation.py:60-63,517,615,630` @ `555967922`); construction from SGLang BCG `python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/breakable_cuda_graph.py:204-243,246-274,309-333,335-367` @ `f63458b5be` (decorator + runtime stream capture, no compiler); its unit suite `test/registered/cuda_graph/breakable/test_breakable_cuda_graph.py:30,172,230` (305 lines, 11 unit cases) is mapped case for case in the spec's `## Tests to port` | **W6 MOVED THE PREDICATE** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374), 2026-08-19): `GPUModelRunner::execute_model` names the step's ACTUAL uniform query length once through `v1::GraphEligibleQueryLen` (`src/vllm/v1/worker/gpu/cudagraph_dispatch.h`, INERT with no caller since #442 and now called from production) and ships it on `ModelForwardInput::uniform_query_len`; the two Qwen3.5 registrations stop re-deriving that test in twenty duplicated lines each, and both key their slot ring on `(S, q, spec)`. [#1020](https://github.com/mudler/vllm.cpp/issues/1020) CLOSES on the pair, and the key half was a LIVE collision rather than the enabler #1020 called it: `S = spec_step ? B : PadToCaptureSize(B)` puts a 4-request spec step at 1+1 tokens and an 8-request padded decode on the same `S == 8` at the base commit. The widening is BOUNDED by `VT_SPEC_GRAPH_MAX_QLENS` (default 2), because reading the actual length multiplies the spec shape ceiling by `1 + k`. Seven of the nine drivers still read `pure_decode` and are byte-identical. **What did NOT move is 'except at the break points'**: no driver in this tree serves a prefill or a mixed batch under any predicate, so that needs a prefill capture driver nobody has written and whose benefit D5 already refutes on this hardware — a publishable negative, recorded in the spec's `## Owed` as a row-level item. The pre-W6 baseline it replaces: all-or-nothing, `src/vllm/v1/worker/gpu/runner.cpp:1338-1341` routing only `pure_decode`; drivers `qwen3_5.h:275`, `qwen3_5_dense.h:391`, `qwen3_moe.h:117`, `qwen3.h:243`, `deepseek_v2.h:324`, `voxtral.h:126`, plus `deepseek_v4.cpp`, `laguna.cpp` — and the spike found the NINTH already written, `src/vllm/model_executor/models/qwen3_dflash.cpp:771,1091`. The re-derivation is measured, not asserted: `StepDevInputs` (`src/vllm/model_executor/models/qwen3_5.cpp:3894`, the persistent DEVICE input path) exists in ONE driver and `grep -c` returns 0 in `qwen3_moe.cpp`, `qwen3.cpp`, `deepseek_v2.cpp` and `voxtral.cpp`, which is why `src/vllm/model_executor/models/qwen3.cpp`'s `DenseDecodeGraphForward` DECLINES the graph outright when the async device-token mirror is live. **That decline is why this is also a CORRECTNESS row** ([#1179](https://github.com/mudler/vllm.cpp/issues/1179)): a SHIPPED model has already lost its decode graph to the duplication, on the driver's own measurement (`depth-1, graph ON PASS 78/78`; `depth-2, graph OFF PASS 82/82`; `depth-2, graph ON FAIL, slots 1-3 degenerate`), and the fix its comment names is the sibling's `StepDevInputs`. The row still makes NO throughput claim: the prefill refutation on the `ENG-CUDAGRAPH` row (3.8% host idle, >96% GPU-busy, 92.5% glue) stands unchanged; **#1305 ADVANCED AND EXPLICITLY NOT CLOSED, and reading the tree found a larger defect than the issue described** (2026-08-19): `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp` never constructed a `detail::DeviceTokenIdsScope` and neither `qwen3_moe.cpp`'s nor `deepseek_v2.cpp`'s `EmbedInto` ever consulted one, so `ModelForwardInput::device_token_ids` reached NOTHING in either translation unit — the decode graph AND both eager arms embedded the host vector the runner's mirror arm deliberately leaves stale for decode rows. The three registries now publish the scope (the mechanism `qwen3.cpp`, `qwen3_5.cpp`, `mistral_registry.cpp`, `internlm2_registry.cpp` and `llama_registry.cpp` already use), and each decode-graph size slot holds a `vllm::StepTokenIds` (`include/vllm/model_executor/models/step_token_ids.h`) whose destination is a device buffer with a stable address, refreshed through `vt::PersistentStepInput` — host arm for the padded vector, DEVICE arm over the real prefix, both on the main queue so the second is ordered after the combine rather than racing it. That is `vt::PersistentStepInput::RefreshFromDevice`'s FIRST production caller, retiring the staged slice W4 landed with none, and it is the fix `qwen3.cpp`'s own decline comment names rather than a fifth private copy. `qwen3.cpp`'s decline is UNTOUCHED: W4 measured its recorded cause false and its real one is unidentified. **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: 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 ids 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 change itself concedes. #1305's own settlement condition is that battery, it did not run, and the issue stays OPEN with the `ENG-CUDAGRAPH-BREAK` row as owner. | owed: bit-exactness vs eager on every migrated model over MORE than one replay, on a real GPU — **W2 did NOT meet it and says so**: no `rc` lease was obtainable in its window and a CPU harness cannot replay a captured segment, so it moves to W3 with the three drivers of the same shape (G1); the host-lifetime contract of `decode-graph-scratch-uaf-2026-07-18.md` enforced AT the seam — D1's INPUT half, making the intermediates a segment reads unavailable to the `DevicePool` free list, which becomes live only for the first PIECEWISE production capture (W4); the auxiliary-stream auto-join before every segment close (`:353-361`, spec D10), live at `src/vllm/model_executor/models/qwen3_5.cpp:6254-6255,6384` and `src/vllm/model_executor/models/laguna.cpp:2572-2576,2612` (W4, W5). **Delivered by W1** ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the reachability mutation (performed; deleting the call site reds `tests/vllm/models/test_qwen3_break_point.cpp` and leaves the unit suite green); the ported SGLang unit cases with their arithmetic chains and post-replay assertions; and the break-function OUTPUT writeback (`replay_fn`/`_copy_output` `breakable_cuda_graph.py:231-235,172-201`, spec D9), whose destination is a `vt::BreakSlot` the seam owns rather than a caller reference it cannot outlive **W6 gates** ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): G2 at THREE levels because the claim has three parts — the engine (`tests/vllm/v1/spec_decode/test_mtp_depth.cpp`, a real LoadedEngine/EngineCore/Scheduler/runner stack, asserting `clamped_spec_steps`, measured 0/0/1/2/4 at k=1/2/3/4/6), the driver (`tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp`, two spec shapes of equal S and different q getting two rings and two captures), and the arithmetic (`tests/vllm/v1/worker/gpu/test_cudagraph_dispatch.cpp`). Five detecting mutations, each reddening ONE level and leaving the others green, plus an over-fire control. A SIXTH mutation was NOT detected and forced a repair: the per-request verify conjunct is redundant on every model that reads the field (both are GDN hybrids whose prefill trips the first conjunct), so it moved into `GraphEligibleQueryLen` where a mutation reds 4 assertions, and the spec records it as unreached defence in depth. **G1 re-run on `thor:gpu0` (sm_110, driver 595.78, nvcc 13.0.88): 2066 assertions, 0 failed, 0 differing on all five migrated drivers — W6 moves no logit.** The ring key's own device case is BLOCKED by [#1380](https://github.com/mudler/vllm.cpp/issues/1380), a pre-existing `cudaMalloc` inside a capturing stream on the spec arm that W6 neither caused nor regressed; the case PINS that refusal and is written to fail when #1380 is fixed. **#1305 (2026-08-19)**: `tests/vllm/models/test_moe_async_device_ids.cpp`, entered at `ModelRegistry::Forward` over a synthetic safetensors checkpoint for `Qwen3MoeForCausalLM` and `DeepseekV2ForCausalLM` — the production entry point, not the driver type. Three runs each: right host ids and no mirror as the reference, stale host ids and no mirror as the CONTROL that must differ, stale host ids with the truth reaching the model ONLY through `device_token_ids` as the gate. RED first at 2 cases / 65 assertions / 10 failed / exit 1, with 800 of 800 logit values differing over four steps on both architectures and every counter at 0; GREEN after at 65 of 65, exit 0. TWO mutations, each compiled clean and each restored by sha256: deleting the registry's scope line — the production call site — reds 4 assertions across both cases and puts all 800 values back, and swapping the seam's DEVICE arm for its HOST arm leaves the logits BIT IDENTICAL at 0 of 800 differing and reds only `device_refreshes` and `host_refreshes`, which is the arm no token gate can see. Neighbours green on the same binary: `test_qwen3_moe_decode_graph_seam` 228 of 228, `test_deepseek_v2_decode_graph_seam` 230 of 230, `test_qwen3_decode_graph_seam` 231 of 231, `test_voxtral_decode_graph_seam` 230 of 230, `test_breakable_graph` 265 of 265, `test_persistent_step_input` 66 of 66, `test_model_registry` 924 of 924, `test_qwen3_moe_forward` 504 of 504, `test_deepseek_v2_forward` 1052 of 1052. **NOT measured:** the depth-2 four-concurrent battery on a device, which needs a GPU and a real checkpoint; owed. **Found red on `main` and NOT caused here:** `test_qwen3_5_decode_graph_seam` exits 139 while its assertion line reads 135 of 135 passed ([#1390](https://github.com/mudler/vllm.cpp/issues/1390)); re-measured on this branch at exit 139 with 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 run to run on ONE unchanged binary — three consecutive runs of the same baseline binary gave 6 passed, 2 failed and 141 assertions, then no summary at all, then no summary at all. The exit code is the only stable observation, so no assertion count from that file carries a verdict. **THE FRESH REVIEW FOUND THE GATE ABOVE COVERED HALF OF WHAT THE CHANGE CLAIMS** and the repair widened it to 6 cases / 191 assertions / exit 0. What was ungated: the EAGER arms of both models — the half no graph refusal could have mitigated — and the THIRD registration, `glm4_moe_lite_registry.cpp`. Deleting the `TakeDeviceTokenIds` + `d.b.Copy` block from BOTH `EmbedInto` overloads left the old gate green at 2/2 and 65/65; deleting the GLM registry's two-line scope did too. The lane is now selected by the registry's OWN predicate: 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 so a case cannot drift onto the other lane and stay green. Three detecting mutations, each compiled clean and each restored: the two `EmbedInto` call sites reds the 3 EAGER cases only (exit 1, 3/6); the GLM scope reds the 2 GLM cases only (exit 1, 4/6); the seam's `RefreshFromDevice` call reds the 3 GRAPH cases only (exit 1, 3/6). A fourth mutation FAILED TO BUILD under `-Wunused-parameter` and its verdict was DISCARDED rather than read as a pass. **Still owed, and not implied:** the behavioural half of the device contract — that the copy reads DEVICE memory, and that it is main-queue-ordered after the combine — is untestable on the CPU backend, where `Backend::Alloc` returns host-addressable memory and both refresh arms reduce to 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, which gate the instrument rather than the behaviour. | spec [eng-cudagraph-break.md](specs/eng-cudagraph-break.md) (W0 spike DONE 2026-08-18: the existing `vt` capture vocabulary `include/vt/backend.h:208-222` expresses a SEGMENTED capture with NO new virtual, because `EndCaptureGraph` stores nothing (`src/vt/cuda/cuda_backend.cu:225-232`); a break point is expressible with one `thread_local` capture pointer plus a free function, no compiler and no decorator); **W1 DONE 2026-08-18 ([#1192](https://github.com/mudler/vllm.cpp/issues/1192)): the seam LANDS** — `vt::BreakableGraph`, `vt::GraphCaptureScope` and `vt::GraphBreak` (`include/vt/breakable_graph.h`, `src/vt/breakable_graph.cpp`), the SGLang unit suite ported case for case (`tests/vt/test_breakable_graph.cpp`, 24 cases / 163 assertions, re-derived 2026-08-18 by `ninja test_breakable_graph && ./build/tests/test_breakable_graph`; the recorded 14/81 never re-derived at any head of this branch), and ONE break point registered at the DENSE ATTENTION ENTRY of `Qwen3ForCausalLM` (`src/vllm/model_executor/models/qwen3.cpp`, inside `RunLayer`). **The exit criterion W0 deliberately left open is ANSWERED on a leased GPU:** `cudaStreamEndCapture` then `cudaStreamBeginCapture` on the SAME stream mid-forward with EAGER work between is LEGAL under `cudaStreamCaptureModeThreadLocal` (`src/vt/cuda/cuda_backend.cu:204-206`) — `orin:gpu0` via an `rc` lease, driver 12060, 3 replays with fresh inputs, 0 mismatches, bare zero-work re-begin legal too. G2 reachability is `tests/vllm/models/test_qwen3_break_point.cpp`, which drives the production `Qwen3DenseModel::Forward` with a scope open and counts `num_hidden_layers + 1` segments (mutation: delete the call site ⇒ 1 segment ⇒ RED), and holds G4 in the same case at 500 logits / 0 differing bit for bit. STAGED SLICE, named: the scope and the container are not yet ENTERED from a production step — no driver opens a scope until W2 migrates `Qwen3DenseDecodeGraph` — and the spec's `## Owed` lists it with W2 as owner, alongside the D10 auxiliary-stream auto-join (W4/W5), G5's ROCm/Tenstorrent arms (W3) and G1 on a real GPU (W2). **The capture-failure drain is NOT among them: it landed HERE**, as behaviour (`std::uncaught_exceptions()` compared against the depth recorded at scope entry, so a break function or ordinary model code throwing mid-capture destroys the partial container instead of handing back a forward that reports `captured() == true`) and as three gated arms (tests 13a, 13b, 13c). The spec's `## Owed` strikes the item through and reads DELIVERED in W1; this cell said the opposite until 2026-08-18 because `cba969857` re-derived field 6 alone. **W2 DONE 2026-08-18 ([#1261](https://github.com/mudler/vllm.cpp/issues/1261)): `Qwen3DenseDecodeGraph` MIGRATED and the seam is ENTERED from a production step**, which retires W1's staged slice. `Qwen3DenseDecodeGraph::Step` opens a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` and replays through `BreakableGraph::Replay`; the hand-rolled `BeginCapture`/`EndCaptureGraph` pair, the raw `void*` handle, the `bool captured` flag, the `DestroyGraph` loop and the driver's own `VLLM_CPP_CUDAGRAPH` read are gone (re-derivation items 1, 2, 5, 6). The migration ADDED `vt::GraphCaptureMode`, mirroring vLLM's `CUDAGraphMode` (`vllm/config/compilation.py:59-63`), whose v1 default `FULL_AND_PIECEWISE` (`:63`) is documented at `:630-632` as a FULL graph for DECODE batches and a piecewise one for prefill/mixed, with `decode_mode()` (`:65-66`) selecting the full half and the runtime reading it at `vllm/v1/worker/gpu/cudagraph_utils.py:185-186`. A decode driver opened `kPiecewise` would have turned a fully graphed decode step into ONE EAGER ATTENTION CALL PER LAYER between graph replays — not vLLM's decode behaviour, and invisible to every token gate here. `GraphBreak` in a `kFull` scope takes the pass-through arm and `AppendBreak` REFUSES a registration in that mode. G2 is `tests/vllm/models/test_qwen3_decode_graph_seam.cpp` (3 cases / 124 assertions), which asserts the SEAM's counters because a driver calling `Backend::ReplayGraph` directly leaves an identical backend log; the mutation restoring the pre-W2 raw pair (18 lines, compiled clean) left `test_breakable_graph` 27/27, `test_qwen3_break_point` 2/2 and `test_qwen3_forward` 10/10 GREEN and reddened only this file. G4 in the same file: capture step vs `Qwen3DenseModel::Forward`, 100 logits, 0 differing. **The async decline at `qwen3.cpp` STANDS and is now GATED in both arms**: migrating the capture does not move the INPUTS, so the depth-2 race is untouched, and the fix is `StepDevInputs` as a SEAM capability, which is W4. **G1 is NOT met by W2** and is recorded owed rather than implied. **W3 DONE 2026-08-19 ([#1291](https://github.com/mudler/vllm.cpp/issues/1291)): the three remaining PLAIN BATCHED drivers migrate — `Qwen3MoeDecodeGraph`, `VoxtralDecodeGraph`, `DeepseekV2DecodeGraph` — one commit each, each with its own RED-first G2 gate.** Four of the nine drivers are now on the seam, and the six batched-driver `VLLM_CPP_CUDAGRAPH` reads `## Our baseline` item 1 counted are down to TWO, both in `qwen3_5.cpp` (W4). Each gate asserts the SEAM's counters and not the backend log, because a driver that kept its raw pair produces identical logits, an identical backend log and an identical `replay_count()`; red-first on four assertions each (`test_qwen3_moe_decode_graph_seam` 222/226, `test_voxtral_decode_graph_seam` 224/228, `test_deepseek_v2_decode_graph_seam` 224/228, all exit 1), green 3/3 each after. The G2 mutation — restoring each pre-W3 driver file, 25/102, 23/92 and 25/94 lines, each compiled clean — reddens ONLY its own gate and leaves `test_breakable_graph` 216/216 and W2's `test_qwen3_decode_graph_seam` 231/231 green. The gate harness is now SHARED (`tests/vllm/models/decode_graph_seam_harness.h`); three more copies inside `tests/` would have reproduced the duplication this row removes from `src/`. **G1 IS DELIVERED and is no longer owed** — the item W1 and W2 both carried. `tests/vllm/models/test_decode_graph_seam_g1_cuda.cpp` runs each driver COLD, CAPTURE and THREE consecutive replays against its own eager arm (selected by `max_num_reqs == 0`, so both arms are one binary on one device rather than two builds, each with its OWN device KV cache) on `thor:gpu0` through an `rc` lease — NVIDIA Thor sm_110, driver 595.78, nvcc 13.0.88, source `c905bb536`, 32 `.cu.o` objects, binary resolving `libcudart.so.13`/`libcublasLt.so.13`: **3 cases, 1600 assertions, exit 0, `5 steps x 100 logits, 0 differing, 4 replays` per driver.** The COUNT carries that claim, not the status line: with no CUDA backend the same file prints `SUCCESS!` over `assertions: 0`. Bounded honestly — synthetic tiny models rather than a checkpoint, and W2's driver shares the seam by argument rather than by measurement. **W3 also found a gate that could not fail.** The three gates' `breaks_registered == 0` mode guard is a TAUTOLOGY for any model with no registered break point, and the one production `vt::GraphBreak` in the tree is W1's in `qwen3.cpp`: flipping `kFull` to `kPiecewise` in `qwen3_moe.cpp`, one token, compiled clean and left that gate GREEN at 226/226. The mode was UNOBSERVABLE from outside a driver, so `vt::GraphBreakStats` gains `full_scopes`/`piecewise_scopes`, counted in `GraphCaptureScope`'s constructor on the ACTIVE path only, with an inert-scope control; the same flip now reds all three gates on exactly those two assertions. **NO break point is registered in these three models, deliberately**: under `kFull` it would be pass-through machinery no gate can exercise, and the break-point set is what the PIECEWISE arm needs (W4/W6). **The async decline, per driver:** Voxtral needs none (its only construction site is `VoxtralGenerateGreedy`, unreachable from the runner); Qwen3-Coder and DeepSeek carry a NEW FINDING instead — `qwen3_moe_registry.cpp:107`, `deepseek_v2_registry.cpp:106` and `glm4_moe_lite_registry.cpp:125` route an async step into a host-vector replay with no `device_token_ids` check at all, filed [#1305](https://github.com/mudler/vllm.cpp/issues/1305) with W4 as owner rather than mitigated on a measurement W3 cannot make. G5's ROCm/Tenstorrent arm is NOT discharged and moves to W5: the fleet carries no such device, so it is blocked on hardware rather than unattempted. **W4 DONE 2026-08-19 ([#1307](https://github.com/mudler/vllm.cpp/issues/1307)): the persistent device input path becomes a SEAM CAPABILITY, and the two Qwen3.5 drivers migrate.** `vt::PersistentStepInput` (`include/vt/persistent_step_input.h`, `src/vt/persistent_step_input.cpp`) binds a capture-stable device destination the DRIVER owns together with its pinned host staging block, and refreshes it in place from a host source or a DEVICE one; it owns the address-stability rule as a REFUSAL, the staging block, and the refreshing ARM as an observable (`last_source()`, `vt::StepInputStats`), and deliberately NOT the device allocation, because `Qwen3_5DecodeGraph` draws its retained inputs from a DEDICATED `DevicePool` so they never pop a block the captured forward's scratch then needs (D3). RED-first against a stub with the declared API and no guarantees: `tests/vt/test_persistent_step_input.cpp` 9 cases / 0 passed / 59 assertions / 32 failed / exit 1, GREEN after at 9/9 and 59/59; three mutations (delete the capacity refusal, make a null device source a silent no-op, collapse the host arm out of staging) each compiled clean and each reds exactly one case. `Qwen3_5DecodeGraph` and `Qwen3_5DenseDecodeGraph` open a `vt::GraphCaptureScope` over a per-slot `vt::BreakableGraph` in `kFull` and replay through it, and their `PinnedStepInputs`/`StageStepInputs` staging now runs THROUGH the capability, which is what makes it reachable rather than a class with a unit test. **Six of the nine drivers are on the seam** and `grep -rn 'std::getenv("VLLM_CPP_CUDAGRAPH")' src/` returns exactly ONE line, `src/vt/breakable_graph.cpp:61` — one switch, at last. Gate `tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp` RED-first on the MoE driver's five seam assertions (3 cases / 62 assertions / 5 failed / exit 1) and GREEN after at 7/7 and 129, G4 reading `40 values, 0 differing` per driver; G2 mutations: the whole pre-W4 file restored reds BOTH drivers (296 lines, 10 assertions), the MoE replay bypassing the container reds ONLY the MoE case (7 lines), the MoE `kFull`->`kPiecewise` flip reds ONLY its mode counters (3 lines), and deleting the `StageStepInputs` call site reds ONLY the reachability case while `test_persistent_step_input` stays 59/59 green — the difference between a class that works and a capability something reaches. **W4 FALSIFIED THIS ROW'S OWN PREMISE, which is its most important result.** This record and the spec both said the fix `qwen3.cpp`'s `DenseDecodeGraphForward`'s decline names already existed as `StepDevInputs`. It does not: `StepDevInputs` has NO token-id member, and its pinned sibling `PinnedStepInputs::token_ids` was allocated at capture, filled every step, zeroed by the poison hook, and NEVER uploaded or read — the embed runs OUTSIDE the captured region from the HOST vector in every batched driver, so **the decode graph carries no token ids to the device in ANY driver**. The dead block is removed. Consequently the DECLINE STANDS and [#1305](https://github.com/mudler/vllm.cpp/issues/1305) STAYS OPEN: W4 also read the decline's recorded cause against the tree at its own parent and found it falsified (the `DeviceTokenIdsScope` WAS live on the graph path, consumed by `EmbedInto` on all three arms at `qwen3.cpp:610,621,644 @ 338cbbfd1^`), so the measured failure is real and its mechanism is unidentified — not a state from which a refactor may retire a mitigation. The async battery was NOT run and W4 says so plainly: it needs `dgx` WITH the Qwen3-0.6B/4B checkpoints, `dgx:gpu0` was held by another session for W4's whole window, and W4's lease was `thor:gpu0`. Still NO throughput claim. W5 DONE 2026-08-19 ([#1335](https://github.com/mudler/vllm.cpp/issues/1335)): the THREE SINGLE-SHAPE drivers migrate — the DFlash draft graph, the DeepSeek V4 decode graph and the Laguna decode graph, whose own note at `laguna.cpp:2116-2119` asked for this seam by name and named V4's as the sibling that moves with it. **NINE OF NINE DRIVERS ARE 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 three per-model rollback switches stay (each an A/B lever for one driver); `VLLM_CPP_CUDAGRAPH` reaches all three for the first time. **D10, the auxiliary-stream fork/join, is DISCHARGED and REACHED** — `GraphCaptureScope` owns the outstanding-fork set and joins it before `EndCaptureGraph` (port of `breakable_cuda_graph.py:353-361` plus the `wait_stream` hook `:101-153`), registered by `vt::GraphNoteFork`/`GraphNoteJoin` from `laguna.cpp:2572-2576,2612`, the only fork inside a captured region by construction. Every prior stage opened `kFull`, which has ONE segment and so no between-segments window, so the rule could not be exercised before W5 and untested machinery was not landed for it. Gated as a COUNTER and an ORDER out of one backend trace, five arms including the control where the model joins first, and two mutations (deleting the join reds only the new case on 5 assertions; making it over-fire reds it on 8). DFlash is the ONE single-shape driver gateable without a GPU, because its admission predicate names neither a device type nor a kernel registry: `test_qwen3_dflash_decode_graph_seam.cpp` RED-first 3 cases/0 passed/16 assertions/7 failed exit 1, GREEN after 3/18, and the G2 mutation reds ONLY that file while seven other suites — the driver's own `test_dflash_propose` included — stay green. **G1 RE-RUN at W5's head on `thor:gpu0`** (sm_110, driver 595.78, nvcc 13.0.88, 32 `.cu.o`, source `79dc6b5bd`) because D10 put a join on the path of EVERY segment close, so the seam changed underneath the five measured drivers: `test_decode_graph_seam_g1_cuda` 5 cases / 2066 assertions / 0 failed, each reading `0 differing, 4 replays`, plus `test_breakable_graph` 265 on the same device. **And the one thing a green build could NOT have told us was measured separately**: Laguna's capture class sits behind `#ifdef VT_MARLIN_NVFP4`, so a passing build is the SAME OBSERVATION as one that compiled the region out. `-DVT_MARLIN_NVFP4=1` is on `laguna.cpp`'s own compile command, and an undeclared identifier injected immediately after its `GraphCaptureScope` line FAILED the object build under `-Werror` (`laguna.cpp:2735`) against an rc-0 baseline, restoring to an empty diff; the identical mutation on V4 failed at `deepseek_v4.cpp:1921`. Both migrated regions are COMPILED, which retires the could-not-even-be-built half. **G1 for all three and G2 for V4 and Laguna are OWED on hardware**, per driver and per reason: V4's `CanRunResidentDecode` refuses `kCPU` and needs the four CUDA-registered kernel families, Laguna's capture class exists only under `VT_MARLIN_NVFP4`. G5's ROCm/Tenstorrent arm stays BLOCKED — the fleet is all NVIDIA — and its owner moves from W5 to the ROW. Still NO throughput claim; analysis [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) W6 DONE 2026-08-19 ([#1374](https://github.com/mudler/vllm.cpp/issues/1374)): the eligibility predicate, #1020, and the negative result on the piecewise arm. | `ACTIVE` | `CLAIM-ENG-CUDAGRAPH-BREAK-W6`; [#1163](https://github.com/mudler/vllm.cpp/issues/1163), [#1192](https://github.com/mudler/vllm.cpp/issues/1192), [#1261](https://github.com/mudler/vllm.cpp/issues/1261), [#1291](https://github.com/mudler/vllm.cpp/issues/1291), [#1307](https://github.com/mudler/vllm.cpp/issues/1307), [#1305](https://github.com/mudler/vllm.cpp/issues/1305), [#1020](https://github.com/mudler/vllm.cpp/issues/1020), [#1335](https://github.com/mudler/vllm.cpp/issues/1335), [#1374](https://github.com/mudler/vllm.cpp/issues/1374), [#1380](https://github.com/mudler/vllm.cpp/issues/1380), [#1390](https://github.com/mudler/vllm.cpp/issues/1390) | | `ENG-CUDAGRAPH-DIFFUSION` | Capture the LTX-2.5 denoise loop (fixed shapes, many identical iterations — the ideal graph target). **BLOCKED, and the blocker is ours:** the render does almost no device compute to capture | T2 | SGLang enabled BCG on this shape AFTER our pin — LTX-2 H200 two-stage 10.75s->6.90s (`d4be483efb`), SANA 1024px -26% (`6c7498113f`), SANA denoise 0.73->0.457s (`56ef810cad`). Dated events, NOT pinned evidence; their win is mostly PyTorch host tax we do not pay | NO capture at all: `grep` for capture across `src/vllm/model_executor/models/ltx2*.cpp` returns nothing | blocked by [#1024](https://github.com/mudler/vllm.cpp/issues/1024) (GPU util **exactly 0 in 321 of 347 samples**, 1.00 core of 20 held for 17+ min after staging), [#1007](https://github.com/mudler/vllm.cpp/issues/1007) (VAE decode has no device arm), [#1087](https://github.com/mudler/vllm.cpp/issues/1087) (**57-66% of wall** is ONE resolution-CONSTANT serial host phase), [#1010](https://github.com/mudler/vllm.cpp/issues/1010) (no phase-boundary log). Decision point is a MEASUREMENT of GPU-busy vs wall once device-resident, not an implementation. **The unblock order now has an owning row:** `LTX25-DEVICE-RESIDENCY` ([#1264](https://github.com/mudler/vllm.cpp/issues/1264), [ltx25-device-residency.md](specs/ltx25-device-residency.md)) stages those defects W0-W6 and carries this decision point as its W7 — if the loop comes back GPU-bound, #1164 closes as a refutation the way [#1161](https://github.com/mudler/vllm.cpp/issues/1161) closed prefill capture | [sglang-breakable-cuda-graph.md](specs/sglang-breakable-cuda-graph.md) | `INVENTORIED` | [#1164](https://github.com/mudler/vllm.cpp/issues/1164) | | `ENG-BATCH-INVARIANT` | Opt-in deterministic execution across scheduler batch sizes (`VLLM_BATCH_INVARIANT=1`): batch-invariant matmul/norm/attention/collectives plus persistent-scheduler NVFP4; production default remains off | T1 | default/env `vllm/envs.py:89,576-578`; initialization `vllm/v1/worker/gpu_worker.py:1262`; NVFP4 dispatch `csrc/libtorch_stable/quantization/fp4/nvfp4_scaled_mm_sm120_kernels.cu:212-220`; suite fixture `tests/v1/determinism/conftest.py:9-12`; operator/e2e `tests/v1/determinism/test_nvfp4_batch_invariant_scaled_mm.py`, `tests/v1/determinism/test_nvfp4_batch_invariant.py` @ `702f481` | - | [W3-C3R executed contract](specs/nvfp4-persistent-plan-cache.md#w3-c3r-batch-shape-localization-and-gate-correction-2026-07-13): production-default ours and vLLM both change outputs across batch shapes; no local opt-in implementation is claimed | `planned: specs/batch-invariant-execution.md` | `INVENTORIED` | - | | `ENG-ASYNC-SCHED` | Async/overlap scheduling (AsyncScheduler placeholders + depth-2 batch-queue step + async D2H on a copy stream); vLLM's DEFAULT at the pin — mirror obligation per B3. **Host-side machinery + runner device-input half + sampler-OUTPUT half LANDED + CPU-gated (2026-07-16):** `AsyncScheduler` placeholder accounting, `step_with_batch_queue` depth-2, `ResolveAsyncScheduling` default-ON-when-compatible + `MaxConcurrentBatches`, `VT_ASYNC_SCHED` rollback; the runner device-input path `combine_sampled_and_draft_tokens`; PLUS the sampler-OUTPUT half — `vt::Backend` event/pinned primitives (`AllocPinned`/events, CUDA cudaHostAlloc+cudaEvent, CPU sync-degeneration), `AsyncGPUModelRunnerOutput` (device sampled-id snapshot → non-blocking D2H on a copy queue + event; `get_output()` waits only that event; MAIN queue never blocked), `Sampler::forward(sampled_ids_out)` device-resident greedy, `GPUModelRunner::sample_tokens_async` + `runner_supports_async`, and the `Executor`+`step_with_batch_queue` seam resolving `get_output()` at CONSUME time. All behind `VT_ASYNC_RUNNER`/`set_async_input_combine`, default OFF. Sync path byte-identical (placeholder sites INERT while count 0; combine off; `sample_tokens_async` degenerates to sync when async off; `sampled_ids_out=nullptr`). **ENABLE-FLIP LANDED + CPU-gated (2026-07-16):** (1) `LoadedEngine` now reorders `runner_` before the scheduler and builds an `AsyncScheduler` + `max_concurrent_batches=2` when `ResolveAsyncScheduling(runner_.runner_supports_async())` resolves ON (else the byte-identical synchronous `Scheduler` + depth-1); the resolved mcb threads into `AsyncLLM`→`EngineCoreProc` (`step_with_batch_queue`) and the "Asynchronous scheduling is enabled/disabled" log mirrors vLLM for A/B audit; (2) the device combine/scatter kernel (`_combine_sampled_and_draft_tokens_kernel` + last_sampled scatter) is ported to CUDA (`src/vt/cuda/cuda_combine_tokens.cu`), main-stream-ordered on the CUDA async path so it DELETES `sample_tokens_async`'s pre-scatter `Synchronize`; the CPU backend keeps the host loop. `VT_ASYNC_RUNNER=1` engages full W3; `VT_ASYNC_SCHED=0` is the same-binary rollback. Production default (no env) stays synchronous byte-identical. **FULL W3 DGX proof RAN twice** — `f086b64` (5/5 gates PASS; c16 TPOT −5.4 ms WIN, tput neutral, TTFT +36 % = Little's-law repayment) and the 2026-07-16 re-proof on the THROUGHPUT-lever fix (persistent pooled sampled-id/pinned buffers + `Sampler` greedy scratch removing ALL per-step `cudaMalloc`/`cudaFree`/`cudaHostAlloc`/event-create from the sampled-id path, incl. the overlap-killing `cudaFree` inside `get_output`; mirrors `gpu_model_runner.py:873-878` + `async_utils.py:12-70`): token-exactness **6/6 PASS**, interleaved c16 **tput −0.32 % (gate ≥+1.5 % FAILS), TPOT −4.95 ms retained, TTFT +34.8 %** — the allocator lever is REFUTED as the tput unlock (≤0.1 % of a ~165 ms c16 step). **DEFAULT FLIPPED ON 2026-07-17** (`VT_ASYNC_RUNNER` default ON via the pure `AsyncRunnerFlagIsOn` predicate, mirroring `vllm/config/vllm.py:992-1044`): the discriminator (`6ea7856`) proved vLLM's own async pays the identical +26–31 % TTFT / −0.7 to −0.9 % tput / −2.6 to −4.3 ms TPOT envelope and W3-ON nets positive (both binding ITL-tail anomalies flip to PASS), so the "needs a throughput lever" ship-gate is RETIRED — W3 is a parity/mirror obligation with a tails+TPOT win. The flip is TOKEN-NEUTRAL (async-ON ≡ async-OFF bit-identical on DGX). `VT_ASYNC_RUNNER=0` = runner-level rollback, `VT_ASYNC_SCHED=0` = scheduler-level rollback. TTFT means rise into vLLM's async envelope BY DESIGN — the next binding grid runs async by default and its TTFT must NOT be misread as a regression. **ROBUSTNESS FIX 2026-07-20 (`discard_request_mask`):** the runner was missing vLLM's `discard_request_mask`, so `GPUModelRunner` emitted a sampled token for prefill-CHUNK requests too; under async this drained a `num_output_placeholders` never reserved (the `is_prefill_chunk` path adds none) → the `async_scheduler.cpp` `num_output_placeholders >= 0` assertion aborted on c8 + short-output (chunked prefill + preemption). FIX mirrors vLLM: `execute_model` computes `exec_state_.discard[i] = seq_len < num_tokens` (`gpu_model_runner.py:2048`); `sample_tokens` clears those rows to empty (`outputs.py:303`), the async path passes `invalid_req_indices` to `AsyncGPUModelRunnerOutput::get_output` (`gpu_model_runner.py:3625` + `outputs.py:303`). Scheduler UNCHANGED (assertion kept — it was correct once the runner honors `scheduler.py:1888-1890`). Sync/non-chunked decode byte-identical (mask all-zero); DGX 27B 235/235 + 35B 315/315, `vllm-bench` c8+short-output+chunked+kv-pressure no longer crashes, memcheck 0. Ledger [parity-ledger.md](parity-ledger.md) 2026-07-20 row | T1 | `vllm/v1/core/sched/async_scheduler.py:12`; `vllm/config/vllm.py:490,990,1038`; `vllm/v1/engine/core.py:519`; `vllm/v1/worker/gpu/input_batch.py:304-406`; `vllm/v1/worker/gpu/async_utils.py:12-70`; `vllm/v1/worker/gpu/gpu_model_runner.py:242-332`; `vllm/v1/outputs.py:298-307` | `src/vllm/v1/core/sched/async_scheduler.cpp:10,45`; placeholder plumbing `src/vllm/v1/core/sched/scheduler.cpp:148,164,605`; `src/vllm/v1/engine/core.cpp:91` (`step_with_batch_queue`, async-output seam); `src/vllm/v1/engine/core_proc.cpp:32,46`; config `include/vllm/config/scheduler.h:117,165,188`, `src/vllm/config/scheduler.cpp:12`; `include/vllm/v1/request.h:187`; runner input leaf `src/vllm/v1/worker/gpu/prepare_inputs.cpp`, `src/vllm/v1/worker/gpu/input_batch.cpp`; runner output leaf `include/vt/backend.h`+`src/vt/backend.cpp`+`src/vt/cuda/cuda_backend.cu` (event/pinned), `include/vllm/v1/worker/gpu/async_output.{h,cpp}` (`AsyncGPUModelRunnerOutput`), `src/vllm/v1/sample/sampler.cpp` (`sampled_ids_out`), `src/vllm/v1/worker/gpu/runner.cpp` (`sample_tokens_async`/`runner_supports_async`), `src/vllm/v1/executor/executor.cpp`+`include/vllm/v1/worker/gpu/model_runner_base.h` (async seam); enable-flip `include/vllm/entrypoints/model_loader.h`+`src/vllm/entrypoints/model_loader.cpp` (`runner_` before scheduler, `ResolveAsyncEnabled`/`MakeScheduler`, `AsyncScheduler`+mcb=2, log), `include/vllm/v1/engine/async_llm.h`+`src/vllm/v1/engine/async_llm.cpp` (mcb param → `EngineCoreProc`); device kernel `include/vt/cuda/combine_tokens.h`+`src/vt/cuda/cuda_combine_tokens.cu`, wired `src/vllm/v1/worker/gpu/runner.cpp` (CUDA combine/scatter branch removes the pre-sync) | `tests/vllm/v1/test_async_scheduler.cpp:1` (6 cases, 54 asserts; RED vs base Scheduler 2/6 fail); depth-2 engine cycle `tests/vllm/v1/test_engine_core_proc.cpp:479` (mcb=2, async-output seam); config resolution `tests/vllm/test_scheduler_config.cpp:75`; enable-flip construction matrix `tests/vllm/entrypoints/test_loaded_engine_dense.cpp` (runner×VT_ASYNC_SCHED → scheduler type + mcb; RED = un-flipped engine, 3/3 ON-arm asserts fail); runner input leaf `test_combine_tokens.cpp` (RED = stale → 5/7 fail), `test_input_batch.cpp`, `test_runner.cpp` (async-ON≡sync); output leaf `tests/vt/test_backend.cpp` (event/pinned contract), `tests/vllm/v1/worker/test_async_output.cpp` (materialize/flush/snapshot; RED = +1 splice), `test_runner.cpp` (`sample_tokens_async` decode ≡ sync); full CPU ctest 111/111, tools 164/164. Prior diagnostic `3812d8` six-leg control: total **1.002153×**, TTFT **0.862159×**, no GPU-time reduction (neutral for speed). **DEFAULT-FLIP (2026-07-17):** new pure CPU flag test [test_async_runner_flag.cpp](../tests/vllm/v1/worker/test_async_runner_flag.cpp) (11 asserts, default-ON/'0'-off); construction matrix [test_loaded_engine_dense.cpp](../tests/vllm/entrypoints/test_loaded_engine_dense.cpp) INVERTED (default → AsyncScheduler+mcb=2; RED verified 5 asserts fail vs un-flipped). CPU clean `-Werror` rebuild, full serial ctest **116/116**, tools **164/164**. **DGX re-confirmation** (evidence `dgx:~/work/vllm.cpp-async-flip`, CUTLASS+FA2 hard-verified, one flock): shipping default (async ON + RMSNorm-fast OFF) → **27B 235/235 + 35B 315/315** with the "Asynchronous scheduling is enabled (mcb=2)" log, and both rollback arms (`VT_ASYNC_RUNNER=0`, `VT_ASYNC_SCHED=0`) 235/235 + 315/315 log "disabled"; async arms BIT-IDENTICAL (token-neutral). Closing record [parity-ledger.md#L502](parity-ledger.md#L502) | [async-serving.md](specs/async-serving.md) | `DONE` | `6ea7856` | diff --git a/.agents/specs/eng-cudagraph-break.md b/.agents/specs/eng-cudagraph-break.md index bf29aa851..dfed068ea 100644 --- a/.agents/specs/eng-cudagraph-break.md +++ b/.agents/specs/eng-cudagraph-break.md @@ -1800,8 +1800,17 @@ Each item names the stage that owns it. Nothing here is claimed by W1. give their padded size slot a `vllm::StepTokenIds` (`include/vllm/model_executor/models/step_token_ids.h`), whose destination is a device buffer with a stable address and whose refresh takes the DEVICE arm - whenever the runner's mirror is live; `last_source()` and `StepInputSource` - gain their reader with it. Reached from `ModelRegistry::Forward` through + whenever the runner's mirror is live. **`last_source()` and `StepInputSource` + do NOT gain a reader with it, and an earlier draft of this entry said they + did.** What gains a production caller is `RefreshFromDevice`; the ARM + OBSERVABLE stays unread. `grep -rn 'last_source()' src/` returns NOTHING; the + six `CHECK`s in `tests/vt/test_persistent_step_input.cpp` are the only readers + of a value; and `include/vllm/model_executor/models/step_token_ids.h:122` + forwards to `cell_.last_source()` but is itself never called. The two headers + (`include/vt/persistent_step_input.h`, `step_token_ids.h`) state the same + thing, and a record asserting a reader exists is exactly the drift the header + repairs in this change were made to end. Reached from `ModelRegistry::Forward` + through `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp`, and gated at `tests/vllm/models/test_moe_async_device_ids.cpp`, which enters at that entry @@ -1969,12 +1978,22 @@ Each item names the stage that owns it. Nothing here is claimed by W1. every assertion passes** ([#1390](https://github.com/mudler/vllm.cpp/issues/1390), found while landing [#1305](https://github.com/mudler/vllm.cpp/issues/1305), not caused by it). - Measured at `5f68e60df`, which is `origin/main` exactly, CPU Release: `8 cases, - 7 passed, 1 failed, 135 assertions, 135 passed, 0 failed`, exit 139, with - `W6: two spec shapes of EQUAL S and different q get two graphs` reporting - `CRASHED: SIGSEGV`. **The assertion counter cannot see it** — the number a - reader greps says 135/135 — so only the exit status and the `CRASHED` line - carry the verdict. It is ORDER-DEPENDENT: `-tc="W6*"` alone passes at 9/9, + Measured at `5f68e60df`, which is `origin/main` exactly, CPU Release: **exit + 139**, with `W6: two spec shapes of EQUAL S and different q get two graphs` + reporting `CRASHED: SIGSEGV`. **No assertion count from that file carries a + verdict, and the first record of this entry treated one as though it did.** + That run printed `8 cases, 7 passed, 1 failed, 135 assertions, 135 passed, 0 + failed`, which reads 135/135 to a grep. The 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 + general rule, stated properly: 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.** The exit status and the `CRASHED` line are the stable + observations, and the crash case and site are reproducible where the counts are + not. `.agents/engine-matrix.md`'s row and the pull-request body carry this same + correction. `.agents/issue-index.md`'s row is APPEND-ONLY and cannot be edited, + so it still presents `8 cases, 7 passed, 1 failed, 135 of 135` as the + measurement; **this spec and the issue are the authority over that row.** It is ORDER-DEPENDENT: `-tc="W6*"` alone passes at 9/9, exit 0, so the crash needs state an earlier case in the same process left behind. `gdb` puts the fault inside `vt::cpu::PagedAttentionKernel` on a threadpool worker, which is what a block table or slot mapping that does not @@ -2286,3 +2305,42 @@ what would have to be true first rather than scheduling it. **That is a publishable negative in the same shape `ENG-CUDAGRAPH-DEDUP` published: the machinery composes and the coverage it would buy has no measured demand on this hardware.** + + +### #1305, the device-token mirror: two BOUNDED residuals the fresh review named + +Both are recorded rather than repaired, each with the reasoning that bounds it. +Neither is a defect this change introduced into a shipped path. + +**R1 — the four shape refusals now name the WRONG FILE for three of their four +callers.** Hoisting the duplicated shape check into one helper in +`src/vllm/model_executor/models/qwen3_5.cpp` moves `__FILE__`/`__LINE__` for all +four refusals to `qwen3_5.cpp:562`, so a refusal raised from any of the other +three callers reports a location in a file that caller does not live in. **Not +repaired, and the bound is what makes that acceptable:** no test asserts these +strings — a grep for the message and for each of the four `what` values returns +only comments — and the `what` prefix still carries CALLER IDENTITY, so the +reader learns which driver disagreed even when the file token is wrong. The +audience is also narrow: a shape refusal is a "the runner and the model disagree" +message, read once, in anger, from a log, by somebody who greps the message text +and not the file token. Repairing it means threading a caller location through the +helper, which is more machinery than the defect it removes. Owner: row +**`ENG-CUDAGRAPH-BREAK`**, if a later stage gives these refusals a gate. + +**R2 — the two rewritten call sites in files this change was not repairing are +UNGATED, and were before it.** `src/vllm/model_executor/models/qwen3.cpp:213` +and `src/vllm/model_executor/models/qwen3_5.cpp:7829` are covered only by +checkpoint-gated skips — `test_qwen3_dense_async_serving` and +`test_qwen36_async_serving`, both reporting `assertions: 0` on this box, which is +a SKIP wearing a pass — and `tests/vllm/models/test_qwen3_decode_graph_seam.cpp:341-349` +gates the graph DECLINE, never the consumption. What the hoist changed at those +two sites is the ARGUMENT LIST alone; the shared BODY they now call is gated, so +the residual ungated surface is two argument lists rather than two shape checks. +**Net the hoist IMPROVES coverage:** before it, a defect in any one of the four +private copies was invisible; after it, a defect in the shared body reds +`tests/vllm/models/test_moe_async_device_ids.cpp`. The gap is PRE-EXISTING — those +two files' async arms had no CPU-reachable gate at the base commit and have none +now — and this change neither created nor widened it. Owner: row +**`ENG-CUDAGRAPH-BREAK`**, the stage that gets a `dgx` window WITH the +Qwen3-0.6B/4B checkpoints, which is the same window the decline and the depth-2 +battery already owe runs to. diff --git a/include/vt/persistent_step_input.h b/include/vt/persistent_step_input.h index ef57500a3..77e651f2c 100644 --- a/include/vt/persistent_step_input.h +++ b/include/vt/persistent_step_input.h @@ -129,9 +129,12 @@ void ResetStepInputStats(); // comparison AND on `device_refreshes`. // // WHAT IS STILL UNREAD, named rather than implied. `last_source()` and -// `StepInputSource` have no production reader — every caller of either is a -// test. And the call site above sits OUTSIDE the captured region, so this arm -// is read once per step and never at REPLAY time. Replay-time reading is what +// `StepInputSource` have no production reader — every caller that reads a +// VALUE is a test. The one non-test caller is +// `include/vllm/model_executor/models/step_token_ids.h:122`, a forwarding +// wrapper that nothing itself calls, so it moves no value anywhere. And the +// call site above sits OUTSIDE the captured region, so this arm is read +// once per step and never at REPLAY time. Replay-time reading is what // the `qwen3.cpp` decline actually needs, and neither that destination nor // that placement exists in the dense driver the decline guards. Owner: row // `ENG-CUDAGRAPH-BREAK`, the stage that gets a From d78c34f672c9bcd5c80a8812c75a0f2d8ba7e4ff Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 19 Aug 2026 22:49:03 +0000 Subject: [PATCH 9/9] style(ENG-CUDAGRAPH-BREAK): rewrap three lines the records repair had 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] --- .agents/specs/eng-cudagraph-break.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.agents/specs/eng-cudagraph-break.md b/.agents/specs/eng-cudagraph-break.md index dfed068ea..95af552f8 100644 --- a/.agents/specs/eng-cudagraph-break.md +++ b/.agents/specs/eng-cudagraph-break.md @@ -1810,8 +1810,7 @@ Each item names the stage that owns it. Nothing here is claimed by W1. (`include/vt/persistent_step_input.h`, `step_token_ids.h`) state the same thing, and a record asserting a reader exists is exactly the drift the header repairs in this change were made to end. Reached from `ModelRegistry::Forward` - through - `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and + through `qwen3_moe_registry.cpp`, `deepseek_v2_registry.cpp` and `glm4_moe_lite_registry.cpp`, and gated at `tests/vllm/models/test_moe_async_device_ids.cpp`, which enters at that entry point over a synthetic safetensors checkpoint and reds when the registry's @@ -1993,10 +1992,12 @@ Each item names the stage that owns it. Nothing here is claimed by W1. not. `.agents/engine-matrix.md`'s row and the pull-request body carry this same correction. `.agents/issue-index.md`'s row is APPEND-ONLY and cannot be edited, so it still presents `8 cases, 7 passed, 1 failed, 135 of 135` as the - measurement; **this spec and the issue are the authority over that row.** It is ORDER-DEPENDENT: `-tc="W6*"` alone passes at 9/9, - exit 0, so the crash needs state an earlier case in the same process left - behind. `gdb` puts the fault inside `vt::cpu::PagedAttentionKernel` on a - threadpool worker, which is what a block table or slot mapping that does not + measurement; **this spec and the issue are the authority over that row.** + + It is ORDER-DEPENDENT: `-tc="W6*"` alone passes at 9/9, exit 0, so the crash + needs state an earlier case in the same process left behind. `gdb` puts the + fault inside `vt::cpu::PagedAttentionKernel` on a threadpool worker, which is + what a block table or slot mapping that does not describe the handed KV cache looks like. Reverse-applying #1305's whole source change and rebuilding leaves the same exit 139, and that change executes none of this binary's crashing path. NOT fixed in flow: a segmentation fault in