Revert "[Perf][GLM-5.2] Blackwell decode optimizations" (#49768)

Woosuk Kwon committed Jul 24, 2026 at 16:04 UTC 213f681f8117b9026ca8189583d2855d70104401
29 files changed +138 -2139
CMakeLists.txt
-17
@@ -732,23 +732,6 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
732 "(requires SM90+ and CUDA >= 12.0).")
733 endif()
734
735 - # BF16 skinny GEMM (M<=32; weight-bandwidth-bound decode shapes).
736 - # Requires SM90+.
737 - cuda_archs_sm90plus(BF16_SKINNY_GEMM_ARCHS "${CUDA_ARCHS}")
738 - if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.0 AND BF16_SKINNY_GEMM_ARCHS)
739 - set(BF16_SKINNY_GEMM_SRCS
740 - "csrc/libtorch_stable/bf16_skinny_gemm_entry.cu"
741 - "csrc/libtorch_stable/bf16_skinny_gemm.cu")
742 - set_gencode_flags_for_srcs(
743 - SRCS "${BF16_SKINNY_GEMM_SRCS}"
744 - CUDA_ARCHS "${BF16_SKINNY_GEMM_ARCHS}")
745 - list(APPEND VLLM_STABLE_EXT_SRC "${BF16_SKINNY_GEMM_SRCS}")
746 - message(STATUS "Building bf16_skinny_gemm for archs: ${BF16_SKINNY_GEMM_ARCHS}")
747 - else()
748 - message(STATUS "Not building bf16_skinny_gemm as no compatible archs found "
749 - "(requires SM90+ and CUDA >= 12.0).")
750 - endif()
751 -
735 # Only build AllSpark kernels if we are building for at least some compatible archs.
736 cuda_archs_loose_intersection(ALLSPARK_ARCHS "8.0;8.6;8.7;8.9" "${CUDA_ARCHS}")
737 if (ALLSPARK_ARCHS)
csrc/libtorch_stable/bf16_skinny_gemm.cu deleted
-262
@@ -1,262 +0,0 @@
1 -// SPDX-License-Identifier: Apache-2.0
2 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -//
4 -// Skinny GEMM: activation(bf16) x weight(bf16)^T -> bf16, for decode-time
5 -// M <= 32 with a large reduction dim. Replaces cuBLAS splitK (GEMM +
6 -// splitKreduce) with a single block-per-output-column kernel; these shapes
7 -// are weight-bandwidth-bound, so one coalesced pass over the weight at
8 -// fp32 accumulation is optimal. Adapted from fp32_router_gemm.cu.
9 -//
10 -// First user: the DeepSeek-V32/GLM-5.2 MTP eh_proj (K=2*hidden=12288,
11 -// N=hidden/TP), whose cuBLAS splitK pick costs ~34.6us vs the ~19us
12 -// bandwidth floor per replicated read (and ~4us once column-parallel).
13 -
14 -#include <cuda_bf16.h>
15 -#include <cuda_runtime.h>
16 -
17 -// ---------------------------------------------------------------------------
18 -// Load helpers (8 x bf16 = one uint4 load, converted to fp32)
19 -// ---------------------------------------------------------------------------
20 -
21 -namespace skinny {
22 -
23 -constexpr int VPT = 8; // bf16 values per thread per load
24 -
25 -__device__ __forceinline__ void load_bf16x8(__nv_bfloat16 const* ptr,
26 - float* dst) {
27 - uint4 v = *reinterpret_cast<uint4 const*>(ptr);
28 - __nv_bfloat16 const* p = reinterpret_cast<__nv_bfloat16 const*>(&v);
29 -#pragma unroll
30 - for (int i = 0; i < VPT; i++) dst[i] = __bfloat162float(p[i]);
31 -}
32 -
33 -// Streaming variant for the weight: each row is read exactly once across the
34 -// whole grid, so bypass L2 residency (evict-first). Measured -1.4us at M=1.
35 -__device__ __forceinline__ void load_bf16x8_cs(__nv_bfloat16 const* ptr,
36 - float* dst) {
37 - uint4 v = __ldcs(reinterpret_cast<uint4 const*>(ptr));
38 - __nv_bfloat16 const* p = reinterpret_cast<__nv_bfloat16 const*>(&v);
39 -#pragma unroll
40 - for (int i = 0; i < VPT; i++) dst[i] = __bfloat162float(p[i]);
41 -}
42 -
43 -// ---------------------------------------------------------------------------
44 -// Kernel: each block computes kNPB output columns for all kNumTokens rows.
45 -// grid = kN / kNPB, block = kBlockSize threads. K is reduced VPT elements
46 -// per thread per iteration; fp32 accumulation, warp butterfly + smem
47 -// finalize, bf16 store.
48 -// ---------------------------------------------------------------------------
49 -
50 -template <int kBlockSize, int kNumTokens, int kNPB, int kPF, int kN, int kK>
51 -__global__ __launch_bounds__(kBlockSize, 1) void bf16_skinny_gemm_kernel(
52 - __nv_bfloat16* out, __nv_bfloat16 const* mat_a, __nv_bfloat16 const* mat_b,
53 - int64_t out_stride) {
54 - constexpr int k_elems_per_iter = VPT * kBlockSize;
55 - constexpr int k_iterations = kK / k_elems_per_iter;
56 - static_assert(kK % k_elems_per_iter == 0);
57 - constexpr int kWarpSize = 32;
58 - constexpr int kNumWarps = kBlockSize / kWarpSize;
59 -
60 - int const n_base = blockIdx.x * kNPB;
61 - int const tid = threadIdx.x;
62 - int const warpId = tid / kWarpSize;
63 - int const laneId = tid % kWarpSize;
64 -
65 - float acc[kNumTokens][kNPB] = {};
66 - __shared__ float sm_reduction[kNumTokens][kNPB][kNumWarps];
67 -
68 - // Register prefetch (kPF > 0): W does not depend on the predecessor, so
69 - // the first kPF iterations' weight chunks are loaded raw BEFORE the
70 - // dependency sync; with a PDL-releasing producer (fused_eh_norm fires
71 - // gdc_launch_dependents early) these DRAM round trips overlap the norm.
72 - // Pair-measured on B300 (norm+gemm in one graph, full 6144x12288):
73 - // M=1 pf2 28.43us vs pf0 29.00us; deeper prefetch or M >= 2 regresses
74 - // (register pressure), hence the per-M selection in the launcher.
75 - uint4 w_pre[kPF > 0 ? kPF : 1][kNPB];
76 -#pragma unroll
77 - for (int pf = 0; pf < kPF; pf++) {
78 -#pragma unroll
79 - for (int n = 0; n < kNPB; n++) {
80 - w_pre[pf][n] =
81 - *reinterpret_cast<uint4 const*>(mat_b + (size_t)(n_base + n) * kK +
82 - pf * k_elems_per_iter + tid * VPT);
83 - }
84 - }
85 -
86 -#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)
87 - cudaGridDependencySynchronize();
88 -#endif
89 -
90 -#pragma unroll
91 - for (int ki = 0; ki < k_iterations; ki++) {
92 - int const k_base = ki * k_elems_per_iter + tid * VPT;
93 -
94 - float b_float[kNPB][VPT];
95 - if (ki < kPF) {
96 -#pragma unroll
97 - for (int n = 0; n < kNPB; n++) {
98 - __nv_bfloat16 const* p =
99 - reinterpret_cast<__nv_bfloat16 const*>(&w_pre[ki][n]);
100 -#pragma unroll
101 - for (int v = 0; v < VPT; v++) b_float[n][v] = __bfloat162float(p[v]);
102 - }
103 - } else {
104 -#pragma unroll
105 - for (int n = 0; n < kNPB; n++) {
106 - load_bf16x8_cs(mat_b + (size_t)(n_base + n) * kK + k_base, b_float[n]);
107 - }
108 - }
109 -
110 -#pragma unroll
111 - for (int m = 0; m < kNumTokens; m++) {
112 - float a_float[VPT];
113 - load_bf16x8(mat_a + (size_t)m * kK + k_base, a_float);
114 -#pragma unroll
115 - for (int n = 0; n < kNPB; n++) {
116 -#pragma unroll
117 - for (int k = 0; k < VPT; k++) {
118 - acc[m][n] += a_float[k] * b_float[n][k];
119 - }
120 - }
121 - }
122 - }
123 -
124 - // Warp-level butterfly reduction
125 -#pragma unroll
126 - for (int m = 0; m < kNumTokens; m++) {
127 -#pragma unroll
128 - for (int n = 0; n < kNPB; n++) {
129 - float sum = acc[m][n];
130 - sum += __shfl_xor_sync(0xffffffff, sum, 16);
131 - sum += __shfl_xor_sync(0xffffffff, sum, 8);
132 - sum += __shfl_xor_sync(0xffffffff, sum, 4);
133 - sum += __shfl_xor_sync(0xffffffff, sum, 2);
134 - sum += __shfl_xor_sync(0xffffffff, sum, 1);
135 - if (laneId == 0) sm_reduction[m][n][warpId] = sum;
136 - }
137 - }
138 -
139 - __syncthreads();
140 -
141 - // Parallel finalize: one thread per (m, n) output.
142 - for (int idx = tid; idx < kNumTokens * kNPB; idx += kBlockSize) {
143 - int const m = idx / kNPB;
144 - int const n = idx % kNPB;
145 - float final_sum = 0.0f;
146 -#pragma unroll
147 - for (int w = 0; w < kNumWarps; w++) final_sum += sm_reduction[m][n][w];
148 - out[(size_t)m * out_stride + n_base + n] = __float2bfloat16(final_sum);
149 - }
150 -
151 - // Trigger after our stores: harmless hardening, not a guarantee — the
152 - // trigger only permits dependent-launch scheduling and carries no memory
153 - // visibility semantics, so a PDL consumer must still gridsync before
154 - // reading our output. Every current consumer is a plain launch (full
155 - // stream order); firing late just avoids an unnecessarily early launch
156 - // window and matches fp32_router_gemm.cu.
157 -#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)
158 - cudaTriggerProgrammaticLaunchCompletion();
159 -#endif
160 -}
161 -
162 -} // namespace skinny
163 -
164 -// ---------------------------------------------------------------------------
165 -// Launcher
166 -// ---------------------------------------------------------------------------
167 -
168 -template <int kBlockSize, int kNPB, int kNumTokens, int kN, int kK>
169 -void invokeBf16SkinnyGemm(__nv_bfloat16* output, __nv_bfloat16 const* mat_a,
170 - __nv_bfloat16 const* mat_b, int64_t out_stride,
171 - cudaStream_t stream) {
172 - static_assert(kN % kNPB == 0);
173 - // Weight prefetch depth: only M=1 measured a win (see kernel comment).
174 - constexpr int kPF = (kNumTokens == 1) ? 2 : 0;
175 - cudaLaunchConfig_t config;
176 - config.gridDim = kN / kNPB;
177 - config.blockDim = kBlockSize;
178 - config.dynamicSmemBytes = 0;
179 - config.stream = stream;
180 - cudaLaunchAttribute attrs[1];
181 - attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
182 - attrs[0].val.programmaticStreamSerializationAllowed = 1;
183 - config.numAttrs = 1;
184 - config.attrs = attrs;
185 - cudaLaunchKernelEx(&config,
186 - skinny::bf16_skinny_gemm_kernel<kBlockSize, kNumTokens,
187 - kNPB, kPF, kN, kK>,
188 - output, mat_a, mat_b, out_stride);
189 -}
190 -
191 -// ---------------------------------------------------------------------------
192 -// Explicit instantiations. M = 1..32; (N, K) pairs:
193 -// (768, 12288) eh_proj shard, TP8
194 -// (1536, 12288) eh_proj shard, TP4
195 -// (6144, 12288) eh_proj unsharded
196 -// kNPB (B300 sweep, M=1): 6144 -> 2 (3072 blocks, 23.2us vs 25.3 at kNPB=8;
197 -// narrow blocks minimize wave quantization); shards 768/1536 keep 4.
198 -// ---------------------------------------------------------------------------
199 -
200 -#define INSTANTIATE(M, NPB, N, K) \
201 - template void invokeBf16SkinnyGemm<128, NPB, M, N, K>( \
202 - __nv_bfloat16*, __nv_bfloat16 const*, __nv_bfloat16 const*, int64_t, \
203 - cudaStream_t);
204 -
205 -#define INSTANTIATE_ALL_M(NPB, N, K) \
206 - INSTANTIATE(1, NPB, N, K) \
207 - INSTANTIATE(2, NPB, N, K) \
208 - INSTANTIATE(3, NPB, N, K) \
209 - INSTANTIATE(4, NPB, N, K) \
210 - INSTANTIATE(5, NPB, N, K) \
211 - INSTANTIATE(6, NPB, N, K) \
212 - INSTANTIATE(7, NPB, N, K) \
213 - INSTANTIATE(8, NPB, N, K) \
214 - INSTANTIATE(9, NPB, N, K) \
215 - INSTANTIATE(10, NPB, N, K) \
216 - INSTANTIATE(11, NPB, N, K) \
217 - INSTANTIATE(12, NPB, N, K) \
218 - INSTANTIATE(13, NPB, N, K) \
219 - INSTANTIATE(14, NPB, N, K) \
220 - INSTANTIATE(15, NPB, N, K) \
221 - INSTANTIATE(16, NPB, N, K) \
222 - INSTANTIATE(17, NPB, N, K) \
223 - INSTANTIATE(18, NPB, N, K) \
224 - INSTANTIATE(19, NPB, N, K) \
225 - INSTANTIATE(20, NPB, N, K) \
226 - INSTANTIATE(21, NPB, N, K) \
227 - INSTANTIATE(22, NPB, N, K) \
228 - INSTANTIATE(23, NPB, N, K) \
229 - INSTANTIATE(24, NPB, N, K) \
230 - INSTANTIATE(25, NPB, N, K) \
231 - INSTANTIATE(26, NPB, N, K) \
232 - INSTANTIATE(27, NPB, N, K) \
233 - INSTANTIATE(28, NPB, N, K) \
234 - INSTANTIATE(29, NPB, N, K) \
235 - INSTANTIATE(30, NPB, N, K) \
236 - INSTANTIATE(31, NPB, N, K) \
237 - INSTANTIATE(32, NPB, N, K)
238 -
239 -INSTANTIATE_ALL_M(4, 768, 12288)
240 -INSTANTIATE_ALL_M(4, 1536, 12288)
241 -INSTANTIATE_ALL_M(2, 6144, 12288)
242 -// LL-mode (M<=8 wiring guard) backbone shapes, B300 sweep vs cuBLAS:
243 -// q_b_proj (2048, 2048): 1.67x/1.29x/1.15x at M=4/6/8 (NPB=4 within
244 -// 0.1us of per-M best)
245 -// shared-expert gate_up (512, 6144): 1.95x/1.58x/1.40x at M=4/6/8
246 -// cuBLAS keeps qkv_a (2624,6144) and o_proj (6144,2048) — already at
247 -// 3.4-3.8 TB/s there; the GEMV loses on activation re-reads.
248 -INSTANTIATE_ALL_M(4, 2048, 2048)
249 -INSTANTIATE_ALL_M(4, 512, 6144)
250 -// fused_qkv_a (2624, 6144), 32MB: skinny wins ONLY at M<=2 (B300: M=1
251 -// 6.99us vs cuBLAS 9.21 = 1.32x, M=2 1.24x; M>=4 cuBLAS holds at 3.5TB/s
252 -// and every alternative loses — cublasLt top-8 3.6TB/s wall, DeepGEMM
253 -// 0.71x, wmma+cp.async custom 0.30x pending a TMA rewrite).
254 -INSTANTIATE_ALL_M(4, 2624, 6144)
255 -// DSv3.2 (TP8) siblings of the GLM shapes above, same dual-chip matrix:
256 -// fused_qkv_a (2112, 7168), 30MB: wins M<=2 (M=1 1.30-1.34x)
257 -// MTP eh_proj (7168, 14336), 205MB: wins M<=2 (M=1 1.12-1.16x)
258 -INSTANTIATE_ALL_M(4, 2112, 7168)
259 -INSTANTIATE_ALL_M(2, 7168, 14336)
260 -
261 -#undef INSTANTIATE_ALL_M
262 -#undef INSTANTIATE
csrc/libtorch_stable/bf16_skinny_gemm_entry.cu deleted
-170
@@ -1,170 +0,0 @@
1 -// SPDX-License-Identifier: Apache-2.0
2 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -
4 -#include <torch/csrc/stable/library.h>
5 -#include <torch/csrc/stable/tensor.h>
6 -#include <torch/headeronly/core/ScalarType.h>
7 -
8 -#include "core/registration.h"
9 -#include "libtorch_stable/torch_utils.h"
10 -
11 -#include <cuda_bf16.h>
12 -#include <cuda_runtime.h>
13 -
14 -#include <stdexcept>
15 -
16 -namespace {
17 -
18 -inline int getSMVersion() {
19 - auto* props = get_device_prop();
20 - return props->major * 10 + props->minor;
21 -}
22 -
23 -} // namespace
24 -
25 -static constexpr int SKINNY_MAX_TOKENS = 32;
26 -
27 -// Supported (N, K) pairs (must match the instantiations in
28 -// bf16_skinny_gemm.cu): eh_proj shard TP8 / TP4 / unsharded.
29 -static inline bool bf16_skinny_gemm_supported(int n, int k) {
30 - if (k == 12288 && (n == 768 || n == 1536 || n == 6144)) return true;
31 - // LL-mode backbone shapes (wire callers with an M <= 8 guard; the GEMV
32 - // family loses to cuBLAS at larger M).
33 - if (k == 2048 && n == 2048) return true; // q_b_proj (TP8)
34 - if (k == 6144 && n == 2624) return true; // fused_qkv_a (wire M <= 2 only)
35 - if (k == 7168 && n == 2112) return true; // DSv3.2 fused_qkv_a (M <= 2)
36 - if (k == 14336 && n == 7168) return true; // DSv3.2 eh_proj (M <= 2)
37 - if (k == 6144 && n == 512) return true; // shared-expert gate_up (TP8)
38 - return false;
39 -}
40 -
41 -// Forward declarations - template params must match bf16_skinny_gemm.cu
42 -template <int kBlockSize, int kNPB, int kNumTokens, int kN, int kK>
43 -void invokeBf16SkinnyGemm(__nv_bfloat16* output, __nv_bfloat16 const* mat_a,
44 - __nv_bfloat16 const* mat_b, int64_t out_stride,
45 - cudaStream_t stream);
46 -
47 -template <int kNPB, int kN, int kK, int kBegin, int kEnd>
48 -struct SkinnyLoopUnroller {
49 - static void unroll(int num_tokens, __nv_bfloat16* output,
50 - __nv_bfloat16 const* mat_a, __nv_bfloat16 const* mat_b,
51 - int64_t out_stride, cudaStream_t stream) {
52 - if (num_tokens == kBegin) {
53 - invokeBf16SkinnyGemm<128, kNPB, kBegin, kN, kK>(output, mat_a, mat_b,
54 - out_stride, stream);
55 - } else {
56 - SkinnyLoopUnroller<kNPB, kN, kK, kBegin + 1, kEnd>::unroll(
57 - num_tokens, output, mat_a, mat_b, out_stride, stream);
58 - }
59 - }
60 -};
61 -
62 -template <int kNPB, int kN, int kK, int kEnd>
63 -struct SkinnyLoopUnroller<kNPB, kN, kK, kEnd, kEnd> {
64 - static void unroll(int num_tokens, __nv_bfloat16* output,
65 - __nv_bfloat16 const* mat_a, __nv_bfloat16 const* mat_b,
66 - int64_t out_stride, cudaStream_t stream) {
67 - if (num_tokens == kEnd) {
68 - invokeBf16SkinnyGemm<128, kNPB, kEnd, kN, kK>(output, mat_a, mat_b,
69 - out_stride, stream);
70 - } else {
71 - throw std::invalid_argument(
72 - "bf16_skinny_gemm: num_tokens must be in [1, 32]");
73 - }
74 - }
75 -};
76 -
77 -static void dispatchBf16SkinnyGemm(int n, int k, int num_tokens,
78 - __nv_bfloat16* output,
79 - __nv_bfloat16 const* mat_a,
80 - __nv_bfloat16 const* mat_b,
81 - int64_t out_stride, cudaStream_t stream) {
82 - if (n == 768 && k == 12288) {
83 - SkinnyLoopUnroller<4, 768, 12288, 1, SKINNY_MAX_TOKENS>::unroll(
84 - num_tokens, output, mat_a, mat_b, out_stride, stream);
85 - } else if (n == 1536 && k == 12288) {
86 - SkinnyLoopUnroller<4, 1536, 12288, 1, SKINNY_MAX_TOKENS>::unroll(
87 - num_tokens, output, mat_a, mat_b, out_stride, stream);
88 - } else if (n == 6144 && k == 12288) {
89 - SkinnyLoopUnroller<2, 6144, 12288, 1, SKINNY_MAX_TOKENS>::unroll(
90 - num_tokens, output, mat_a, mat_b, out_stride, stream);
91 - } else if (n == 2048 && k == 2048) {
92 - SkinnyLoopUnroller<4, 2048, 2048, 1, SKINNY_MAX_TOKENS>::unroll(
93 - num_tokens, output, mat_a, mat_b, out_stride, stream);
94 - } else if (n == 2624 && k == 6144) {
95 - SkinnyLoopUnroller<4, 2624, 6144, 1, SKINNY_MAX_TOKENS>::unroll(
96 - num_tokens, output, mat_a, mat_b, out_stride, stream);
97 - } else if (n == 2112 && k == 7168) {
98 - SkinnyLoopUnroller<4, 2112, 7168, 1, SKINNY_MAX_TOKENS>::unroll(
99 - num_tokens, output, mat_a, mat_b, out_stride, stream);
100 - } else if (n == 7168 && k == 14336) {
101 - SkinnyLoopUnroller<2, 7168, 14336, 1, SKINNY_MAX_TOKENS>::unroll(
102 - num_tokens, output, mat_a, mat_b, out_stride, stream);
103 - } else if (n == 512 && k == 6144) {
104 - SkinnyLoopUnroller<4, 512, 6144, 1, SKINNY_MAX_TOKENS>::unroll(
105 - num_tokens, output, mat_a, mat_b, out_stride, stream);
106 - } else {
107 - throw std::invalid_argument("bf16_skinny_gemm: unsupported (N, K) pair");
108 - }
109 -}
110 -
111 -void bf16_skinny_gemm(
112 - torch::stable::Tensor& output, // [num_tokens, N] bf16
113 - torch::stable::Tensor const& mat_a, // [num_tokens, K] bf16
114 - torch::stable::Tensor const& mat_b // [N, K] bf16
115 -) {
116 - STD_TORCH_CHECK(output.dim() == 2 && mat_a.dim() == 2 && mat_b.dim() == 2);
117 - STD_TORCH_CHECK(output.is_cuda() && mat_a.is_cuda() && mat_b.is_cuda(),
118 - "bf16_skinny_gemm: all tensors must be CUDA tensors");
119 - STD_TORCH_CHECK(output.get_device_index() == mat_a.get_device_index() &&
120 - output.get_device_index() == mat_b.get_device_index(),
121 - "bf16_skinny_gemm: all tensors must be on the same device");
122 - STD_TORCH_CHECK(mat_a.is_contiguous() && mat_b.is_contiguous(),
123 - "bf16_skinny_gemm: inputs must be contiguous");
124 - // Output may be a column-slice view of a wider padded buffer: unit column
125 - // stride, row stride >= N (rows must not overlap).
126 - STD_TORCH_CHECK(output.stride(1) == 1,
127 - "bf16_skinny_gemm: output columns must be contiguous");
128 -
129 - const int num_tokens = mat_a.size(0);
130 - const int n = mat_b.size(0);
131 - const int k = mat_a.size(1);
132 -
133 - STD_TORCH_CHECK(output.size(0) == num_tokens && output.size(1) == n,
134 - "bf16_skinny_gemm: output must be [num_tokens, N]");
135 - STD_TORCH_CHECK(mat_b.size(1) == k,
136 - "bf16_skinny_gemm: mat_a and mat_b must share K");
137 - STD_TORCH_CHECK(bf16_skinny_gemm_supported(n, k),
138 - "bf16_skinny_gemm: unsupported (N, K) pair");
139 - const int64_t out_stride = output.stride(0);
140 - STD_TORCH_CHECK(num_tokens <= 1 || out_stride >= n,
141 - "bf16_skinny_gemm: output rows overlap");
142 - STD_TORCH_CHECK(num_tokens >= 0 && num_tokens <= SKINNY_MAX_TOKENS,
143 - "bf16_skinny_gemm: num_tokens must be in [0, 32]");
144 - STD_TORCH_CHECK(
145 - mat_a.scalar_type() == torch::headeronly::ScalarType::BFloat16 &&
146 - mat_b.scalar_type() == torch::headeronly::ScalarType::BFloat16 &&
147 - output.scalar_type() == torch::headeronly::ScalarType::BFloat16,
148 - "bf16_skinny_gemm: all tensors must be bfloat16");
149 -
150 - // Empty batch (e.g. an empty rank at a DP/PP boundary): nothing to compute.
151 - if (num_tokens == 0) {
152 - return;
153 - }
154 -
155 - const torch::stable::accelerator::DeviceGuard device_guard(
156 - mat_a.get_device_index());
157 - STD_TORCH_CHECK(getSMVersion() >= 90, "bf16_skinny_gemm: requires SM90+");
158 -
159 - auto stream = get_current_cuda_stream(mat_a.get_device_index());
160 - dispatchBf16SkinnyGemm(
161 - n, k, num_tokens,
162 - reinterpret_cast<__nv_bfloat16*>(output.mutable_data_ptr()),
163 - reinterpret_cast<__nv_bfloat16 const*>(mat_a.data_ptr()),
164 - reinterpret_cast<__nv_bfloat16 const*>(mat_b.data_ptr()), out_stride,
165 - stream);
166 -}
167 -
168 -STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, m) {
169 - m.impl("bf16_skinny_gemm", TORCH_BOX(&bf16_skinny_gemm));
170 -}
csrc/libtorch_stable/dsv3_fused_a_gemm.cu
+36 -112
@@ -391,7 +391,7 @@ struct MmaComputer {
391 static constexpr int n_iter_cnt =
392 (tile_n + 7) /
393 8; // Possible to have non-1 n_iter_cnt for ab_swap m16 case.
394 - static_assert(m_iter_cnt == 1 || m_iter_cnt == 2);
394 + static_assert(m_iter_cnt == 1);
395 static_assert(n_iter_cnt == 1 || n_iter_cnt == 2);
396
397 __device__ MmaComputer(bf16_t* gmem_c_local_, bf16_t* smem_a_,
@@ -416,18 +416,13 @@ struct MmaComputer {
416 public:
417 __device__ void prepare() {
418 #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 900
419 - // Fragment addressing is per 16-row ldmatrix tile; m_iter selects the
420 - // 16-row half within tile_m.
419 #pragma unroll
422 - for (int m = 0; m < m_iter_cnt; m++) {
423 - #pragma unroll
424 - for (int i = 0; i < k_phase_cnt; i++) {
425 - int linear_idx = (lane_idx % 16) + (lane_idx / 16) * 128 + i * 256;
426 - int m_idx = linear_idx % 16 + m * 16;
427 - int k_idx = linear_idx / 16 + warp_k_offset_in_tile_k;
428 - k_idx = apply_swizzle_343_on_elem_row_col<bf16_t>(m_idx, k_idx);
429 - a_smem_offsets[m][i] = m_idx * tile_k + k_idx;
430 - }
420 + for (int i = 0; i < k_phase_cnt; i++) {
421 + int linear_idx = (lane_idx % 16) + (lane_idx / 16) * 128 + i * 256;
422 + int m_idx = linear_idx % tile_m;
423 + int k_idx = linear_idx / tile_m + warp_k_offset_in_tile_k;
424 + k_idx = apply_swizzle_343_on_elem_row_col<bf16_t>(m_idx, k_idx);
425 + a_smem_offsets[0][i] = m_idx * tile_k + k_idx;
426 }
427 #pragma unroll
428 for (int n_iter_idx = 0; n_iter_idx < n_iter_cnt; n_iter_idx++) {
@@ -451,14 +446,11 @@ struct MmaComputer {
446 wait_barrier(smem_barrier + 0 + stage_idx * 2, phase_bit);
447
448 #pragma unroll
454 - for (int m = 0; m < m_iter_cnt; m++) {
455 - #pragma unroll
456 - for (int i = 0; i < k_phase_cnt; i++) {
457 - int smem_offset = a_smem_offsets[m][i];
458 - bf16_t* smem_ptr_this_iter =
459 - smem_a + stage_idx * tile_m * tile_k + smem_offset;
460 - ldsm_x4(smem_ptr_this_iter, reinterpret_cast<uint32_t*>(a_reg[m][i]));
461 - }
449 + for (int i = 0; i < k_phase_cnt; i++) {
450 + int smem_offset = a_smem_offsets[0][i];
451 + bf16_t* smem_ptr_this_iter =
452 + smem_a + stage_idx * tile_m * tile_k + smem_offset;
453 + ldsm_x4(smem_ptr_this_iter, reinterpret_cast<uint32_t*>(a_reg[0][i]));
454 }
455
456 #pragma unroll
@@ -477,12 +469,9 @@ struct MmaComputer {
469 for (int k_iter_idx = 0; k_iter_idx < k_phase_cnt; k_iter_idx++) {
470 #pragma unroll
471 for (int n_iter_idx = 0; n_iter_idx < n_iter_cnt; n_iter_idx++) {
480 - #pragma unroll
481 - for (int m = 0; m < m_iter_cnt; m++) {
482 - hmma_16_8_16_f32acc_bf16ab(
483 - acc_reg[m][n_iter_idx], a_reg[m][k_iter_idx],
484 - b_reg[n_iter_idx][k_iter_idx], acc_reg[m][n_iter_idx]);
485 - }
472 + hmma_16_8_16_f32acc_bf16ab(
473 + acc_reg[0][n_iter_idx], a_reg[0][k_iter_idx],
474 + b_reg[n_iter_idx][k_iter_idx], acc_reg[0][n_iter_idx]);
475 }
476 }
477 ::arrive_barrier(smem_barrier + 1 + stage_idx * 2);
@@ -497,14 +486,14 @@ struct MmaComputer {
486 #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 900
487 asm volatile("bar.sync %0, %1;" : : "r"(1), "r"(thread_cnt));
488 // reorganize the acc_reg
500 - constexpr int thread_m = 2 * m_iter_cnt;
489 + constexpr int thread_m = 2;
490 constexpr int thread_n = 2 * n_iter_cnt;
491 constexpr int cta_mma_n = n_iter_cnt * 8;
492 float acc_reg_reorg[thread_m][thread_n];
493
494 for (int i = 0; i < thread_m; i++) {
495 for (int j = 0; j < thread_n; j++) {
507 - acc_reg_reorg[i][j] = acc_reg[i / 2][j / 2][(j % 2) + (i % 2) * 2];
496 + acc_reg_reorg[i][j] = acc_reg[0][j / 2][(j % 2) + (i * 2)];
497 }
498 }
499
@@ -525,8 +514,7 @@ struct MmaComputer {
514 for (int m_idx_thread = 0; m_idx_thread < thread_m; m_idx_thread++) {
515 #pragma unroll
516 for (int n_idx_thread = 0; n_idx_thread < thread_n; n_idx_thread++) {
528 - int m_idx =
529 - (lane_idx / 4) + (m_idx_thread % 2) * 8 + (m_idx_thread / 2) * 16;
517 + int m_idx = (lane_idx / 4) + m_idx_thread * 8;
518 int n_idx =
519 ((lane_idx % 4) * 2) + (n_idx_thread % 2) + (n_idx_thread / 2) * 8;
520 smem_c[cosize_smem_c * warp_idx + smem_c_index_func(m_idx, n_idx)] =
@@ -599,7 +587,7 @@ __global__ __launch_bounds__(256, 1) void fused_a_gemm_kernel(
587 static_assert(
588 tile_k == 128 || tile_k == 256 || tile_k == 512 ||
589 tile_k == 1024); // tile_k must be larger than 64 since 4 warp splitK.
602 - static_assert(tile_m == 16 || tile_m == 32);
590 + static_assert(tile_m == 16);
591 constexpr int g2s_vec_bytes = 16;
592 constexpr int a_elem_bytes = 2;
593 constexpr int b_elem_bytes = 2;
@@ -659,7 +647,7 @@ __global__ __launch_bounds__(256, 1) void fused_a_gemm_kernel(
647 #endif
648 }
649
662 -template <typename T, int kHdIn, int kHdOut, int kTileN, int kTileM = 16>
650 +template <typename T, int kHdIn, int kHdOut, int kTileN>
651 void invokeFusedAGemm(T* output, T const* mat_a, T const* mat_b, int num_tokens,
652 cudaStream_t const stream) {
653 constexpr int gemm_m = kHdOut; // 2112
@@ -667,7 +655,7 @@ void invokeFusedAGemm(T* output, T const* mat_a, T const* mat_b, int num_tokens,
655 constexpr int gemm_k = kHdIn; // 7168
656 constexpr int batch_size = 1;
657 std::swap(mat_a, mat_b);
670 - constexpr int tile_m = kTileM;
658 + constexpr int tile_m = 16;
659 constexpr int tile_n = kTileN; // 8 or 16
660 constexpr int tile_k = std::max(256, 1024 / tile_n); // 256
661 constexpr int max_stage_cnt =
@@ -714,44 +702,6 @@ template void invokeFusedAGemm<__nv_bfloat16, 7168, 2112, 16>(
702 __nv_bfloat16*, __nv_bfloat16 const*, __nv_bfloat16 const*, int num_tokens,
703 cudaStream_t);
704
717 -// GLM-5.2 fused_qkv_a (K=6144 -> N=2624). tile_m=32 so the grid is
718 -// 2624/32 = 82 CTAs, a single wave on B300 (148 SMs); tile_m=16's 164 CTAs
719 -// straddle two waves and drop to 2.9 TB/s vs 3.9-4.0 here. Beats cuBLAS
720 -// at every decode M: 1.11-1.15x for M<=8 (tile_n=8), 1.12x at M=16
721 -// (tile_n=16). The M<=2 dispatch still belongs to bf16_skinny_gemm
722 -// (4.5 TB/s).
723 -template void invokeFusedAGemm<__nv_bfloat16, 6144, 2624, 8, 32>(
724 - __nv_bfloat16*, __nv_bfloat16 const*, __nv_bfloat16 const*, int num_tokens,
725 - cudaStream_t);
726 -
727 -template void invokeFusedAGemm<__nv_bfloat16, 6144, 2624, 16, 32>(
728 - __nv_bfloat16*, __nv_bfloat16 const*, __nv_bfloat16 const*, int num_tokens,
729 - cudaStream_t);
730 -
731 -// GLM-5.2 q_b_proj TP8 (K=2048 -> N=2048). tile_m=16 keeps 128 CTAs (already
732 -// a single wave) and measures ahead of tile_m=32 here. Beats cuBLAS
733 -// 1.79-1.87x at M=3..8 (tile_n=8) and 1.19-1.30x at M=9..16 (tile_n=16);
734 -// M<=2 belongs to bf16_skinny_gemm, M>=20 to cuBLAS.
735 -template void invokeFusedAGemm<__nv_bfloat16, 2048, 2048, 8>(
736 - __nv_bfloat16*, __nv_bfloat16 const*, __nv_bfloat16 const*, int num_tokens,
737 - cudaStream_t);
738 -
739 -template void invokeFusedAGemm<__nv_bfloat16, 2048, 2048, 16>(
740 - __nv_bfloat16*, __nv_bfloat16 const*, __nv_bfloat16 const*, int num_tokens,
741 - cudaStream_t);
742 -
743 -// DSv3.2 q_b_proj TP8 (K=1536 -> N=3072). tile_m=32 keeps 96 CTAs (single
744 -// wave; tile_m=16's 192 straddle two). Beats cuBLAS 1.6-1.8x at M=1..8 and
745 -// 1.05-1.18x at M=9..16 on B300/B200; the whole 1..16 range dispatches here
746 -// (no skinny tier: K=1536 does not fit the GEMV's 128x8 K-step).
747 -template void invokeFusedAGemm<__nv_bfloat16, 1536, 3072, 8, 32>(
748 - __nv_bfloat16*, __nv_bfloat16 const*, __nv_bfloat16 const*, int num_tokens,
749 - cudaStream_t);
750 -
751 -template void invokeFusedAGemm<__nv_bfloat16, 1536, 3072, 16, 32>(
752 - __nv_bfloat16*, __nv_bfloat16 const*, __nv_bfloat16 const*, int num_tokens,
753 - cudaStream_t);
754 -
705 void dsv3_fused_a_gemm(torch::stable::Tensor& output,
706 torch::stable::Tensor const& mat_a,
707 torch::stable::Tensor const& mat_b) {
@@ -760,15 +710,12 @@ void dsv3_fused_a_gemm(torch::stable::Tensor& output,
710 int const hd_in = mat_a.size(1);
711 int const hd_out = mat_b.size(1);
712
763 - bool const is_dsv3 = hd_in == 7168 && hd_out == 2112;
764 - bool const is_glm = hd_in == 6144 && hd_out == 2624;
765 - bool const is_glm_qb = hd_in == 2048 && hd_out == 2048;
766 - bool const is_ds_qb = hd_in == 1536 && hd_out == 3072;
713 + constexpr int kHdIn = 7168;
714 + constexpr int kHdOut = 2112;
715 STD_TORCH_CHECK(num_tokens >= 1 && num_tokens <= 16,
716 "required 1 <= mat_a.shape[0] <= 16");
769 - STD_TORCH_CHECK(is_dsv3 || is_glm || is_glm_qb || is_ds_qb,
770 - "supported (hd_in, hd_out): (7168, 2112), (6144, 2624), "
771 - "(2048, 2048), (1536, 3072)");
717 + STD_TORCH_CHECK(hd_in == kHdIn, "required mat_a.shape[1] == 7168");
718 + STD_TORCH_CHECK(hd_out == kHdOut, "required mat_b.shape[1] == 2112");
719 STD_TORCH_CHECK(output.size(0) == num_tokens,
720 "required output.shape[0] == mat_a.shape[0]");
721 STD_TORCH_CHECK(output.size(1) == hd_out,
@@ -791,41 +738,18 @@ void dsv3_fused_a_gemm(torch::stable::Tensor& output,
738 STD_TORCH_CHECK(getSMVersion() >= 90, "required CUDA ARCH >= SM_90");
739
740 auto stream = get_current_cuda_stream(mat_a.get_device_index());
794 - auto* out = reinterpret_cast<__nv_bfloat16*>(output.mutable_data_ptr());
795 - auto* a = reinterpret_cast<__nv_bfloat16 const*>(mat_a.data_ptr());
796 - auto* b = reinterpret_cast<__nv_bfloat16 const*>(mat_b.data_ptr());
797 - if (is_dsv3) {
798 - if (num_tokens <= 8) {
799 - invokeFusedAGemm<__nv_bfloat16, 7168, 2112, 8>(out, a, b, num_tokens,
800 - stream);
801 - } else {
802 - invokeFusedAGemm<__nv_bfloat16, 7168, 2112, 16>(out, a, b, num_tokens,
803 - stream);
804 - }
805 - } else if (is_glm) {
806 - if (num_tokens <= 8) {
807 - invokeFusedAGemm<__nv_bfloat16, 6144, 2624, 8, 32>(out, a, b, num_tokens,
808 - stream);
809 - } else {
810 - invokeFusedAGemm<__nv_bfloat16, 6144, 2624, 16, 32>(out, a, b, num_tokens,
811 - stream);
812 - }
813 - } else if (is_glm_qb) {
814 - if (num_tokens <= 8) {
815 - invokeFusedAGemm<__nv_bfloat16, 2048, 2048, 8>(out, a, b, num_tokens,
816 - stream);
817 - } else {
818 - invokeFusedAGemm<__nv_bfloat16, 2048, 2048, 16>(out, a, b, num_tokens,
819 - stream);
820 - }
741 + if (num_tokens <= 8) {
742 + invokeFusedAGemm<__nv_bfloat16, kHdIn, kHdOut, 8>(
743 + reinterpret_cast<__nv_bfloat16*>(output.mutable_data_ptr()),
744 + reinterpret_cast<__nv_bfloat16 const*>(mat_a.data_ptr()),
745 + reinterpret_cast<__nv_bfloat16 const*>(mat_b.data_ptr()), num_tokens,
746 + stream);
747 } else {
822 - if (num_tokens <= 8) {
823 - invokeFusedAGemm<__nv_bfloat16, 1536, 3072, 8, 32>(out, a, b, num_tokens,
824 - stream);
825 - } else {
826 - invokeFusedAGemm<__nv_bfloat16, 1536, 3072, 16, 32>(out, a, b, num_tokens,
827 - stream);
828 - }
748 + invokeFusedAGemm<__nv_bfloat16, kHdIn, kHdOut, 16>(
749 + reinterpret_cast<__nv_bfloat16*>(output.mutable_data_ptr()),
750 + reinterpret_cast<__nv_bfloat16 const*>(mat_a.data_ptr()),
751 + reinterpret_cast<__nv_bfloat16 const*>(mat_b.data_ptr()), num_tokens,
752 + stream);
753 }
754 }
755
csrc/libtorch_stable/quantization/fp4/nvfp4_quant_kernels.cu
+10 -42
@@ -49,11 +49,6 @@ __global__ void __launch_bounds__(512, VLLM_BLOCKS_PER_SM(512))
49 static_assert(sizeof(PackedVec) == sizeof(Type) * CVT_FP4_ELTS_PER_THREAD,
50 "Vec size is not matched.");
51
52 -#if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900))
53 - cudaGridDependencySynchronize();
54 - cudaTriggerProgrammaticLaunchCompletion();
55 -#endif
56 -
52 // Precompute SF layout parameter (constant for entire kernel).
53 int32_t const numKTiles = (outputCols + 63) / 64;
54
@@ -128,11 +123,6 @@ __global__ void __launch_bounds__(512, VLLM_BLOCKS_PER_SM(512))
123 static_assert(sizeof(PackedVec) == sizeof(Type) * CVT_FP4_ELTS_PER_THREAD,
124 "Vec size is not matched.");
125
131 -#if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900))
132 - cudaGridDependencySynchronize();
133 - cudaTriggerProgrammaticLaunchCompletion();
134 -#endif
135 -
126 int32_t const colIdx = blockDim.x * blockIdx.y + threadIdx.x;
127 int elem_idx = colIdx * CVT_FP4_ELTS_PER_THREAD;
128
@@ -211,8 +201,6 @@ void scaled_fp4_quant_sm1xxa(torch::stable::Tensor const& output,
201 const torch::stable::accelerator::DeviceGuard device_guard(
202 input.get_device_index());
203 auto stream = get_current_cuda_stream(input.get_device_index());
214 - auto* device_props = get_device_prop();
215 - int const sm_version = device_props->major * 10 + device_props->minor;
204
205 int output_sf_n_unpadded = int(output_n / CVT_FP4_SF_VEC_SIZE);
206
@@ -236,21 +224,10 @@ void scaled_fp4_quant_sm1xxa(torch::stable::Tensor const& output,
224 input.scalar_type(), "nvfp4_quant_kernel", [&] {
225 using cuda_type = vllm::CUDATypeConverter<scalar_t>::Type;
226 auto input_ptr = static_cast<cuda_type const*>(input.data_ptr());
239 - cudaLaunchConfig_t config = {};
240 - config.gridDim = grid;
241 - config.blockDim = block;
242 - config.dynamicSmemBytes = 0;
243 - config.stream = stream;
244 - cudaLaunchAttribute attrs[1];
245 - attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
246 - attrs[0].val.programmaticStreamSerializationAllowed = 1;
247 - config.numAttrs = (sm_version >= 90) ? 1 : 0;
248 - config.attrs = attrs;
249 - cudaLaunchKernelEx(&config, vllm::cvt_fp16_to_fp4<cuda_type, false>,
250 - m, n, output_n, num_padded_cols, input_ptr,
251 - input_sf_ptr,
252 - reinterpret_cast<uint32_t*>(output_ptr),
253 - reinterpret_cast<uint32_t*>(sf_out));
227 + vllm::cvt_fp16_to_fp4<cuda_type, false><<<grid, block, 0, stream>>>(
228 + m, n, output_n, num_padded_cols, input_ptr, input_sf_ptr,
229 + reinterpret_cast<uint32_t*>(output_ptr),
230 + reinterpret_cast<uint32_t*>(sf_out));
231 });
232 } else {
233 int num_packed_cols = output_n / CVT_FP4_ELTS_PER_THREAD;
@@ -263,21 +240,12 @@ void scaled_fp4_quant_sm1xxa(torch::stable::Tensor const& output,
240 input.scalar_type(), "nvfp4_quant_kernel", [&] {
241 using cuda_type = vllm::CUDATypeConverter<scalar_t>::Type;
242 auto input_ptr = static_cast<cuda_type const*>(input.data_ptr());
266 - cudaLaunchConfig_t config = {};
267 - config.gridDim = grid;
268 - config.blockDim = block;
269 - config.dynamicSmemBytes = 0;
270 - config.stream = stream;
271 - cudaLaunchAttribute attrs[1];
272 - attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
273 - attrs[0].val.programmaticStreamSerializationAllowed = 1;
274 - config.numAttrs = (sm_version >= 90) ? 1 : 0;
275 - config.attrs = attrs;
276 - cudaLaunchKernelEx(
277 - &config, vllm::cvt_fp16_to_fp4_sf_major<cuda_type, false>, m, n,
278 - output_n, output_sf_n_unpadded, num_packed_cols, input_ptr,
279 - input_sf_ptr, reinterpret_cast<uint32_t*>(output_ptr),
280 - reinterpret_cast<uint32_t*>(sf_out));
243 + vllm::cvt_fp16_to_fp4_sf_major<cuda_type, false>
244 + <<<grid, block, 0, stream>>>(
245 + m, n, output_n, output_sf_n_unpadded, num_packed_cols,
246 + input_ptr, input_sf_ptr,
247 + reinterpret_cast<uint32_t*>(output_ptr),
248 + reinterpret_cast<uint32_t*>(sf_out));
249 });
250 }
251 }
csrc/libtorch_stable/torch_bindings.cpp
-4
@@ -330,10 +330,6 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
330 // conditionally compiled so impl registration is in source file
331 ops.def("fp32_router_gemm(Tensor! output, Tensor mat_a, Tensor mat_b) -> ()");
332
333 - // BF16 skinny GEMM (M<=32, weight-BW-bound shapes, e.g. MTP eh_proj).
334 - // conditionally compiled so impl registration is in source file
335 - ops.def("bf16_skinny_gemm(Tensor! output, Tensor mat_a, Tensor mat_b) -> ()");
336 -
333 // reorder weight for AllSpark Ampere W8A16 Fused Gemm kernel
334 ops.def(
335 "rearrange_kn_weight_as_n32k16_order(Tensor b_qweight, Tensor b_scales, "
recipes/glm5.2-ll-b300-tp8-mtp5.md deleted
-41
@@ -1,41 +0,0 @@
1 -# GLM-5.2-NVFP4 — Low-Latency (LL) serving recipe · 8×B300 · TP8 · MTP=5
2 -
3 -Single-node 8×B300 (sm_100/103), pure tensor parallel, NVFP4 weights, fp8 KV cache,
4 -MTP=5 speculative decode, model runner v2.
5 -
6 -Validated on `glm5.2-LL` @ `8d407aee1`.
7 -
8 -## Versions
9 -
10 -| Component | Version |
11 -| --- | --- |
12 -| vLLM | `glm5.2-LL` @ `8d407aee1` |
13 -| flashinfer-python | `0.6.15` |
14 -| Model | `nvidia/GLM-5.2-NVFP4` (`modelopt_fp4`) |
15 -
16 -## Build (once)
17 -
18 -```bash
19 -pip install 'flashinfer-python==0.6.15'
20 -
21 -CUDA_HOME=/usr/local/cuda-13.0 TORCH_CUDA_ARCH_LIST="10.0 10.3" MAX_JOBS=96 \
22 -VLLM_USE_PRECOMPILED=0 VLLM_USE_PRECOMPILED_RUST=0 \
23 -pip install -e . --no-deps --no-build-isolation
24 -```
25 -
26 -## Server
27 -
28 -```bash
29 -export VLLM_USE_V2_MODEL_RUNNER=1 # model runner v2
30 -export VLLM_DEEP_GEMM_WARMUP=skip
31 -
32 -vllm serve nvidia/GLM-5.2-NVFP4 \
33 - --trust-remote-code \
34 - --tensor-parallel-size 8 \
35 - --quantization modelopt_fp4 --kv-cache-dtype fp8_e4m3 \
36 - --max-model-len 32768 --max-num-batched-tokens 16384 --max-num-seqs 256 \
37 - --no-enable-prefix-caching --gpu-memory-utilization 0.85 \
38 - --speculative-config '{"method":"mtp","num_speculative_tokens":5}' \
39 - --kernel-config '{"ir_op_priority":{"rms_norm":["vllm_c","native"],"fused_add_rms_norm":["vllm_c","native"]}}' \
40 - --host 0.0.0.0 --port 8000
41 -```
tests/kernels/test_bf16_skinny_gemm.py deleted
-70
@@ -1,70 +0,0 @@
1 -# SPDX-License-Identifier: Apache-2.0
2 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""Numerical correctness for the bf16 skinny GEMM (decode-M GEMV) kernel.
4 -
5 -Covers every supported (N, K) shape across M = 0/1/2/8/16/32 (M=32 exercises
6 -the tile_m=32 MMA path), plus strided (column-slice) output. M=0 must return
7 -an empty result without launching the kernel (empty ranks at DP/PP
8 -boundaries)."""
9 -
10 -import pytest
11 -import torch
12 -
13 -from vllm import _custom_ops as ops
14 -from vllm.platforms import current_platform
15 -
16 -# (N, K) pairs must match bf16_skinny_gemm_supported() in the entry .cu.
17 -SHAPES = [
18 - (768, 12288),
19 - (1536, 12288),
20 - (6144, 12288),
21 - (2048, 2048),
22 - (2624, 6144),
23 - (2112, 7168),
24 - (7168, 14336),
25 - (512, 6144),
26 -]
27 -MS = [0, 1, 2, 8, 16, 32]
28 -
29 -pytestmark = pytest.mark.skipif(
30 - not current_platform.is_cuda() or not current_platform.has_device_capability(90),
31 - reason="bf16_skinny_gemm requires CUDA SM90+",
32 -)
33 -
34 -
35 -def _rel_err(out: torch.Tensor, ref: torch.Tensor) -> float:
36 - return (out.float() - ref).norm().item() / ref.norm().clamp_min(1e-6).item()
37 -
38 -
39 -@pytest.mark.parametrize("n,k", SHAPES)
40 -@pytest.mark.parametrize("m", MS)
41 -def test_bf16_skinny_gemm_matches_reference(n: int, k: int, m: int):
42 - torch.manual_seed(0)
43 - x = torch.randn(m, k, dtype=torch.bfloat16, device="cuda")
44 - w = torch.randn(n, k, dtype=torch.bfloat16, device="cuda")
45 -
46 - out = ops.bf16_skinny_gemm(x, w)
47 - assert out.shape == (m, n)
48 - assert out.dtype == torch.bfloat16
49 -
50 - if m == 0: # empty batch: nothing to check beyond the empty shape
51 - return
52 - ref = x.float() @ w.float().t()
53 - rel = _rel_err(out, ref)
54 - assert rel < 2e-2, f"rel err {rel:.4f} for (m,n,k)=({m},{n},{k})"
55 -
56 -
57 -@pytest.mark.parametrize("n,k", [(2048, 2048), (512, 6144)])
58 -def test_bf16_skinny_gemm_strided_output(n: int, k: int):
59 - """Output is a column-slice of a wider padded buffer (row stride > N)."""
60 - torch.manual_seed(0)
61 - m = 8
62 - x = torch.randn(m, k, dtype=torch.bfloat16, device="cuda")
63 - w = torch.randn(n, k, dtype=torch.bfloat16, device="cuda")
64 -
65 - wide = torch.empty(m, n + 64, dtype=torch.bfloat16, device="cuda")
66 - view = wide[:, :n]
67 - torch.ops._C.bf16_skinny_gemm(view, x, w)
68 -
69 - ref = x.float() @ w.float().t()
70 - assert _rel_err(view, ref) < 2e-2
tests/kernels/test_fused_deepseek_v32_norm_rope.py
-77
@@ -49,32 +49,6 @@ pytestmark = pytest.mark.skipif(
49 )
50
51
52 -def test_platform_capability_queries_are_constant_during_compile(monkeypatch):
53 - monkeypatch.setattr(
54 - K.current_platform, "has_device_capability", lambda capability: True
55 - )
56 - monkeypatch.setattr(K, "has_cutedsl", lambda: True)
57 - monkeypatch.setattr(K.current_platform, "is_arch_support_pdl", lambda: True)
58 -
59 - def capability_branches(x: torch.Tensor) -> torch.Tensor:
60 - if K._can_use_fused_q_cutedsl() and K._is_arch_support_pdl():
61 - return x + 1
62 - return x - 1
63 -
64 - compiled = torch.compile(capability_branches, backend="eager", fullgraph=True)
65 - x = torch.zeros(1, device="cuda")
66 - torch.testing.assert_close(compiled(x), torch.ones_like(x))
67 -
68 -
69 -def test_pdl_is_disabled_before_blackwell(monkeypatch):
70 - monkeypatch.setattr(
71 - K.current_platform, "has_device_capability", lambda capability: False
72 - )
73 - monkeypatch.setattr(K.current_platform, "is_arch_support_pdl", lambda: True)
74 -
75 - assert not K._is_arch_support_pdl()
76 -
77 -
52 # ── reference helpers ────────────────────────────────────────────────────────
53
54
@@ -318,57 +292,6 @@ def test_fused_norm_rope_no_indexer(num_tokens: int):
292 assert (topk == 7).all(), "topk buffer should be untouched on shared layer"
293
294
321 -def test_fused_norm_rope_profile_without_cache_compiles():
322 - """The cache-free profiling path must still compile and produce Q."""
323 - torch.manual_seed(2)
324 - dev = "cuda"
325 - num_tokens = 4
326 - pos = torch.arange(num_tokens, device=dev, dtype=torch.int64)
327 - q_c = torch.randn(num_tokens, Q_LORA, device=dev, dtype=torch.bfloat16)
328 - kv_c = torch.randn(num_tokens, KV_LORA, device=dev, dtype=torch.bfloat16)
329 - k_pe = torch.randn(num_tokens, ROPE_DIM, device=dev, dtype=torch.bfloat16)
330 - qw = torch.randn(Q_LORA, device=dev, dtype=torch.bfloat16)
331 - kvw = torch.randn(KV_LORA, device=dev, dtype=torch.bfloat16)
332 - index_k = torch.randn(num_tokens, INDEX_HEAD_DIM, device=dev, dtype=torch.bfloat16)
333 - index_w = torch.randn(INDEX_HEAD_DIM, device=dev, dtype=torch.float32)
334 - index_b = torch.randn(INDEX_HEAD_DIM, device=dev, dtype=torch.float32)
335 - cos_sin = make_cos_sin(64, ROPE_DIM, dev)
336 - topk = torch.empty(num_tokens, 2048, device=dev, dtype=torch.int32)
337 -
338 - def profile_run(
339 - q_c: torch.Tensor,
340 - kv_c: torch.Tensor,
341 - k_pe: torch.Tensor,
342 - index_k: torch.Tensor,
343 - ) -> torch.Tensor:
344 - return K.fused_norm_rope(
345 - pos,
346 - q_c,
347 - qw,
348 - EPS,
349 - kv_c,
350 - kvw,
351 - EPS,
352 - k_pe,
353 - cos_sin,
354 - index_k,
355 - index_w,
356 - index_b,
357 - EPS,
358 - cos_sin,
359 - topk,
360 - slot_mapping=None,
361 - indexer_k_cache=None,
362 - mla_kv_cache=None,
363 - has_indexer=True,
364 - index_rope_interleave=False,
365 - )
366 -
367 - compiled = torch.compile(profile_run, fullgraph=True)
368 - q_out = compiled(q_c, kv_c, k_pe, index_k)
369 - assert_bf16(q_out, rms_norm(q_c, qw), "q_c rmsnorm (profiling)")
370 -
371 -
295 @pytest.mark.parametrize("num_tokens", [1, 4, 17, 512])
296 def test_fused_norm_rope_ds_mla(num_tokens: int):
297 """fp8_ds_mla MLA cache layout (FlashMLA sparse, bf16-query path; SM90/SM100).
tests/models/registry.py
-4
@@ -1600,10 +1600,6 @@ _SPECULATIVE_DECODING_EXAMPLE_MODELS = {
1600 speculative_model="luccafong/deepseek_mtp_draft_random",
1601 trust_remote_code=True,
1602 ),
1603 - "DeepseekV32MTPModel": _HfExamplesInfo(
1604 - "deepseek-ai/DeepSeek-V3.2-Exp",
1605 - speculative_model="deepseek-ai/DeepSeek-V3.2-Exp",
1606 - ),
1603 "DeepSeekV4MTPModel": _HfExamplesInfo(
1604 "deepseek-ai/DeepSeek-V4-Flash",
1605 speculative_model="deepseek-ai/DeepSeek-V4-Flash",
vllm/_custom_ops.py
-25
@@ -2371,20 +2371,6 @@ def fp32_router_gemm(
2371 return output
2372
2373
2374 -def bf16_skinny_gemm(
2375 - x: torch.Tensor,
2376 - weight: torch.Tensor,
2377 -) -> torch.Tensor:
2378 - """Skinny bf16 GEMM (M<=32): x [M, K] @ weight [N, K]^T -> [M, N].
2379 -
2380 - Single block-per-column kernel with fp32 accumulation; replaces cuBLAS
2381 - splitK for weight-bandwidth-bound decode shapes (e.g. MTP eh_proj).
2382 - """
2383 - output = torch.empty((x.shape[0], weight.shape[0]), dtype=x.dtype, device=x.device)
2384 - torch.ops._C.bf16_skinny_gemm(output, x, weight)
2385 - return output
2386 -
2387 -
2374 if hasattr(torch.ops, "_C") and hasattr(torch.ops._C, "fp32_router_gemm"):
2375
2376 @register_fake("_C::fp32_router_gemm")
@@ -2396,17 +2382,6 @@ if hasattr(torch.ops, "_C") and hasattr(torch.ops._C, "fp32_router_gemm"):
2382 return
2383
2384
2399 -if hasattr(torch.ops, "_C") and hasattr(torch.ops._C, "bf16_skinny_gemm"):
2400 -
2401 - @register_fake("_C::bf16_skinny_gemm")
2402 - def bf16_skinny_gemm_fake(
2403 - output: torch.Tensor,
2404 - mat_a: torch.Tensor,
2405 - mat_b: torch.Tensor,
2406 - ) -> None:
2407 - return
2408 -
2409 -
2385 def topk_softmax(
2386 topk_weights: torch.Tensor,
2387 topk_ids: torch.Tensor,
vllm/compilation/passes/fusion/allreduce_rms_fusion.py
+35 -111
@@ -202,7 +202,11 @@ if flashinfer_comm is not None:
202 element_size = allreduce_in.element_size()
203 current_tensor_size = num_tokens * hidden_size * element_size
204 max_tensor_size = max_token_num * hidden_size * element_size
205 - oversize = current_tensor_size > max_tensor_size
205 + assert current_tensor_size <= max_tensor_size, (
206 + f"Current tensor size {current_tensor_size} is larger than "
207 + f"max token num {max_token_num} * hidden size {hidden_size} * "
208 + f"element size {element_size}"
209 + )
210 curr_device = current_platform.get_device_capability()
211 device_capability = curr_device.to_int() if curr_device is not None else None
212
@@ -215,47 +219,6 @@ if flashinfer_comm is not None:
219 get_workspace_fn = (
220 get_fi_ar_quant_workspace if is_quant_pattern else get_fi_ar_workspace
221 )
218 -
219 - orig_norm_out = norm_out
220 -
221 - def unfused_fallback() -> None:
222 - # Fused-path equivalent honoring the op's output-buffer contract:
223 - # with norm_out given, normed -> norm_out and the new residual ->
224 - # the allreduce_in buffer; otherwise normed -> allreduce_in and
225 - # the new residual -> the residual buffer. Runs fine during CUDA
226 - # graph capture (the branch is resolved per captured shape).
227 - #
228 - # weight_bias mirrors the fused kernel's gamma offset (Gemma
229 - # patterns pass 1.0 for rms_norm(x, weight + 1)); fold it into
230 - # the gamma in fp32 like the reference before the plain rms_norm.
231 - gamma = (
232 - rms_gamma
233 - if weight_bias == 0.0
234 - else (rms_gamma.float() + weight_bias).to(rms_gamma.dtype)
235 - )
236 - reduced = tensor_model_parallel_all_reduce(allreduce_in)
237 - new_residual = reduced + residual
238 - if orig_norm_out is None:
239 - torch.ops._C.rms_norm(allreduce_in, new_residual, gamma, rms_eps)
240 - residual.copy_(new_residual)
241 - else:
242 - torch.ops._C.rms_norm(orig_norm_out, new_residual, gamma, rms_eps)
243 - allreduce_in.copy_(new_residual)
244 -
245 - if oversize:
246 - assert not is_quant_pattern, (
247 - "flashinfer fused allreduce: input exceeds the workspace budget "
248 - "and the unfused fallback does not cover quant patterns"
249 - )
250 - logger.warning_once(
251 - "fused allreduce+rmsnorm: %d tokens exceeds the workspace "
252 - "budget (max_token_num=%d); using the unfused fallback for "
253 - "this shape.",
254 - num_tokens,
255 - max_token_num,
256 - )
257 - unfused_fallback()
258 - return
222 workspace = get_workspace_fn(
223 world_size=world_size,
224 rank=get_tensor_model_parallel_rank(),
@@ -289,75 +252,36 @@ if flashinfer_comm is not None:
252 if workspace.backend in ("trtllm", "mnnvl"):
253 layout_code = flashinfer_comm.QuantizationSFLayout.SWIZZLED_128x4
254
292 - # vllm's one-shot size table is tuned for the trtllm backend. For
293 - # mnnvl, flashinfer caps the workspace's one-shot region at its own
294 - # MNNVL_ONE_SHOT_THRESHOLD (~4MB, e.g. 44 tokens at hidden 6144 bf16
295 - # TP8); forcing use_oneshot=True past that raises ValueError instead
296 - # of using two-shot, so every decode capture between the one-shot cap
297 - # and the fusion threshold silently lost fusion. Let flashinfer's
298 - # AUTO heuristic pick the strategy (it matches its own workspace
299 - # sizing), and keep trigger_completion_at_end=True since AUTO may
300 - # select one-shot (see the Lamport NaN note below).
301 - mnnvl_auto = workspace.backend == "mnnvl"
302 - if mnnvl_auto:
303 - use_oneshot = None
304 -
305 - try:
306 - flashinfer_comm.allreduce_fusion(
307 - input=allreduce_in,
308 - workspace=workspace,
309 - pattern=pattern_code,
310 - launch_with_pdl=launch_with_pdl,
311 - output=None,
312 - residual_out=residual_out,
313 - norm_out=norm_out,
314 - quant_out=quant_out,
315 - scale_out=scale_out,
316 - residual_in=residual,
317 - rms_gamma=rms_gamma,
318 - rms_eps=rms_eps,
319 - scale_factor=scale_factor,
320 - layout_code=layout_code,
321 - use_oneshot=use_oneshot,
322 - fp32_acc=fp32_acc,
323 - weight_bias=weight_bias,
324 - # The one-shot Lamport all-reduce signals PDL completion before
325 - # its output buffer is committed when trigger_completion_at_end
326 - # is False, so the next PDL-launched kernel can read the
327 - # uninitialized Lamport buffer and produce NaN. This only fires
328 - # for num_tokens <= PDL_ADVANCE_LAUNCH_TOKENS (the batch=1 /
329 - # spec-decode shapes, where the one-shot path is always
330 - # selected). Complete at the end for the one-shot path; the
331 - # two-shot path is synchronized and keeps the early completion.
332 - # Related one-shot instability in the same kernel:
333 - # flashinfer-ai/flashinfer#1223.
334 - trigger_completion_at_end=True
335 - if mnnvl_auto
336 - else (use_oneshot or num_tokens > PDL_ADVANCE_LAUNCH_TOKENS),
337 - )
338 - except ValueError as e:
339 - # The globally cached workspace can be smaller than this call's
340 - # budget (it is created once by whichever caller runs first, and
341 - # the mnnvl workspace's is_buffer_size_sufficient() checks trtllm
342 - # metadata, so vllm-side accounting cannot see the real capacity).
343 - # flashinfer raises before launching anything, so falling back
344 - # mid-capture is safe. Observed with spec-decode draft graphs
345 - # whose token count (max_num_reqs * (1 + num_spec)) exceeds the
346 - # workspace sized for the fusion threshold.
347 - assert not is_quant_pattern, (
348 - "flashinfer fused allreduce failed and the unfused fallback "
349 - f"does not cover quant patterns: {e}"
350 - )
351 - logger.warning_once(
352 - "flashinfer fused allreduce+rmsnorm unavailable for this "
353 - "shape (num_tokens=%d, max_token_num=%d, world_size=%d): %s. "
354 - "Using the unfused fallback.",
355 - num_tokens,
356 - max_token_num,
357 - world_size,
358 - e,
359 - )
360 - unfused_fallback()
255 + flashinfer_comm.allreduce_fusion(
256 + input=allreduce_in,
257 + workspace=workspace,
258 + pattern=pattern_code,
259 + launch_with_pdl=launch_with_pdl,
260 + output=None,
261 + residual_out=residual_out,
262 + norm_out=norm_out,
263 + quant_out=quant_out,
264 + scale_out=scale_out,
265 + residual_in=residual,
266 + rms_gamma=rms_gamma,
267 + rms_eps=rms_eps,
268 + scale_factor=scale_factor,
269 + layout_code=layout_code,
270 + use_oneshot=use_oneshot,
271 + fp32_acc=fp32_acc,
272 + weight_bias=weight_bias,
273 + # The one-shot Lamport all-reduce signals PDL completion before its
274 + # output buffer is committed when trigger_completion_at_end is
275 + # False, so the next PDL-launched kernel can read the uninitialized
276 + # Lamport buffer and produce NaN. This only fires for
277 + # num_tokens <= PDL_ADVANCE_LAUNCH_TOKENS (the batch=1 / spec-decode
278 + # shapes, where the one-shot path is always selected). Complete at
279 + # the end for the one-shot path; the two-shot path is synchronized
280 + # and keeps the early completion. Related one-shot instability in
281 + # the same kernel: flashinfer-ai/flashinfer#1223.
282 + trigger_completion_at_end=(use_oneshot is True)
283 + or num_tokens > PDL_ADVANCE_LAUNCH_TOKENS,
284 + )
285
286 def call_trtllm_fused_allreduce_norm_fake(
287 allreduce_in: torch.Tensor,
vllm/config/speculative.py
+1 -7
@@ -330,7 +330,6 @@ class SpeculativeConfig:
330 @staticmethod
331 def hf_config_override(hf_config: PretrainedConfig) -> PretrainedConfig:
332 initial_architecture = hf_config.architectures[0]
333 - use_sparse_mtp = hf_config.model_type == "glm_moe_dsa"
333 if hf_config.model_type in (
334 "deepseek_v3",
335 "deepseek_v32",
@@ -340,12 +339,7 @@ class SpeculativeConfig:
339 if hf_config.model_type == "deepseek_mtp":
340 n_predict = getattr(hf_config, "num_nextn_predict_layers", None)
341 hf_config.update(
343 - {
344 - "n_predict": n_predict,
345 - "architectures": [
346 - "DeepseekV32MTPModel" if use_sparse_mtp else "DeepSeekMTPModel"
347 - ],
348 - }
342 + {"n_predict": n_predict, "architectures": ["DeepSeekMTPModel"]}
343 )
344 if hf_config.model_type == "deepseek_v4":
345 hf_config.model_type = "deepseek_mtp"
vllm/cute_utils/__init__.py
-5
@@ -33,11 +33,6 @@ EVICT_NORMAL = Int64(0x1000000000000000)
33 EVICT_FIRST = Int64(0x12F0000000000000)
34 EVICT_LAST = Int64(0x14F0000000000000)
35
36 -TORCH_TO_CUTE_DTYPE = {
37 - torch.bfloat16: BFloat16,
38 - torch.float32: Float32,
39 -}
40 -
36
37 @dsl_user_op
38 def recast_val(x, dtype, *, loc=None, ip=None):
vllm/cute_utils/cvt.py
+1 -22
@@ -1,6 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -from cutlass import BFloat16, Constexpr, Float32, Uint16, Uint32, cute
3 +from cutlass import Constexpr, Float32, Uint32, cute
4 from cutlass._mlir import ir
5 from cutlass._mlir.dialects import llvm, vector
6 from cutlass.cutlass_dsl import T, dsl_user_op
@@ -38,11 +38,6 @@ def bf16x2_to_fp32x2(data, *, loc=None, ip=None) -> tuple[Float32, Float32]:
38 )
39
40 elif isinstance(data, (cute.Tensor, cute.TensorSSA)):
41 - # recast to 32-bit registers if needed
42 - if data.element_type == BFloat16:
43 - data = cute.recast_tensor(data, Uint32)
44 - assert data.element_type == Uint32
45 -
41 # NOTE: the output is always 1D
42 size = cute.size(data.shape)
43 out = cute.make_rmem_tensor(size * 2, Float32)
@@ -89,22 +84,6 @@ def fp8x4_to_bf16x4(x: Uint32, *, loc=None, ip=None) -> cute.TensorSSA:
84 return cute.TensorSSA(vec, 2, Uint32)
85
86
92 -@dsl_user_op
93 -def fp32x2_to_fp8x2(a0: Float32, a1: Float32, *, loc=None, ip=None) -> Uint16:
94 - out = llvm.inline_asm(
95 - T.i16(),
96 - [
97 - a0.ir_value(loc=loc, ip=ip),
98 - a1.ir_value(loc=loc, ip=ip),
99 - ],
100 - "cvt.rn.satfinite.e4m3x2.f32 $0, $2, $1;",
101 - "=h,f,f",
102 - has_side_effects=False,
103 - is_align_stack=False,
104 - )
105 - return Uint16(out)
106 -
107 -
87 @dsl_user_op
88 def fp8x4_to_fp16x4(x: Uint32, *, loc=None, ip=None) -> cute.TensorSSA:
89 out = llvm.inline_asm(
vllm/model_executor/layers/fused_allreduce_gemma_rms_norm.py
+4 -15
@@ -43,7 +43,7 @@ try:
43 if flashinfer_comm is not None
44 else None
45 )
46 -except (ImportError, AttributeError):
46 +except ImportError:
47 flashinfer_trtllm_fused_allreduce_norm = None # type: ignore[assignment]
48 get_fi_ar_workspace = None # type: ignore[assignment]
49 _AR_RESIDUAL_RMS_NORM = None
@@ -52,23 +52,12 @@ except (ImportError, AttributeError):
52 _FI_SUPPORTED_DTYPES = (torch.bfloat16, torch.float16)
53
54
55 -@torch.compiler.assume_constant_result
56 -def _fi_ar_max_size_mb() -> dict[int, float]:
57 - """Flashinfer all-reduce fusion size table for the current device.
58 -
59 - Device capability is constant; marking the result constant keeps this out
60 - of the traced graph. Otherwise the per-forward call reaches
61 - ``current_platform.get_device_capability()`` inside ``torch.compile`` and
62 - graph-breaks ("can't handle functions not implemented in python")."""
63 - from vllm.config.compilation import PassConfig
64 -
65 - return PassConfig.default_fi_allreduce_fusion_max_size_mb()
66 -
67 -
55 def _max_token_num(tp_size: int, hidden_size: int, dtype: torch.dtype) -> int | None:
56 """Workspace token budget for flashinfer fused all-reduce, or None if the
57 current world size / device is unsupported. Mirrors ``FlashInferAllReduce``."""
71 - max_size_mb = _fi_ar_max_size_mb().get(tp_size)
58 + from vllm.config.compilation import PassConfig
59 +
60 + max_size_mb = PassConfig.default_fi_allreduce_fusion_max_size_mb().get(tp_size)
61 if not max_size_mb:
62 return None
63 element_size = torch.tensor([], dtype=dtype).element_size()
vllm/model_executor/models/registry.py
+1 -2
@@ -114,7 +114,7 @@ _TEXT_GENERATION_MODELS = {
114 "Glm4ForCausalLM": ("glm4", "Glm4ForCausalLM"),
115 "Glm4MoeForCausalLM": ("glm4_moe", "Glm4MoeForCausalLM"),
116 "Glm4MoeLiteForCausalLM": ("glm4_moe_lite", "Glm4MoeLiteForCausalLM"),
117 - "GlmMoeDsaForCausalLM": ("vllm.models.deepseek_v32", "GlmMoeDsaForCausalLM"),
117 + "GlmMoeDsaForCausalLM": ("deepseek_v2", "GlmMoeDsaForCausalLM"),
118 "GptOssForCausalLM": ("gpt_oss", "GptOssForCausalLM"),
119 "GPT2LMHeadModel": ("gpt2", "GPT2LMHeadModel"),
120 "GPTJForCausalLM": ("gpt_j", "GPTJForCausalLM"),
@@ -628,7 +628,6 @@ _SPECULATIVE_DECODING_MODELS = {
628 "Eagle3DeepseekV3ForCausalLM": ("deepseek_eagle3", "Eagle3DeepseekV2ForCausalLM"),
629 "EagleDeepSeekMTPModel": ("deepseek_eagle", "EagleDeepseekV3ForCausalLM"),
630 "DeepSeekMTPModel": ("deepseek_mtp", "DeepSeekMTP"),
631 - "DeepseekV32MTPModel": ("vllm.models.deepseek_v32", "DeepseekV32MTP"),
631 "DeepSeekV4MTPModel": ("vllm.models.deepseek_v4", "DeepSeekV4MTP"),
632 "MiniMaxM3MTP": ("vllm.models.minimax_m3", "MiniMaxM3MTP"),
633 "BailingMoeV25MTPModel": ("bailing_moe_mtp", "BailingMoeV25MTPModel"),
vllm/models/deepseek_v32/__init__.py
+4 -18
@@ -6,31 +6,17 @@ DeepSeek V3.2 introduced the DeepSeek Sparse Attention (DSA) architecture:
6 MLA + a "lightning indexer" that selects the top-k tokens for a sparse MLA
7 attend. The same model code serves any DSA checkpoint, including GLM-5.2
8 (``glm_moe_dsa``), which reuses this architecture.
9 -
10 -The optimized kernels under ``nvidia/`` target the Blackwell (SM100) family.
11 -Every other platform — ROCm, XPU, pre-SM100 CUDA (e.g. H100), CPU — falls back
12 -to the generic ``deepseek_v2`` implementation, which already handles the DSA
13 -(index_topk) architecture and is ``torch.compile``-friendly there. This matches
14 -main's behavior on those platforms (no hard failure).
9 """
10
11 from vllm.platforms import current_platform
12
19 -if current_platform.is_cuda() and current_platform.is_device_capability_family(100):
20 - from .nvidia.model import DeepseekV32ForCausalLM
21 - from .nvidia.mtp import DeepseekV32MTP
13 +if current_platform.is_rocm() or current_platform.is_xpu():
14 + raise NotImplementedError("deepseek_v32 currently supports NVIDIA SM100 only.")
15
23 - # GLM-5.2 (glm_moe_dsa) reuses the same optimized DSA module on SM100.
24 - GlmMoeDsaForCausalLM = DeepseekV32ForCausalLM
25 -else:
26 - from vllm.model_executor.models.deepseek_mtp import DeepSeekMTP as DeepseekV32MTP
27 - from vllm.model_executor.models.deepseek_v2 import (
28 - DeepseekV3ForCausalLM as DeepseekV32ForCausalLM,
29 - )
30 - from vllm.model_executor.models.deepseek_v2 import GlmMoeDsaForCausalLM
16 +from .nvidia.model import DeepseekV32ForCausalLM
17 +from .nvidia.mtp import DeepseekV32MTP
18
19 __all__ = [
20 "DeepseekV32ForCausalLM",
21 "DeepseekV32MTP",
35 - "GlmMoeDsaForCausalLM",
22 ]
vllm/models/deepseek_v32/nvidia/attention.py
+12 -72
@@ -4,7 +4,6 @@ import torch
4 import torch.nn as nn
5 from transformers import DeepseekV2Config, DeepseekV3Config
6
7 -import vllm._custom_ops as ops
7 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
8 from vllm.config import CacheConfig, VllmConfig
9 from vllm.distributed import get_tensor_model_parallel_world_size
@@ -22,15 +21,17 @@ from vllm.model_executor.layers.quantization.utils.fp8_utils import (
21 per_token_group_quant_fp8,
22 )
23 from vllm.model_executor.layers.rotary_embedding import get_rope
25 -from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer
24 +from vllm.model_executor.layers.sparse_attn_indexer import (
25 + SparseAttnIndexer,
26 + sparse_attn_indexer,
27 +)
28 from vllm.model_executor.models.deepseek_v2 import (
29 DeepSeekV2FusedQkvAProjLinear,
30 DeepseekV32IndexerCache,
31 yarn_get_mscale,
32 )
33 from vllm.model_executor.models.utils import extract_layer_index
32 -from vllm.platforms import current_platform
33 -from vllm.utils.torch_utils import _encode_layer_name, is_quantized_kv_cache
34 +from vllm.utils.torch_utils import is_quantized_kv_cache
35
36 from .kernels import fused_norm_rope, fused_q
37
@@ -170,11 +171,6 @@ class DeepseekV32Attention(MLAAttention):
171 ) -> None:
172 quant_config = vllm_config.quant_config
173 cache_config = vllm_config.cache_config
173 - if cache_config is not None and cache_config.cache_dtype == "auto":
174 - # This implementation requires an FP8 sparse cache. Start with the
175 - # generic FP8 format; MLAAttention will canonicalize it for the
176 - # selected sparse backend.
177 - cache_config.cache_dtype = "fp8"
174
175 hidden_size = config.hidden_size
176 qk_nope_head_dim = config.qk_nope_head_dim
@@ -335,37 +331,6 @@ class DeepseekV32Attention(MLAAttention):
331 rope_parameters=config.rope_parameters,
332 is_neox_style=False,
333 )
338 - # Decode-M GEMM dispatch for the bf16 A/B projections, measured on
339 - # B300 and B200 (graph replay + rotating weights vs fp64):
340 - # bf16_skinny_gemm for M <= skinny_max, dsv3_fused_a_gemm up to
341 - # M = 16, cuBLAS above. Blackwell-only: the boundaries and the
342 - # fused_a tile choices are tied to the 148/152-SM single-wave
343 - # geometry. DSv3.2 q_b has no skinny tier (K=1536 does not fit the
344 - # GEMV's 128x8 K-step; fused_a still wins 1.6-1.8x there).
345 - # Quantized linear methods may not register .weight until
346 - # process_weights_after_loading (e.g. compressed-tensors NVFP4
347 - # registers weight_packed at init) — probe with getattr.
348 - # (N, K) -> skinny_max
349 - qkv_a_dispatch = {(2624, 6144): 2, (2112, 7168): 2}
350 - q_b_dispatch = {(2048, 2048): 2, (3072, 1536): 0}
351 - ok = (
352 - current_platform.is_device_capability_family(100)
353 - and hasattr(torch.ops._C, "bf16_skinny_gemm")
354 - and hasattr(torch.ops._C, "dsv3_fused_a_gemm")
355 - )
356 - qkv_a_weight = getattr(self.fused_qkv_a_proj, "weight", None)
357 - q_b_weight = getattr(self.q_b_proj, "weight", None)
358 - self._qkv_a_skinny_max = (
359 - qkv_a_dispatch.get(tuple(qkv_a_weight.shape))
360 - if ok and qkv_a_weight is not None and qkv_a_weight.dtype == torch.bfloat16
361 - else None
362 - )
363 - self._q_b_skinny_max = (
364 - q_b_dispatch.get(tuple(q_b_weight.shape))
365 - if ok and q_b_weight is not None and q_b_weight.dtype == torch.bfloat16
366 - else None
367 - )
368 -
334 # Lightning indexer uses its own RoPE; interleave maps to non-NeoX.
335 self.indexer_rope_emb = get_rope(
336 qk_rope_head_dim,
@@ -374,32 +339,13 @@ class DeepseekV32Attention(MLAAttention):
339 is_neox_style=not getattr(config, "indexer_rope_interleave", False),
340 )
341
377 - def _decode_m_gemm(
378 - self, x: torch.Tensor, weight: torch.Tensor, skinny_max: int
379 - ) -> torch.Tensor:
380 - """x @ weight.T for decode M <= 16."""
381 - if x.shape[0] == 0:
382 - return torch.empty((0, weight.shape[0]), dtype=x.dtype, device=x.device)
383 - if x.shape[0] <= skinny_max:
384 - return ops.bf16_skinny_gemm(x, weight)
385 - out = torch.empty((x.shape[0], weight.shape[0]), dtype=x.dtype, device=x.device)
386 - ops.dsv3_fused_a_gemm(out, x, weight.t())
387 - return out
388 -
342 def forward( # type: ignore[override]
343 self,
344 positions: torch.Tensor,
345 hidden_states: torch.Tensor,
346 ) -> torch.Tensor:
347 # Captured: A-projections (+ indexer A-GEMM on indexer layers).
395 - if self._qkv_a_skinny_max is not None and hidden_states.shape[0] <= 16:
396 - qkv_lora = self._decode_m_gemm(
397 - hidden_states,
398 - self.fused_qkv_a_proj.weight,
399 - self._qkv_a_skinny_max,
400 - )
401 - else:
402 - qkv_lora = self.fused_qkv_a_proj(hidden_states)[0]
348 + qkv_lora = self.fused_qkv_a_proj(hidden_states)[0]
349 q_c, kv_c, k_pe = qkv_lora.split(
350 [self.q_lora_rank, self.kv_lora_rank, self.qk_rope_head_dim], dim=-1
351 )
@@ -452,9 +398,7 @@ class DeepseekV32Attention(MLAAttention):
398 assert isinstance(slot_mapping, dict)
399 mla_slot = slot_mapping.get(self.layer_name)
400
455 - # skip_topk (index_share_for_mtp_iteration reuse mode): don't run the
456 - # indexer this step; keep the top-k already in the shared buffer.
457 - if self.indexer is not None and not self.skip_topk:
401 + if self.indexer is not None:
402 has_indexer = True
403 indexer_k_norm_w = self.indexer.k_norm.weight
404 indexer_k_norm_bias = self.indexer.k_norm.bias
@@ -507,16 +451,12 @@ class DeepseekV32Attention(MLAAttention):
451 index_rope_interleave=self._index_rope_interleave,
452 )
453
510 - if self._q_b_skinny_max is not None and q_c.shape[0] <= 16:
511 - q = self._decode_m_gemm(q_c, self.q_b_proj.weight, self._q_b_skinny_max)
512 - else:
513 - q = self.q_b_proj(q_c)[0]
514 - q = q.view(-1, self.num_local_heads, self.qk_head_dim)
454 + q = self.q_b_proj(q_c)[0].view(-1, self.num_local_heads, self.qk_head_dim)
455 q_nope, q_pe = q.split([self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1)
456 q_nope = q_nope.transpose(0, 1)
457 ql_nope = torch.bmm(q_nope, self.W_UK_T).transpose(0, 1)
458
519 - if self.indexer is not None and not self.skip_topk:
459 + if self.indexer is not None:
460 index_q = self.indexer.wq_b(q_c)[0]
461 index_q = index_q.view(-1, self.indexer.n_head, self.indexer.head_dim)
462 else:
@@ -538,10 +478,10 @@ class DeepseekV32Attention(MLAAttention):
478 quantize_mqa=self._fp8_query,
479 )
480
541 - if self.indexer is not None and not self.skip_topk:
542 - torch.ops.vllm.sparse_attn_indexer(
481 + if self.indexer is not None:
482 + sparse_attn_indexer(
483 q_c,
544 - _encode_layer_name(self.indexer.k_cache.prefix),
484 + self.indexer.k_cache.prefix,
485 self.indexer.k_cache.kv_cache,
486 index_q_fp8,
487 None, # q_scale folded into weights on the fp8 path
vllm/models/deepseek_v32/nvidia/kernels.py
+16 -284
@@ -2,80 +2,13 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import torch
4
5 -from vllm.platforms import current_platform
5 from vllm.triton_utils import tl, triton
7 -from vllm.utils.import_utils import has_cutedsl
8 -from vllm.utils.torch_utils import direct_register_custom_op
6
7 # Cache of tiny 1-element dummy tensors (per device, dtype) reused by the
8 # has_indexer=False path so the indexer args don't allocate every call.
9 _DUMMY_CACHE: dict[tuple, torch.Tensor] = {}
10
11
15 -@torch.compiler.assume_constant_result
16 -def _can_use_fused_q_cutedsl() -> bool:
17 - return current_platform.has_device_capability(100) and has_cutedsl()
18 -
19 -
20 -@torch.compiler.assume_constant_result
21 -def _is_arch_support_pdl() -> bool:
22 - return (
23 - current_platform.has_device_capability(100)
24 - and current_platform.is_arch_support_pdl()
25 - )
26 -
27 -
28 -def _fused_q_cutedsl_impl(
29 - positions: torch.Tensor,
30 - q_pe: torch.Tensor,
31 - rope_cache: torch.Tensor,
32 - ql_nope: torch.Tensor,
33 - q_scale: torch.Tensor,
34 - mqa_output: torch.Tensor,
35 - idx_q: torch.Tensor,
36 - idx_rope_cache: torch.Tensor,
37 - idx_weights: torch.Tensor,
38 - idx_weights_softmax_scale: float,
39 - idx_weights_head_scale: float,
40 - idx_q_fp8: torch.Tensor,
41 - idx_weights_out: torch.Tensor,
42 - has_indexer: bool,
43 - index_rope_interleave: bool,
44 -) -> None:
45 - from .ops.fused_q_cutedsl import fused_q_cutedsl
46 -
47 - fused_q_cutedsl(
48 - positions,
49 - q_pe,
50 - rope_cache,
51 - ql_nope,
52 - q_scale,
53 - mqa_output,
54 - idx_q,
55 - idx_rope_cache,
56 - idx_weights,
57 - idx_weights_softmax_scale,
58 - idx_weights_head_scale,
59 - idx_q_fp8,
60 - idx_weights_out,
61 - has_indexer=has_indexer,
62 - index_rope_interleave=index_rope_interleave,
63 - )
64 -
65 -
66 -def _fused_q_cutedsl_fake(*args, **kwargs) -> None:
67 - pass
68 -
69 -
70 -direct_register_custom_op(
71 - op_name="fused_q_cutedsl",
72 - op_func=_fused_q_cutedsl_impl,
73 - mutates_args=["mqa_output", "idx_q_fp8", "idx_weights_out"],
74 - fake_impl=_fused_q_cutedsl_fake,
75 - dispatch_key="CUDA",
76 -)
77 -
78 -
12 def _dummy(shape: tuple, dtype: torch.dtype, device: torch.device) -> torch.Tensor:
13 key = (shape, dtype, device)
14 t = _DUMMY_CACHE.get(key)
@@ -214,14 +147,9 @@ def _fused_norm_rope_kernel(
147 TOPK_BLOCK_SIZE: tl.constexpr,
148 HAS_INDEXER: tl.constexpr,
149 INDEX_ROPE_INTERLEAVE: tl.constexpr,
217 - USE_PDL: tl.constexpr,
150 ):
151 pid = tl.program_id(0)
152 tok_idx = tl.program_id(1)
221 - if USE_PDL:
222 - tl.extra.cuda.gdc_wait()
223 - tl.extra.cuda.gdc_launch_dependents()
224 -
153 if pid == 3:
154 if not HAS_INDEXER:
155 # Shared layer: reuse the previous indexer layer's top-k; do not
@@ -238,17 +166,6 @@ def _fused_norm_rope_kernel(
166 )
167 return
168
241 - if pid == 2:
242 - # Q RMS norm does not depend on cache availability. In particular,
243 - # profiling uses a negative dummy slot mapping but still consumes Q.
244 - q_block = tl.arange(0, Q_BLOCK_SIZE)
245 - q_mask = q_block < Q_DIM
246 - q_c = tl.load(q_c_ptr + tok_idx * q_c_stride + q_block, mask=q_mask, other=0.0)
247 - q_c_rms_w = tl.load(q_rms_norm_w_ptr + q_block, mask=q_mask)
248 - q_c = _rms_norm(q_c, q_c_rms_w, q_rms_eps, Q_DIM)
249 - tl.store(q_c_out_ptr + tok_idx * q_c_out_stride + q_block, q_c, mask=q_mask)
250 - return
251 -
169 if slot_mapping_ptr is None:
170 # Memory profiling run.
171 return
@@ -257,7 +174,15 @@ def _fused_norm_rope_kernel(
174 # Padding
175 return
176
260 - if pid == 1:
177 + if pid == 2:
178 + # Q RMS norm
179 + q_block = tl.arange(0, Q_BLOCK_SIZE)
180 + q_mask = q_block < Q_DIM
181 + q_c = tl.load(q_c_ptr + tok_idx * q_c_stride + q_block, mask=q_mask, other=0.0)
182 + q_c_rms_w = tl.load(q_rms_norm_w_ptr + q_block, mask=q_mask)
183 + q_c = _rms_norm(q_c, q_c_rms_w, q_rms_eps, Q_DIM)
184 + tl.store(q_c_out_ptr + tok_idx * q_c_out_stride + q_block, q_c, mask=q_mask)
185 + elif pid == 1:
186 # KV RMS Norm + KV RoPE + MLA concat_and_cache.
187 # Merged so the normed kv_c and RoPE'd k_pe can be written
188 # to the MLA KV cache directly without a separate kernel.
@@ -286,6 +211,9 @@ def _fused_norm_rope_kernel(
211 r2 = x2 * cos + x1 * sin
212
213 # MLA concat_and_cache: write [kv_c_normed, k_pe_roped] to cache.
214 + if mla_cache_entry_stride == 0:
215 + return
216 +
217 mla_block_size = mla_cache_block_stride // mla_cache_entry_stride
218 mla_block_idx = slot_idx // mla_block_size
219 mla_block_off = slot_idx % mla_block_size
@@ -435,7 +363,7 @@ def _fused_norm_rope_kernel(
363 )
364
365
438 -def _fused_norm_rope_impl(
366 +def fused_norm_rope(
367 positions: torch.Tensor,
368 q_c: torch.Tensor,
369 q_rms_norm_w: torch.Tensor,
@@ -537,17 +465,12 @@ def _fused_norm_rope_impl(
465 # Dummy values — pid 2 will skip the MLA cache write because
466 # slot_mapping is all -1.
467 mla_kv_cache = torch.empty(0, dtype=torch.bfloat16, device=device)
540 - # Torch's Triton HOP mutation analysis compiles every program-id
541 - # branch even though the dummy slot mapping returns before cache
542 - # access. Keep the unused strides nonzero so that analysis can lower
543 - # the block-size calculation safely.
544 - mla_block_stride = 1
545 - mla_entry_stride = 1
468 + mla_block_stride = 0
469 + mla_entry_stride = 0
470 mla_k_scale = torch.ones(1, dtype=torch.float32, device=device)
471
472 if q_c_out is None:
473 q_c_out = torch.empty_like(q_c)
550 - use_pdl = _is_arch_support_pdl()
474 _fused_norm_rope_kernel[(4, num_tokens)](
475 positions,
476 # Q RMS norm
@@ -606,157 +529,6 @@ def _fused_norm_rope_impl(
529 TOPK_BLOCK_SIZE=1024,
530 HAS_INDEXER=has_indexer,
531 INDEX_ROPE_INTERLEAVE=index_rope_interleave,
609 - USE_PDL=use_pdl,
610 - launch_pdl=use_pdl,
611 - )
612 - return q_c_out
613 -
614 -
615 -def _fused_norm_rope_op(
616 - positions: torch.Tensor,
617 - q_c: torch.Tensor,
618 - q_rms_norm_w: torch.Tensor,
619 - q_rms_eps: float,
620 - kv_c: torch.Tensor,
621 - kv_rms_norm_w: torch.Tensor,
622 - kv_rms_eps: float,
623 - k_pe: torch.Tensor,
624 - k_rope_cos_sin_cache: torch.Tensor,
625 - index_k: torch.Tensor | None,
626 - index_k_layer_norm_w: torch.Tensor | None,
627 - index_k_layer_norm_bias: torch.Tensor | None,
628 - index_k_layer_norm_eps: float,
629 - index_k_rope_cos_sin_cache: torch.Tensor | None,
630 - topk_indices_buffer: torch.Tensor,
631 - slot_mapping: torch.Tensor | None,
632 - indexer_k_cache: torch.Tensor | None,
633 - mla_kv_cache: torch.Tensor | None,
634 - mla_kv_cache_dtype: str,
635 - mla_k_scale: torch.Tensor | None,
636 - has_indexer: bool,
637 - index_rope_interleave: bool,
638 - q_c_out: torch.Tensor,
639 -) -> None:
640 - _fused_norm_rope_impl(
641 - positions,
642 - q_c,
643 - q_rms_norm_w,
644 - q_rms_eps,
645 - kv_c,
646 - kv_rms_norm_w,
647 - kv_rms_eps,
648 - k_pe,
649 - k_rope_cos_sin_cache,
650 - index_k,
651 - index_k_layer_norm_w,
652 - index_k_layer_norm_bias,
653 - index_k_layer_norm_eps,
654 - index_k_rope_cos_sin_cache,
655 - topk_indices_buffer,
656 - slot_mapping=slot_mapping,
657 - indexer_k_cache=indexer_k_cache,
658 - mla_kv_cache=mla_kv_cache,
659 - mla_kv_cache_dtype=mla_kv_cache_dtype,
660 - mla_k_scale=mla_k_scale,
661 - has_indexer=has_indexer,
662 - index_rope_interleave=index_rope_interleave,
663 - q_c_out=q_c_out,
664 - )
665 -
666 -
667 -def _fused_norm_rope_fake(
668 - positions: torch.Tensor,
669 - q_c: torch.Tensor,
670 - q_rms_norm_w: torch.Tensor,
671 - q_rms_eps: float,
672 - kv_c: torch.Tensor,
673 - kv_rms_norm_w: torch.Tensor,
674 - kv_rms_eps: float,
675 - k_pe: torch.Tensor,
676 - k_rope_cos_sin_cache: torch.Tensor,
677 - index_k: torch.Tensor | None,
678 - index_k_layer_norm_w: torch.Tensor | None,
679 - index_k_layer_norm_bias: torch.Tensor | None,
680 - index_k_layer_norm_eps: float,
681 - index_k_rope_cos_sin_cache: torch.Tensor | None,
682 - topk_indices_buffer: torch.Tensor,
683 - slot_mapping: torch.Tensor | None,
684 - indexer_k_cache: torch.Tensor | None,
685 - mla_kv_cache: torch.Tensor | None,
686 - mla_kv_cache_dtype: str,
687 - mla_k_scale: torch.Tensor | None,
688 - has_indexer: bool,
689 - index_rope_interleave: bool,
690 - q_c_out: torch.Tensor,
691 -) -> None:
692 - return
693 -
694 -
695 -direct_register_custom_op(
696 - op_name="fused_norm_rope_deepseek_v32",
697 - op_func=_fused_norm_rope_op,
698 - mutates_args=[
699 - "topk_indices_buffer",
700 - "indexer_k_cache",
701 - "mla_kv_cache",
702 - "q_c_out",
703 - ],
704 - fake_impl=_fused_norm_rope_fake,
705 - dispatch_key="CUDA",
706 -)
707 -
708 -
709 -def fused_norm_rope(
710 - positions: torch.Tensor,
711 - q_c: torch.Tensor,
712 - q_rms_norm_w: torch.Tensor,
713 - q_rms_eps: float,
714 - kv_c: torch.Tensor,
715 - kv_rms_norm_w: torch.Tensor,
716 - kv_rms_eps: float,
717 - k_pe: torch.Tensor,
718 - k_rope_cos_sin_cache: torch.Tensor,
719 - index_k: torch.Tensor | None,
720 - index_k_layer_norm_w: torch.Tensor | None,
721 - index_k_layer_norm_bias: torch.Tensor | None,
722 - index_k_layer_norm_eps: float,
723 - index_k_rope_cos_sin_cache: torch.Tensor | None,
724 - topk_indices_buffer: torch.Tensor,
725 - slot_mapping: torch.Tensor | None = None,
726 - indexer_k_cache: torch.Tensor | None = None,
727 - mla_kv_cache: torch.Tensor | None = None,
728 - mla_kv_cache_dtype: str = "auto",
729 - mla_k_scale: torch.Tensor | None = None,
730 - has_indexer: bool = True,
731 - index_rope_interleave: bool = False,
732 - q_c_out: torch.Tensor | None = None,
733 -) -> torch.Tensor:
734 - if q_c_out is None:
735 - q_c_out = torch.empty_like(q_c)
736 - torch.ops.vllm.fused_norm_rope_deepseek_v32(
737 - positions,
738 - q_c,
739 - q_rms_norm_w,
740 - q_rms_eps,
741 - kv_c,
742 - kv_rms_norm_w,
743 - kv_rms_eps,
744 - k_pe,
745 - k_rope_cos_sin_cache,
746 - index_k,
747 - index_k_layer_norm_w,
748 - index_k_layer_norm_bias,
749 - index_k_layer_norm_eps,
750 - index_k_rope_cos_sin_cache,
751 - topk_indices_buffer,
752 - slot_mapping,
753 - indexer_k_cache,
754 - mla_kv_cache,
755 - mla_kv_cache_dtype,
756 - mla_k_scale,
757 - has_indexer,
758 - index_rope_interleave,
759 - q_c_out,
532 )
533 return q_c_out
534
@@ -810,14 +582,10 @@ def _fused_q_kernel(
582 HAS_INDEXER: tl.constexpr,
583 INDEX_ROPE_INTERLEAVE: tl.constexpr,
584 QUANTIZE_MQA: tl.constexpr,
813 - USE_PDL: tl.constexpr,
585 ):
586 pid = tl.program_id(0)
587 tok_idx = tl.program_id(1)
588 head_idx = tl.program_id(2)
818 - if USE_PDL:
819 - tl.extra.cuda.gdc_wait()
820 - tl.extra.cuda.gdc_launch_dependents()
589
590 if pid == 2:
591 # ql_nope quantize + pack into the front of mqa_q_fp8. On the bf16
@@ -1005,12 +773,10 @@ def fused_q(
773 ``(ql_nope, q_pe)`` tuple the backend expects.
774 """
775 assert positions.ndim == 1
1008 - assert positions.dtype == torch.int64
776 assert q_pe.ndim == 3
777 assert q_pe_cos_sin_cache.ndim == 2
778 assert ql_nope.ndim == 3
779 assert ql_nope.shape[:2] == q_pe.shape[:2]
1013 - assert q_scale.dtype == torch.float32 and q_scale.numel() == 1
780
781 num_tokens = positions.shape[0]
782 num_q_heads = q_pe.shape[1]
@@ -1028,18 +794,7 @@ def fused_q(
794 assert index_weights is not None
795 num_index_q_heads = index_q.shape[1]
796 index_q_head_dim = index_q.shape[2]
1031 - use_cutedsl = False
1032 - if _can_use_fused_q_cutedsl():
1033 - from .ops.fused_q_cutedsl import is_fused_q_cutedsl_supported
1034 -
1035 - use_cutedsl = is_fused_q_cutedsl_supported(
1036 - q_pe,
1037 - index_q,
1038 - ql_nope,
1039 - has_indexer=has_indexer,
1040 - quantize_mqa=quantize_mqa,
1041 - )
1042 - grid_heads = max(mqa_grid_heads, num_index_q_heads if has_indexer else 1)
797 + grid_heads = max(mqa_grid_heads, num_index_q_heads)
798 if quantize_mqa:
799 # fp8 path: pack [ql_nope; q_pe] into a single fp8 tensor.
800 mqa_q_fp8 = torch.empty(
@@ -1060,27 +815,6 @@ def fused_q(
815
816 index_q_fp8 = torch.empty_like(index_q, dtype=torch.float8_e4m3fn)
817 index_weights_out = torch.empty_like(index_weights, dtype=torch.float32)
1063 - if use_cutedsl:
1064 - torch.ops.vllm.fused_q_cutedsl(
1065 - positions,
1066 - q_pe,
1067 - q_pe_cos_sin_cache,
1068 - ql_nope,
1069 - q_scale,
1070 - mqa_q,
1071 - index_q,
1072 - index_q_cos_sin_cache,
1073 - index_weights,
1074 - index_weights_softmax_scale,
1075 - index_weights_head_scale,
1076 - index_q_fp8,
1077 - index_weights_out,
1078 - has_indexer=has_indexer,
1079 - index_rope_interleave=index_rope_interleave,
1080 - )
1081 - return index_q_fp8, index_weights_out, mqa_q
1082 -
1083 - use_pdl = _is_arch_support_pdl()
818 _fused_q_kernel[(3, num_tokens, grid_heads)](
819 positions,
820 q_pe,
@@ -1122,8 +856,6 @@ def fused_q(
856 HAS_INDEXER=has_indexer,
857 INDEX_ROPE_INTERLEAVE=index_rope_interleave,
858 QUANTIZE_MQA=quantize_mqa,
1125 - USE_PDL=use_pdl,
1126 - launch_pdl=use_pdl,
859 # num_warps=1 is optimal here: each program is a single 128-element
860 # rope+quant, so the kernel is program-count/occupancy bound, not
861 # per-program compute bound (swept 1/2/4/8 — 1 wins or ties everywhere).
vllm/models/deepseek_v32/nvidia/model.py
-4
@@ -6,7 +6,6 @@ from itertools import islice
6
7 import torch
8
9 -from vllm.compilation.decorators import support_torch_compile
9 from vllm.config import VllmConfig
10 from vllm.distributed import (
11 get_pp_group,
@@ -40,7 +39,6 @@ from vllm.model_executor.models.utils import (
39 sequence_parallel_chunk,
40 )
41 from vllm.sequence import IntermediateTensors
43 -from vllm.v1.attention.backends.mla.sparse_utils import register_phys_shadow
42
43 from .attention import DeepseekV32Attention
44 from .fused_ops import fused_allreduce_rms_norm
@@ -179,7 +177,6 @@ class DeepseekV32DecoderLayer(torch.nn.Module):
177 return hidden_states, residual
178
179
182 -@support_torch_compile
180 class DeepseekV32Model(torch.nn.Module):
181 fall_back_to_pt_during_load = False
182
@@ -203,7 +200,6 @@ class DeepseekV32Model(torch.nn.Module):
200 dtype=torch.int32,
201 device=self.device,
202 )
206 - register_phys_shadow(topk_indices_buffer)
203
204 if get_pp_group().is_first_rank:
205 self.embed_tokens = VocabParallelEmbedding(
vllm/models/deepseek_v32/nvidia/mtp.py
+6 -76
@@ -6,9 +6,9 @@ from collections.abc import Callable, Iterable
6 import torch
7 import torch.nn as nn
8
9 -import vllm._custom_ops as ops
9 from vllm._aiter_ops import rocm_aiter_ops
10 from vllm.config import VllmConfig
11 +from vllm.distributed import tensor_model_parallel_all_reduce
12 from vllm.model_executor.layers.fused_moe import (
13 fused_moe_make_expert_params_mapping,
14 )
@@ -40,12 +40,7 @@ from vllm.model_executor.models.utils import (
40 )
41 from vllm.platforms import current_platform
42 from vllm.sequence import IntermediateTensors
43 -from vllm.v1.attention.backends.mla.sparse_utils import (
44 - phys_shadow,
45 - register_phys_shadow,
46 -)
43
48 -from .fused_ops import fused_allreduce_rms_norm
44 from .kernels import fused_eh_norm
45 from .model import DeepseekV32DecoderLayer
46
@@ -61,19 +56,6 @@ class DeepseekV32MultiTokenPredictorLayer(nn.Module):
56 self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
57 self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
58 self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
64 - # bf16 skinny GEMM for the eh_proj, B300+B200 measured: GLM-5.2
65 - # (6144, 12288) wins at M <= 3, DSv3.2 (7168, 14336) at M <= 2;
66 - # cuBLAS holds above (the 151/205MB weights stream at ~5.7TB/s).
67 - eh_dispatch = {(6144, 12288): 3, (7168, 14336): 2}
68 - eh_weight = getattr(self.eh_proj, "weight", None)
69 - self._eh_skinny_max = (
70 - eh_dispatch.get(tuple(eh_weight.shape), 0)
71 - if current_platform.is_device_capability_family(100)
72 - and hasattr(torch.ops._C, "bf16_skinny_gemm")
73 - and eh_weight is not None
74 - and eh_weight.dtype == torch.bfloat16
75 - else 0
76 - )
59
60 topk_indices_buffer = torch.empty(
61 vllm_config.scheduler_config.max_num_batched_tokens,
@@ -81,7 +63,6 @@ class DeepseekV32MultiTokenPredictorLayer(nn.Module):
63 dtype=torch.int32,
64 device=current_platform.device_type,
65 )
84 - register_phys_shadow(topk_indices_buffer)
66 self.shared_head = SharedHead(
67 config=config, prefix=prefix, quant_config=quant_config
68 )
@@ -110,13 +91,7 @@ class DeepseekV32MultiTokenPredictorLayer(nn.Module):
91 self.hnorm.weight,
92 self.enorm.variance_epsilon,
93 )
113 - if (
114 - eh_input.shape[0] <= self._eh_skinny_max
115 - and eh_input.dtype == torch.bfloat16
116 - ):
117 - hidden_states = ops.bf16_skinny_gemm(eh_input, self.eh_proj.weight)
118 - else:
119 - hidden_states = self.eh_proj(eh_input)
94 + hidden_states = self.eh_proj(eh_input)
95 hidden_states, residual = self.mtp_block(
96 positions=positions, hidden_states=hidden_states, residual=None
97 )
@@ -126,6 +101,9 @@ class DeepseekV32MultiTokenPredictorLayer(nn.Module):
101 positions.shape[0],
102 is_sequence_parallel=self.mtp_block.use_sequence_parallel_moe,
103 )
104 + if not self.mtp_block.use_sequence_parallel_moe:
105 + # Without sequence parallelism, the MoE output is left un-reduced.
106 + hidden_states = tensor_model_parallel_all_reduce(hidden_states)
107 # Recycle the POST-final-norm hidden into the next draft step. The
108 # residual-add is fused into the final RMSNorm so it is computed
109 # exactly once, and the result is returned for both tuple positions:
@@ -136,14 +114,7 @@ class DeepseekV32MultiTokenPredictorLayer(nn.Module):
114 # is understood by both the V2 speculator (isinstance-tuple check) and
115 # the legacy proposer (model_returns_tuple is True for the
116 # DeepSeekMTPModel architecture).
139 - if self.mtp_block.use_sequence_parallel_moe:
140 - hidden_states, _ = self.shared_head.norm(hidden_states, residual)
141 - else:
142 - # The MoE output is left un-reduced; fuse its all-reduce into the
143 - # final norm, as the main model does at layer boundaries.
144 - hidden_states, _ = fused_allreduce_rms_norm(
145 - hidden_states, residual, self.shared_head.norm
146 - )
117 + hidden_states, _ = self.shared_head.norm(hidden_states, residual)
118 return hidden_states, hidden_states
119
120
@@ -186,15 +157,6 @@ class DeepseekV32MultiTokenPredictor(nn.Module):
157 if self_attn is not None and hasattr(self_attn, "topk_indices_buffer"):
158 topk_indices_buffer = self_attn.topk_indices_buffer
159 topk_indices_buffer[:num_slots] = topk_indices_buffer[slot_ids]
189 - # The physical-index shadow mirrors this buffer row-wise
190 - # (shadow[j] == convert(logical[j])); re-arrange it the same
191 - # way or skip_topk draft steps read the step-0 multi-token
192 - # layout's rows for the wrong tokens/requests.
193 - shadow = phys_shadow(topk_indices_buffer)
194 - if shadow is not None:
195 - phys, seq = shadow
196 - phys[:num_slots] = phys[slot_ids]
197 - seq[:num_slots] = seq[slot_ids]
160
161 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
162 return self.embed_tokens(input_ids)
@@ -230,19 +192,6 @@ class DeepseekV32MultiTokenPredictor(nn.Module):
192 # second RMSNorm.
193 return self.logits_processor(mtp_layer.shared_head.head, hidden_states)
194
233 - def get_top_tokens(
234 - self,
235 - hidden_states: torch.Tensor,
236 - spec_step_idx: int = 0,
237 - ) -> torch.Tensor:
238 - # Greedy draft token ids via vocab-parallel local argmax: skips the
239 - # full-vocab logits all-gather (use_local_argmax_reduction).
240 - current_step_idx = spec_step_idx % self.num_mtp_layers
241 - mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
242 - return self.logits_processor.get_top_tokens(
243 - mtp_layer.shared_head.head, hidden_states
244 - )
245 -
195
196 class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
197 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
@@ -291,25 +240,6 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
240 ) -> torch.Tensor | None:
241 return self.model.compute_logits(hidden_states, spec_step_idx)
242
294 - def get_top_tokens(
295 - self,
296 - hidden_states: torch.Tensor,
297 - spec_step_idx: int | None = None,
298 - ) -> torch.Tensor:
299 - # The V2 speculator calls this without spec_step_idx; that is only
300 - # correct while a single MTP layer is cycled for every draft step
301 - # (num_nextn_predict_layers == 1, as in GLM-5.2/DSV3.2). Guard the
302 - # assumption so a future multi-layer MTP fails loudly instead of
303 - # silently using step 0's head for every step.
304 - if spec_step_idx is None:
305 - assert self.model.num_mtp_layers == 1, (
306 - "get_top_tokens called without spec_step_idx on a "
307 - "multi-layer MTP; thread the draft step index through "
308 - "the speculator."
309 - )
310 - spec_step_idx = 0
311 - return self.model.get_top_tokens(hidden_states, spec_step_idx)
312 -
243 def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
244 spec_layer_weight_names = [
245 "embed_tokens",
vllm/models/deepseek_v32/nvidia/ops/__init__.py deleted
-2
@@ -1,2 +0,0 @@
1 -# SPDX-License-Identifier: Apache-2.0
2 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/models/deepseek_v32/nvidia/ops/fused_q_cutedsl.py deleted
-532
@@ -1,532 +0,0 @@
1 -# SPDX-License-Identifier: Apache-2.0
2 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -from functools import cache
4 -
5 -import cutlass
6 -import cutlass.cute as cute
7 -import torch
8 -from cuda.bindings.driver import CUstream
9 -from cutlass import BFloat16, Float8E4M3FN, Float32, Int64, Uint8, Uint16, Uint32
10 -
11 -from vllm.cute_utils import TORCH_TO_CUTE_DTYPE, cvt
12 -
13 -
14 -def _make_fake_tensor(dtype, shape, divisibility):
15 - stride = tuple(
16 - 1 if i == len(shape) - 1 else cute.sym_int64(divisibility=divisibility)
17 - for i in range(len(shape))
18 - )
19 - return cute.runtime.make_fake_tensor(
20 - dtype,
21 - shape,
22 - stride,
23 - assumed_align=divisibility * dtype.width // 8,
24 - )
25 -
26 -
27 -def is_fused_q_cutedsl_supported(
28 - q_pe: torch.Tensor,
29 - index_q: torch.Tensor | None,
30 - ql_nope: torch.Tensor,
31 - *,
32 - has_indexer: bool,
33 - quantize_mqa: bool,
34 -) -> bool:
35 - if not (
36 - quantize_mqa
37 - and q_pe.dtype == ql_nope.dtype == torch.bfloat16
38 - and q_pe.shape[-1] == 64
39 - and ql_nope.shape[-1] == 512
40 - ):
41 - return False
42 - return not has_indexer or (
43 - index_q is not None
44 - and index_q.dtype == torch.bfloat16
45 - and index_q.shape[1] % 16 == 0
46 - and index_q.shape[-1] == 128
47 - )
48 -
49 -
50 -def fused_q_cutedsl(
51 - positions: torch.Tensor,
52 - q_pe: torch.Tensor,
53 - rope_cache: torch.Tensor,
54 - ql_nope: torch.Tensor,
55 - q_scale: torch.Tensor,
56 - mqa_output: torch.Tensor,
57 - idx_q: torch.Tensor,
58 - idx_rope_cache: torch.Tensor,
59 - idx_weights: torch.Tensor,
60 - idx_weights_softmax_scale: float,
61 - idx_weights_head_scale: float,
62 - idx_q_fp8: torch.Tensor,
63 - idx_weights_out: torch.Tensor,
64 - has_indexer: bool = True,
65 - index_rope_interleave: bool = True,
66 -) -> None:
67 - _, num_heads, rope_dim = q_pe.shape
68 - _, _, nope_dim = ql_nope.shape
69 - _, num_idx_heads, idx_dim = idx_q.shape
70 -
71 - if has_indexer:
72 - idx_rope_type = TORCH_TO_CUTE_DTYPE[idx_rope_cache.dtype]
73 - idx_weights_type = TORCH_TO_CUTE_DTYPE[idx_weights.dtype]
74 - else:
75 - idx_dim = num_idx_heads = 0
76 - idx_q = idx_rope_cache = idx_q_fp8 = None
77 - idx_weights = idx_weights_out = None
78 - idx_rope_type = idx_weights_type = None
79 -
80 - rope_type = TORCH_TO_CUTE_DTYPE[rope_cache.dtype]
81 - compiled = FusedQKernel.compile(
82 - rope_dim,
83 - nope_dim,
84 - num_heads,
85 - rope_type,
86 - idx_dim,
87 - num_idx_heads,
88 - idx_rope_type,
89 - idx_weights_type,
90 - index_rope_interleave,
91 - )
92 - compiled(
93 - positions,
94 - q_pe,
95 - rope_cache,
96 - ql_nope,
97 - q_scale.view(1),
98 - mqa_output,
99 - idx_q,
100 - idx_rope_cache,
101 - idx_weights,
102 - idx_q_fp8,
103 - idx_weights_out,
104 - float(idx_weights_softmax_scale * idx_weights_head_scale),
105 - )
106 -
107 -
108 -class FusedQKernel:
109 - def __init__(
110 - self,
111 - rope_dim: int,
112 - nope_dim: int,
113 - num_heads: int,
114 - idx_dim: int,
115 - num_idx_heads: int,
116 - index_rope_interleave: bool,
117 - ) -> None:
118 - assert rope_dim == 64
119 - assert nope_dim == 512
120 - assert idx_dim in (128, 0)
121 -
122 - self.rope_dim = rope_dim
123 - self.nope_dim = nope_dim
124 - self.num_heads = num_heads
125 - self.idx_dim = idx_dim
126 - self.num_idx_heads = num_idx_heads
127 - self.index_rope_interleave = index_rope_interleave
128 -
129 - # mqa: rope_dim=64, nope_dim=512, num_heads=64/TP
130 - # indexer: rope_dim=64, nope_dim=64, num_heads=32
131 -
132 - self.num_warps = 4
133 - assert num_heads % self.num_warps == 0
134 - assert num_idx_heads % (4 * self.num_warps) == 0
135 - self.num_ctas_per_tok = num_heads // self.num_warps
136 - self.num_ctas_per_idx_tok = num_idx_heads // (4 * self.num_warps)
137 -
138 - @cute.jit
139 - def __call__(
140 - self,
141 - positions: cute.Tensor,
142 - q_pe: cute.Tensor,
143 - rope_cache: cute.Tensor,
144 - ql_nope: cute.Tensor,
145 - q_scale: cute.Tensor,
146 - mqa_output: cute.Tensor,
147 - idx_q: cute.Tensor,
148 - idx_rope_cache: cute.Tensor,
149 - idx_weights: cute.Tensor,
150 - idx_q_fp8: cute.Tensor,
151 - idx_weights_out: cute.Tensor,
152 - weight_scale: Float32,
153 - stream: CUstream,
154 - ):
155 - num_tokens = positions.shape[0]
156 - if cutlass.const_expr(self.idx_dim == 0):
157 - grid = (num_tokens, self.num_ctas_per_tok, 1)
158 - else:
159 - num_mqa_ctas = num_tokens * self.num_ctas_per_tok
160 - num_idx_ctas = num_tokens * self.num_ctas_per_idx_tok
161 - grid = (num_mqa_ctas + num_idx_ctas, 1, 1)
162 -
163 - self.kernel(
164 - positions,
165 - q_pe,
166 - rope_cache,
167 - ql_nope,
168 - q_scale,
169 - mqa_output,
170 - idx_q,
171 - idx_rope_cache,
172 - idx_weights,
173 - idx_q_fp8,
174 - idx_weights_out,
175 - weight_scale,
176 - ).launch(
177 - grid=grid,
178 - block=(self.num_warps * 32, 1, 1),
179 - stream=stream,
180 - use_pdl=True,
181 - )
182 -
183 - @cute.kernel
184 - def kernel(
185 - self,
186 - positions: cute.Tensor,
187 - q_pe: cute.Tensor,
188 - q_pe_rope_cache: cute.Tensor,
189 - ql_nope: cute.Tensor,
190 - q_scale: cute.Tensor,
191 - mqa_output: cute.Tensor,
192 - idx_q: cute.Tensor,
193 - idx_q_rope_cache: cute.Tensor,
194 - idx_weights: cute.Tensor,
195 - idx_q_fp8: cute.Tensor,
196 - idx_weights_out: cute.Tensor,
197 - weight_scale: Float32,
198 - ):
199 - if cutlass.const_expr(self.idx_dim == 0):
200 - token_id, group_id, _ = cute.arch.block_idx()
201 - self.mqa(
202 - positions,
203 - q_pe,
204 - q_pe_rope_cache,
205 - ql_nope,
206 - q_scale,
207 - mqa_output,
208 - token_id,
209 - group_id,
210 - )
211 - else:
212 - # CTA-specialization
213 - bid, _, _ = cute.arch.block_idx()
214 - num_mqa_ctas = positions.shape[0] * self.num_ctas_per_tok
215 - if bid < num_mqa_ctas:
216 - self.mqa(
217 - positions,
218 - q_pe,
219 - q_pe_rope_cache,
220 - ql_nope,
221 - q_scale,
222 - mqa_output,
223 - bid // self.num_ctas_per_tok,
224 - bid % self.num_ctas_per_tok,
225 - )
226 - else:
227 - bid -= num_mqa_ctas
228 - self.indexer(
229 - positions,
230 - idx_q,
231 - idx_q_rope_cache,
232 - idx_weights,
233 - idx_q_fp8,
234 - idx_weights_out,
235 - weight_scale,
236 - bid // self.num_ctas_per_idx_tok,
237 - bid % self.num_ctas_per_idx_tok,
238 - )
239 -
240 - @cute.jit
241 - def mqa(
242 - self,
243 - positions: cute.Tensor,
244 - q_pe: cute.Tensor,
245 - q_pe_rope_cache: cute.Tensor,
246 - ql_nope: cute.Tensor,
247 - q_scale: cute.Tensor,
248 - mqa_output: cute.Tensor,
249 - token_id,
250 - group_id,
251 - ):
252 - tid, _, _ = cute.arch.thread_idx()
253 - warp_id = cute.arch.make_warp_uniform(tid // 32)
254 - lane_id = tid % 32
255 - head_id = group_id * self.num_warps + warp_id
256 -
257 - cute.arch.griddepcontrol_wait()
258 -
259 - pos = positions[token_id]
260 - inv_scale = 1.0 / q_scale[0]
261 -
262 - cp_op = cute.nvgpu.CopyUniversalOp()
263 - cp_32B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=256)
264 - cp_16B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=128)
265 - cp_4B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=32)
266 - cp_2B = cute.make_copy_atom(cp_op, Uint8, num_bits_per_copy=16)
267 -
268 - ##### issue all loads asap #####
269 - rQ_nope_bf16 = cute.make_rmem_tensor(16, BFloat16)
270 - rQ_rope_bf16 = cute.make_rmem_tensor(2, BFloat16)
271 -
272 - src_ql_nope = cute.local_tile(
273 - ql_nope[token_id, head_id, None], (16,), (lane_id,)
274 - )
275 - src_q_rope = cute.local_tile(q_pe[token_id, head_id, None], (2,), (lane_id,))
276 - cute.copy(cp_32B, src_ql_nope, rQ_nope_bf16)
277 - cute.copy(cp_4B, src_q_rope, rQ_rope_bf16)
278 -
279 - rCos_raw = q_pe_rope_cache[pos, 0 + lane_id]
280 - rSin_raw = q_pe_rope_cache[pos, 32 + lane_id]
281 -
282 - ##### process NoPE #####
283 - rQ_nope_f32 = cvt.bf16x2_to_fp32x2(rQ_nope_bf16).load() * inv_scale
284 - rQ_nope_f8 = cute.make_rmem_tensor(16, Float8E4M3FN)
285 - rQ_nope_f8.store(rQ_nope_f32.to(Float8E4M3FN))
286 - dst_Q_nope = cute.local_tile(
287 - mqa_output[token_id, head_id, None], (16,), (lane_id,)
288 - )
289 - cute.copy(cp_16B, rQ_nope_f8, dst_Q_nope)
290 -
291 - ##### process RoPE ######
292 - rQ_rope_f32 = cvt.bf16x2_to_fp32x2(rQ_rope_bf16)
293 - rCos = rCos_raw.to(Float32)
294 - rSin = rSin_raw.to(Float32)
295 - r0 = (rQ_rope_f32[0] * rCos - rQ_rope_f32[1] * rSin) * inv_scale
296 - r1 = (rQ_rope_f32[1] * rCos + rQ_rope_f32[0] * rSin) * inv_scale
297 -
298 - cute.arch.griddepcontrol_launch_dependents()
299 -
300 - # TensorSSA fp32->fp8 cvt has a bug. rely on direct PTX
301 - rQ_rope_f8 = cute.make_rmem_tensor(2, Float8E4M3FN)
302 - cute.recast_tensor(rQ_rope_f8, Uint16)[0] = cvt.fp32x2_to_fp8x2(r0, r1)
303 - dst_Q_rope = cute.local_tile(
304 - mqa_output[token_id, head_id, None], (2,), (256 + lane_id,)
305 - )
306 - cute.copy(cp_2B, rQ_rope_f8, dst_Q_rope)
307 -
308 - @cute.jit
309 - def indexer(
310 - self,
311 - positions: cute.Tensor,
312 - idx_q: cute.Tensor,
313 - idx_q_rope_cache: cute.Tensor,
314 - idx_weights: cute.Tensor,
315 - idx_q_fp8: cute.Tensor,
316 - idx_weights_out: cute.Tensor,
317 - weight_scale: Float32,
318 - token_id,
319 - group_id,
320 - ):
321 - tid, _, _ = cute.arch.thread_idx()
322 - subwarp_id = tid // 8
323 - sublane_id = tid % 8
324 - head_id = group_id * (4 * self.num_warps) + subwarp_id
325 - cute.arch.griddepcontrol_wait()
326 -
327 - pos = positions[token_id]
328 -
329 - cp_op = cute.nvgpu.CopyUniversalOp()
330 - cp_16B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=128)
331 - cp_8B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=64)
332 - cp_4B = cute.make_copy_atom(cp_op, Uint32, num_bits_per_copy=32)
333 -
334 - ##### issue all loads first #####
335 - rQ_rope_bf16 = cute.make_rmem_tensor(8, BFloat16)
336 - if cutlass.const_expr(self.index_rope_interleave):
337 - src_idx_q_rope = cute.local_tile(
338 - idx_q[token_id, head_id, None], (8,), (sublane_id,)
339 - )
340 - cute.copy(cp_16B, src_idx_q_rope, rQ_rope_bf16)
341 - else:
342 - src_idx_q_rope = cute.zipped_divide(
343 - idx_q[token_id, head_id, None], (4,)
344 - ) # (4,32)
345 - cute.copy(
346 - cp_8B,
347 - src_idx_q_rope[None, 0 + sublane_id],
348 - cute.local_tile(rQ_rope_bf16, (4,), (0,)),
349 - )
350 - cute.copy(
351 - cp_8B,
352 - src_idx_q_rope[None, 8 + sublane_id],
353 - cute.local_tile(rQ_rope_bf16, (4,), (1,)),
354 - )
355 -
356 - rQ_nope_bf16 = cute.make_rmem_tensor(8, BFloat16)
357 - src_idx_q_nope = cute.local_tile(
358 - idx_q[token_id, head_id, None], (8,), (8 + sublane_id,)
359 - )
360 - cute.copy(cp_16B, src_idx_q_nope, rQ_nope_bf16)
361 -
362 - rCos_raw = cute.make_rmem_tensor(4, idx_q_rope_cache.element_type)
363 - rSin_raw = cute.make_rmem_tensor(4, idx_q_rope_cache.element_type)
364 - rope_cache_view = cute.zipped_divide(idx_q_rope_cache[pos, None], (4,))
365 -
366 - if cutlass.const_expr(idx_q_rope_cache.element_type == Float32):
367 - cute.copy(cp_16B, rope_cache_view[None, sublane_id], rCos_raw)
368 - cute.copy(cp_16B, rope_cache_view[None, 8 + sublane_id], rSin_raw)
369 - elif cutlass.const_expr(idx_q_rope_cache.element_type == BFloat16):
370 - cute.copy(cp_8B, rope_cache_view[None, sublane_id], rCos_raw)
371 - cute.copy(cp_8B, rope_cache_view[None, 8 + sublane_id], rSin_raw)
372 -
373 - # unpack to FP32
374 - rQ_rope_f32 = cvt.bf16x2_to_fp32x2(rQ_rope_bf16)
375 - rQ_nope_f32 = cvt.bf16x2_to_fp32x2(rQ_nope_bf16)
376 - if cutlass.const_expr(idx_q_rope_cache.element_type == Float32):
377 - rCos = rCos_raw
378 - rSin = rSin_raw
379 - elif cutlass.const_expr(idx_q_rope_cache.element_type == BFloat16):
380 - rCos = cvt.bf16x2_to_fp32x2(rCos_raw)
381 - rSin = cvt.bf16x2_to_fp32x2(rSin_raw)
382 -
383 - # apply rope
384 - for i in cutlass.range_constexpr(4):
385 - if cutlass.const_expr(self.index_rope_interleave):
386 - r0 = rQ_rope_f32[i * 2 + 0] * rCos[i] - rQ_rope_f32[i * 2 + 1] * rSin[i]
387 - r1 = rQ_rope_f32[i * 2 + 1] * rCos[i] + rQ_rope_f32[i * 2 + 0] * rSin[i]
388 - rQ_rope_f32[i * 2 + 0] = r0
389 - rQ_rope_f32[i * 2 + 1] = r1
390 - else:
391 - r0 = rQ_rope_f32[0 + i] * rCos[i] - rQ_rope_f32[4 + i] * rSin[i]
392 - r1 = rQ_rope_f32[4 + i] * rCos[i] + rQ_rope_f32[0 + i] * rSin[i]
393 - rQ_rope_f32[0 + i] = r0
394 - rQ_rope_f32[4 + i] = r1
395 -
396 - # amax
397 - amax = Float32(1e-4)
398 - for i in cutlass.range_constexpr(8):
399 - amax = cute.arch.fmax(amax, cute.math.absf(rQ_rope_f32[i]))
400 - amax = cute.arch.fmax(amax, cute.math.absf(rQ_nope_f32[i]))
401 -
402 - # warp reduction among 8 lanes
403 - for i in cutlass.range_constexpr(3):
404 - other = cute.arch.shuffle_sync_bfly(amax, 1 << i)
405 - amax = cute.arch.fmax(amax, other)
406 -
407 - # compute scale from amax
408 - # exp2(ceil(log2(scale))) via bit manipulation
409 - scale = amax * (1.0 / 448.0)
410 - bits = scale.bitcast(Uint32)
411 - exp_bits = (bits + Uint32(0x007FFFFF)) & Uint32(0x7F800000)
412 - scale = exp_bits.bitcast(Float32)
413 - inv_scale = (Uint32(0x7F000000) - exp_bits).bitcast(Float32)
414 -
415 - for i in cutlass.range_constexpr(8):
416 - rQ_nope_f32[i] *= inv_scale
417 - rQ_rope_f32[i] *= inv_scale
418 -
419 - cute.arch.griddepcontrol_launch_dependents()
420 -
421 - # quantize and store
422 - rQ_nope_f8 = cute.make_rmem_tensor(8, Float8E4M3FN)
423 - rQ_nope_f8.store(rQ_nope_f32.load().to(Float8E4M3FN))
424 - dst_idx_q_nope = cute.local_tile(
425 - idx_q_fp8[token_id, head_id, None], (8,), (8 + sublane_id,)
426 - )
427 - cute.copy(cp_8B, rQ_nope_f8, dst_idx_q_nope)
428 -
429 - rQ_rope_f8 = cute.make_rmem_tensor(8, Float8E4M3FN)
430 - rQ_rope_f8.store(rQ_rope_f32.load().to(Float8E4M3FN))
431 - if cutlass.const_expr(self.index_rope_interleave):
432 - dst_idx_q_rope = cute.local_tile(
433 - idx_q_fp8[token_id, head_id, None], (8,), (sublane_id,)
434 - )
435 - cute.copy(cp_8B, rQ_rope_f8, dst_idx_q_rope)
436 - else:
437 - dst_idx_q_rope = cute.zipped_divide(
438 - idx_q_fp8[token_id, head_id, None], (4,)
439 - ) # (4,32)
440 - cute.copy(
441 - cp_4B,
442 - cute.local_tile(rQ_rope_f8, (4,), (0,)),
443 - dst_idx_q_rope[None, 0 + sublane_id],
444 - )
445 - cute.copy(
446 - cp_4B,
447 - cute.local_tile(rQ_rope_f8, (4,), (1,)),
448 - dst_idx_q_rope[None, 8 + sublane_id],
449 - )
450 -
451 - # scale indexer weights
452 - if sublane_id == 0:
453 - w = idx_weights[token_id, head_id].to(Float32)
454 - idx_weights_out[token_id, head_id] = w * scale * weight_scale
455 -
456 - @cache
457 - @staticmethod
458 - def compile(
459 - rope_dim: int,
460 - nope_dim: int,
461 - num_heads: int,
462 - rope_type: type[cutlass.Numeric],
463 - idx_dim: int,
464 - num_idx_heads: int,
465 - idx_rope_type: type[cutlass.Numeric] | None,
466 - idx_weights_type: type[cutlass.Numeric] | None,
467 - index_rope_interleave: bool,
468 - ):
469 - num_tokens = cute.sym_int()
470 - max_pos = cute.sym_int()
471 -
472 - positions = _make_fake_tensor(Int64, (num_tokens,), divisibility=1)
473 - q_pe = _make_fake_tensor(
474 - BFloat16, (num_tokens, num_heads, rope_dim), divisibility=16
475 - )
476 - rope_cache = _make_fake_tensor(rope_type, (max_pos, rope_dim), divisibility=8)
477 - ql_nope = _make_fake_tensor(
478 - BFloat16, (num_tokens, num_heads, nope_dim), divisibility=16
479 - )
480 - q_scale = _make_fake_tensor(Float32, (1,), divisibility=4)
481 - mqa_output = _make_fake_tensor(
482 - Float8E4M3FN,
483 - (num_tokens, num_heads, nope_dim + rope_dim),
484 - divisibility=16,
485 - )
486 -
487 - if idx_rope_type is not None:
488 - index_q = _make_fake_tensor(
489 - BFloat16, (num_tokens, num_idx_heads, idx_dim), divisibility=16
490 - )
491 - index_rope_cache = _make_fake_tensor(
492 - idx_rope_type, (max_pos, rope_dim), divisibility=8
493 - )
494 - index_weights = _make_fake_tensor(
495 - idx_weights_type, (num_tokens, num_idx_heads), divisibility=8
496 - )
497 - index_q_fp8 = _make_fake_tensor(
498 - Float8E4M3FN, (num_tokens, num_idx_heads, idx_dim), divisibility=16
499 - )
500 - index_weights_out = _make_fake_tensor(
501 - Float32, (num_tokens, num_idx_heads), divisibility=4
502 - )
503 - else:
504 - index_q = index_rope_cache = index_q_fp8 = None
505 - index_weights = index_weights_out = None
506 -
507 - kernel = FusedQKernel(
508 - rope_dim,
509 - nope_dim,
510 - num_heads,
511 - idx_dim,
512 - num_idx_heads,
513 - index_rope_interleave,
514 - )
515 - stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
516 - return cute.compile(
517 - kernel,
518 - positions,
519 - q_pe,
520 - rope_cache,
521 - ql_nope,
522 - q_scale,
523 - mqa_output,
524 - index_q,
525 - index_rope_cache,
526 - index_weights,
527 - index_q_fp8,
528 - index_weights_out,
529 - Float32(0.0),
530 - stream,
531 - options="--enable-tvm-ffi",
532 - )
vllm/v1/attention/backends/mla/flashinfer_mla_sparse.py
-31
@@ -28,7 +28,6 @@ from vllm.v1.attention.backend import (
28 MultipleOf,
29 )
30 from vllm.v1.attention.backends.mla.sparse_utils import (
31 - phys_shadow,
31 triton_convert_req_index_to_global_index,
32 triton_filter_and_convert_dcp_index,
33 )
@@ -405,36 +404,6 @@ class FlashInferMLASparseImpl(SparseMLACommonImpl[FlashInferMLASparseMetadata]):
404 NUM_TOPK_TOKENS=topk_indices.shape[1],
405 return_valid_counts=True,
406 )
408 - elif (shadow := phys_shadow(self.topk_indices_buffer)) is not None:
409 - # skip_topk layers reuse the previous fresh layer's PHYSICAL
410 - # indices: the logical top-k indices are shared (one buffer for
411 - # the whole forward) and block_table/req_id are per-step constant,
412 - # so re-converting them per layer produces identical output. This
413 - # covers both the backbone (index_topk_freq > 1) and MTP draft
414 - # steps 1+ (index_share_for_mtp_iteration sets skip_topk; the
415 - # draft compact re-arranges the shadow together with the logical
416 - # buffer). The fresh/skip split is fixed per captured graph, so
417 - # the branch is cudagraph-safe.
418 - phys_buf, seq_buf = shadow
419 - wrote_fresh = getattr(layer, "indexer", None) is not None and not getattr(
420 - layer, "skip_topk", False
421 - )
422 - if wrote_fresh:
423 - topk_indices_physical, seq_lens = (
424 - triton_convert_req_index_to_global_index(
425 - attn_metadata.req_id_per_token[:num_actual_toks],
426 - attn_metadata.block_table,
427 - topk_indices,
428 - BLOCK_SIZE=attn_metadata.block_size,
429 - NUM_TOPK_TOKENS=topk_indices.shape[1],
430 - return_valid_counts=True,
431 - out=phys_buf[:num_actual_toks],
432 - valid_counts_out=seq_buf[:num_actual_toks],
433 - )
434 - )
435 - else:
436 - topk_indices_physical = phys_buf[:num_actual_toks]
437 - seq_lens = seq_buf[:num_actual_toks]
407 else:
408 topk_indices_physical, seq_lens = triton_convert_req_index_to_global_index(
409 attn_metadata.req_id_per_token[:num_actual_toks],
vllm/v1/attention/backends/mla/sparse_utils.py
+4 -56
@@ -128,8 +128,6 @@ def triton_convert_req_index_to_global_index(
128 prefill_workspace_request_ids: torch.Tensor | None = None,
129 prefill_workspace_starts: torch.Tensor | None = None,
130 return_valid_counts: bool = False,
131 - out: torch.Tensor | None = None,
132 - valid_counts_out: torch.Tensor | None = None,
131 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
132 """
133 out[token_id, indice_id] =
@@ -180,20 +178,14 @@ def triton_convert_req_index_to_global_index(
178 req_id_c = req_id.contiguous()
179 block_table_c = block_table.contiguous()
180 token_indices_c = token_indices.contiguous()
183 - # `out`/`valid_counts_out` allow writing into a stable pre-allocated buffer
184 - # (used by the shared-physical-index cache to stay cudagraph-safe).
185 - out = torch.empty_like(token_indices_c) if out is None else out
181 + out = torch.empty_like(token_indices_c)
182
183 # Allocate valid count buffer if needed (must be zero-initialized for atomics)
184 valid_counts: torch.Tensor | None = None
185 if return_valid_counts:
190 - if valid_counts_out is None:
191 - valid_counts = torch.zeros(
192 - num_tokens, dtype=torch.int32, device=token_indices.device
193 - )
194 - else:
195 - valid_counts = valid_counts_out
196 - valid_counts.zero_()
186 + valid_counts = torch.zeros(
187 + num_tokens, dtype=torch.int32, device=token_indices.device
188 + )
189
190 # Strides in elements
191 bt_stride0, bt_stride1 = block_table_c.stride()
@@ -349,47 +341,3 @@ def triton_filter_and_convert_dcp_index(
341 assert valid_counts is not None
342 return out, valid_counts
343 return out
352 -
353 -
354 -# --- Physical-index shadow (DSA index_topk_freq > 1) -------------------------
355 -# GLM-5.2 uses index_topk_freq=4: only ~1/4 of layers write a fresh top-k into
356 -# the single shared topk_indices_buffer; the rest reuse it. Since block_table
357 -# and req_id_per_token are constant within a decode step, the physical indices
358 -# (block_table lookup of the logical top-k) are identical across a freq-group.
359 -# Fresh layers convert once into a STABLE shadow of the logical buffer and
360 -# skip layers read it -- eliminating ~3/4 of the per-layer convert kernels.
361 -#
362 -# Invariant: shadow[j] == convert(logical[j]) row-wise. Whoever re-arranges
363 -# the logical buffer (e.g. the MTP draft compact between the multi-token
364 -# step-0 layout and the single-token steps-1+ layout) must apply the same
365 -# gather to the shadow.
366 -#
367 -# Shadows are registered ONLY at buffer creation time (model init, never
368 -# inside cudagraph capture) via register_phys_shadow; every other access is
369 -# the read-only phys_shadow lookup, so addresses are stable across graph
370 -# replays and no allocation can happen during capture.
371 -import weakref as _weakref # noqa: E402
372 -
373 -_PHYS_SHADOWS: dict[int, tuple[torch.Tensor, torch.Tensor]] = {}
374 -
375 -
376 -def register_phys_shadow(topk_buf: torch.Tensor) -> None:
377 - """Allocate the (physical-index, valid-count) shadow for a logical top-k
378 - buffer. Call once where the buffer is created. The finalizer drops the
379 - entry with the owning buffer, so a recycled id can never alias a stale
380 - shadow."""
381 - key = id(topk_buf)
382 - if key not in _PHYS_SHADOWS:
383 - _PHYS_SHADOWS[key] = (
384 - torch.empty_like(topk_buf),
385 - torch.empty(topk_buf.shape[0], dtype=torch.int32, device=topk_buf.device),
386 - )
387 - _weakref.finalize(topk_buf, _PHYS_SHADOWS.pop, key, None)
388 -
389 -
390 -def phys_shadow(
391 - topk_buf: torch.Tensor,
392 -) -> tuple[torch.Tensor, torch.Tensor] | None:
393 - """The registered shadow for this buffer, or None if it was never
394 - registered. Never allocates; capture-safe."""
395 - return _PHYS_SHADOWS.get(id(topk_buf))
vllm/v1/spec_decode/llm_base_proposer.py
+2 -3
@@ -1009,9 +1009,8 @@ class SpecDecodeBaseProposer:
1009 # DeepSeek-family MTP (deepseek_mtp.py) recycles the post-final-
1010 # norm hidden, so its forward returns (logit_hidden,
1011 # recycle_hidden). Other MTP families return a single tensor.
1012 - return bool(
1013 - {"DeepSeekMTPModel", "DeepseekV32MTPModel"}
1014 - & set(self.draft_model_config.hf_config.architectures or [])
1012 + return "DeepSeekMTPModel" in (
1013 + self.draft_model_config.hf_config.architectures or []
1014 )
1015 return self.method not in ("mtp", "draft_model", "dflash")
1016
vllm/v1/worker/gpu/spec_decode/autoregressive/speculator.py
+5 -35
@@ -48,18 +48,6 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
48 self.prefill_cudagraph_manager: SpeculatorCudaGraphManager | None = None
49 self.decode_cudagraph_manager: SpeculatorCudaGraphManager | None = None
50
51 - # Lifecycle hooks for model-specific optimizations. Subclasses override
52 - # the ones they need. These fire in both `capture` and `propose` so that
53 - # any state they toggle (e.g. attention flags baked into a CUDA graph) is
54 - # identical at capture time and replay time.
55 - def on_prefill_begin(self, num_reqs: int, num_tokens: int) -> None: ...
56 -
57 - def on_prefill_end(self, num_reqs: int, num_tokens: int) -> None: ...
58 -
59 - def on_multi_step_decode_begin(self, num_reqs: int) -> None: ...
60 -
61 - def on_multi_step_decode_end(self, num_reqs: int) -> None: ...
62 -
51 @property
52 def advance_draft_positions(self) -> bool:
53 """
@@ -110,9 +98,6 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
98 assert self.prefill_cudagraph_manager is not None
99 if self.prefill_cudagraph_manager.use_breakable_cg:
100 self.prefill_cudagraph_manager.init_breakable_cg_runner(self.model)
113 -
114 - self.on_prefill_begin(self.max_num_reqs, self.max_num_tokens)
115 -
101 self.prefill_cudagraph_manager.capture(
102 self._prefill,
103 self.model_state,
@@ -123,13 +108,9 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
108 progress_bar_desc="Capturing prefill CUDA graphs",
109 )
110
126 - self.on_prefill_end(self.max_num_reqs, self.max_num_tokens)
127 -
111 if self.num_speculative_steps == 1:
112 return
113
131 - self.on_multi_step_decode_begin(self.max_num_reqs)
132 -
114 # Capture the decode draft generation routine (model forward +
115 # sample + update_draft_inputs) for a single
116 # step.
@@ -144,8 +125,6 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
125 progress_bar_desc="Capturing decode CUDA graphs",
126 )
127
147 - self.on_multi_step_decode_end(self.max_num_reqs)
148 -
128 @torch.inference_mode()
129 def propose(
130 self,
@@ -174,8 +153,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
153 mm_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
154 is_profile: bool = False,
155 ) -> torch.Tensor:
177 - num_tokens = input_batch.num_tokens
178 - num_tokens_padded = input_batch.num_tokens_after_padding
156 + num_tokens = input_batch.num_tokens_after_padding
157 num_reqs = input_batch.num_reqs
158 max_query_len = input_batch.num_scheduled_tokens.max()
159 max_seq_len = input_batch.seq_lens_cpu_upper_bound[:num_reqs].max().item()
@@ -196,7 +174,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
174 )
175 else:
176 hidden_states = last_hidden_states
199 - self.hidden_states[:num_tokens_padded].copy_(hidden_states)
177 + self.hidden_states[:num_tokens].copy_(hidden_states)
178
179 self._copy_request_inputs(
180 num_reqs,
@@ -224,22 +202,20 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
202 num_reqs,
203 # Use the actual number of tokens without padding added by
204 # the target model during FULL cudagraph.
227 - num_tokens,
205 + input_batch.num_tokens,
206 max_query_len,
207 )
208 prefill_batch_desc, num_tokens_across_dp = dispatch_cg_and_sync_dp(
209 self.prefill_cudagraph_manager,
210 num_reqs,
233 - num_tokens_padded,
211 + num_tokens,
212 uniform_token_count,
213 dp_size=self.dp_size,
214 dp_rank=self.dp_rank,
215 need_eager=is_profile,
216 )
217
240 - self._prepare_eplb_forward(num_tokens)
241 -
242 - self.on_prefill_begin(num_reqs, num_tokens)
218 + self._prepare_eplb_forward(input_batch.num_tokens)
219
220 if prefill_batch_desc.cg_mode == CUDAGraphMode.FULL:
221 # Replay the full graph for draft prefill.
@@ -259,8 +235,6 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
235 mm_inputs=mm_inputs,
236 )
237
262 - self.on_prefill_end(num_reqs, num_tokens)
263 -
238 if self.num_speculative_steps == 1:
239 # Early exit.
240 return self.draft_tokens[:num_reqs, :1]
@@ -288,8 +262,6 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
262 need_eager=is_profile,
263 )
264
291 - self.on_multi_step_decode_begin(num_reqs)
292 -
265 # Generate the remaining num_speculative_steps - 1 draft tokens.
266 self._multi_step_decode(
267 num_reqs,
@@ -299,8 +271,6 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
271 input_batch.seq_lens_cpu_upper_bound,
272 )
273
302 - self.on_multi_step_decode_end(num_reqs)
303 -
274 return self.draft_tokens[:num_reqs]
275
276 @torch.inference_mode()
vllm/v1/worker/gpu/spec_decode/mtp/speculator.py
+1 -40
@@ -1,10 +1,8 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -import torch
4 import torch.nn as nn
5
7 -from vllm.config import VllmConfig
6 from vllm.v1.worker.gpu.spec_decode.autoregressive.speculator import (
7 AutoRegressiveSpeculator,
8 )
@@ -12,46 +10,9 @@ from vllm.v1.worker.gpu.spec_decode.eagle.utils import load_eagle_model
10
11
12 class MTPSpeculator(AutoRegressiveSpeculator):
15 - def __init__(self, vllm_config: VllmConfig, device: torch.device):
16 - super().__init__(vllm_config, device)
17 -
18 - spec_config = vllm_config.speculative_config
19 - draft_hf_config = (
20 - spec_config.draft_model_config.hf_config
21 - if spec_config is not None
22 - else None
23 - )
24 - # Detect index_share_for_mtp_iteration. When True, the proposer
25 - # toggles skip_topk so step 0 computes MTP's own indices and
26 - # steps 1+ reuse them.
27 - self.share_mtp_topk_indices = getattr(
28 - draft_hf_config, "index_share_for_mtp_iteration", False
29 - )
30 -
13 def load_draft_model(
14 self,
15 target_model: nn.Module,
16 target_attn_layer_names: set[str],
17 ) -> nn.Module:
36 - draft_model = load_eagle_model(target_model, self.vllm_config)
37 - self.share_mtp_topk_indices = self.share_mtp_topk_indices and hasattr(
38 - draft_model.model, "set_skip_topk"
39 - )
40 - return draft_model
41 -
42 - def on_prefill_end(self, num_reqs: int, num_tokens: int) -> None:
43 - # Step 0 (prefill) wrote topk indices for every query token in the
44 - # multi-token batch. Compact them down to each request's last token so
45 - # steps 1+ can reuse them from the shared buffer.
46 - if self.share_mtp_topk_indices and self.num_speculative_steps > 1:
47 - self.model.model.compact_topk_indices(self.last_token_indices[:num_reqs])
48 -
49 - def on_multi_step_decode_begin(self, num_reqs: int) -> None:
50 - # Switch to reuse mode so draft steps 1+ skip the indexer op and read
51 - # the indices that step 0 wrote into the shared buffer.
52 - if self.share_mtp_topk_indices:
53 - self.model.model.set_skip_topk(True)
54 -
55 - def on_multi_step_decode_end(self, num_reqs: int) -> None:
56 - if self.share_mtp_topk_indices:
57 - self.model.model.set_skip_topk(False)
18 + return load_eagle_model(target_model, self.vllm_config)