Skip to content

Commit 2999431

Browse files
mudlerclaude
andcommitted
M2.5 Phase 2: decode CUDA-graph capture/replay (single-stream) + fused-MoE residency
Wire CUDA-graph capture/replay into the fp4/CUDA pure-decode path (mirrors vLLM's decode CUDAGraph capture: capture per batch shape, persistent inputs, decode-only). New Qwen3_5DecodeGraph captures the forward's layer region once per shape and replays it per token; the embedding is kept OUTSIDE the graph (its CUDA op mallocs/frees/syncs a bounds-check flag) and writes a persistent hidden buffer that the captured region reads. Per-step-varying inputs live in persistent HOST vectors mutated in place (capturable on GB10 pageable memory). A cold shape runs one eager step to pre-warm the DevicePool + residency, the next step captures, subsequent steps replay. Fused-MoE (MoeBlockFusedCuda): the per-layer expert pointer/scale arrays + pair->token map are now uploaded ONCE into a resident cache instead of rebuilt + re-uploaded from host stack temporaries every step (those dangling host sources are illegal inside a capture region; also pure per-step waste). Correctness (GB10, free box): test_qwen36_paged_engine greedy gate passes 16/16 token-for-token with the graph active. All other tests pass (the sole red, test_qwen36_weights, is pre-existing: it expects bf16 experts but the on-disk checkpoint is NVFP4 — fails identically at baseline). Measured decode A/B (GB10, 0% contention), graph ON vs OFF: num_reqs==1 (1x16x128): TPOT 65.5 vs 67.2 ms -> ~2.6% faster (graph helps) num_reqs==8 (8x1024x128): TPOT 265 vs 247 ms -> ~7% slower (graph hurts) After Phase 1 made decode async-on-stream, host launch overhead is largely hidden behind the GPU, so graph capture recovers little; batched decode is GPU-bound and gains extra graph-launch overhead from the many GDN-gather nodes. The runner therefore gates the graph to num_reqs==1 (single-stream latency win, no batched-throughput regression). Env VLLM_CPP_CUDAGRAPH=0 disables it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJyFKcK62CcR3imhgbiBnW
1 parent b5bcbb4 commit 2999431

4 files changed

Lines changed: 501 additions & 69 deletions

File tree

include/vllm/model_executor/models/qwen3_5.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#pragma once
2525

2626
#include <cstdint>
27+
#include <memory>
2728
#include <vector>
2829

2930
#include "vllm/model_executor/models/qwen3_5_weights.h"
@@ -101,6 +102,58 @@ class Qwen3_5Model {
101102
const HfConfig& config, vt::Queue& queue);
102103
};
103104

105+
// Decode-step CUDA-graph driver (M2.5 Phase 2, gate-#1 decode-launch unlock).
106+
// Wraps the paged forward COMPUTE body (embed -> layers -> lm_head) in a
107+
// capture-once / replay-per-token CUDA graph for PURE-DECODE batches, collapsing
108+
// the ~thousands of per-step kernel-launch + memcpy host-API calls into a single
109+
// cudaGraphLaunch. Mirrors vLLM's decode CUDAGraph capture: capture keyed on the
110+
// batch SHAPE, per-step-varying inputs threaded through PERSISTENT buffers, and
111+
// decode-only (prefill / mixed batches stay eager, kept off this path by the
112+
// runner).
113+
//
114+
// ── vt-runtime realization (deviations, so upstream ports mechanically) ──────
115+
// * PERSISTENT INPUTS are the HOST step vectors (token_ids / positions / the
116+
// attention+GDN metadata), held here and MUTATED IN PLACE each step. On GB10
117+
// (pageable memory access) the forward's host->device input copies are
118+
// capturable, so a replay re-reads each new token's inputs from the fixed
119+
// host addresses — no separate device staging buffers (vLLM keeps torch
120+
// tensors on-GPU; here the "buffer" is the host vector the copy reads from).
121+
// * The GDN mamba-state gather offsets and the block-table column count are
122+
// BAKED at capture, so the SHAPE key includes them; any change re-captures.
123+
// * A cold shape runs one EAGER step first (pre-warms the DevicePool + the
124+
// resident weights / fused-MoE constants) so the capture region does zero
125+
// cudaMalloc; the next same-shape step captures, and subsequent ones replay.
126+
// * VLLM_CPP_CUDAGRAPH=0 disables capture (always eager) for the A/B and as a
127+
// safety valve. Non-CUDA devices always run eager.
128+
class Qwen3_5DecodeGraph {
129+
public:
130+
Qwen3_5DecodeGraph(const Qwen3_5MoeWeights& weights, const HfConfig& config,
131+
vt::Queue queue);
132+
~Qwen3_5DecodeGraph();
133+
Qwen3_5DecodeGraph(const Qwen3_5DecodeGraph&) = delete;
134+
Qwen3_5DecodeGraph& operator=(const Qwen3_5DecodeGraph&) = delete;
135+
136+
// One PURE-DECODE step. Returns [T, vocab] f32 logits, bit-identical to
137+
// Qwen3_5Model::Forward for the same inputs/caches. attn_kv / gdn_state are the
138+
// runner's persistent caches (stable addresses across steps). The caller must
139+
// only route pure-decode batches here (all query_len==1, no prefill).
140+
std::vector<float> Step(const std::vector<int32_t>& token_ids,
141+
const std::vector<int32_t>& positions,
142+
const v1::CommonAttentionMetadata& attn_meta,
143+
const v1::GDNAttentionMetadata& gdn_meta,
144+
const std::vector<PagedKvCache>& attn_kv,
145+
const std::vector<GdnStateCache>& gdn_state);
146+
147+
// Diagnostics (A/B + tests): is a graph currently captured, and how many
148+
// replays have run since the last (re)capture.
149+
bool captured() const;
150+
int64_t replay_count() const;
151+
152+
private:
153+
struct Impl;
154+
std::unique_ptr<Impl> impl_;
155+
};
156+
104157
// Per-layer parity replay: runs ONE decoder layer over the combined residual
105158
// stream. `hidden_in` is the [T*H] f32 stream INTO the layer (= residual +
106159
// hidden, as the pinned oracle reconstructs it); `positions` is the length-T

include/vllm/v1/worker/gpu/runner.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454

5555
#include <cstdint>
5656
#include <map>
57+
#include <memory>
5758
#include <optional>
5859
#include <string>
5960
#include <vector>
@@ -174,6 +175,12 @@ class GPUModelRunner final : public ModelRunnerBase {
174175
std::vector<PagedKvCache> attn_kv_;
175176
std::vector<GdnStateCache> gdn_state_;
176177

178+
// Decode CUDA-graph driver (M2.5 Phase 2), created lazily on the first
179+
// pure-decode step of an fp4/CUDA model. Captures the decode forward once per
180+
// batch shape and replays it per token (mirrors vLLM's decode CUDAGraph
181+
// capture). nullptr on CPU / bf16 / when disabled — those keep the eager path.
182+
std::unique_ptr<Qwen3_5DecodeGraph> decode_graph_;
183+
177184
// Stashed forward result between execute_model and sample_tokens (upstream
178185
// ExecuteModelState — hidden_states + input_batch handoff, here the full
179186
// logits + the dense-order step). num_reqs == 0 marks a 0-token flush step.

0 commit comments

Comments
 (0)