[Kernel][Perf] Add fused CUDA post-conv MTP decode kernel for Qwen3.5 GDN (#51674)
Signed-off-by: Jie Fang <jief@nvidia.com> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Jie Fang committed
Aug 14, 2026 at 11:44 UTC
1be36283678a9a94fc8fdaad6c95c2896d6b4015
12 files changed
+1456
-7
CMakeLists.txt
+18
@@ -1118,6 +1118,8 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
1118
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
1119
cuda_archs_loose_intersection(FUSED_KDA_DECODE_ARCHS
1120
"9.0a;10.0f;12.0f" "${CUDA_ARCHS}")
1121
+ cuda_archs_loose_intersection(FUSED_GDN_DECODE_ARCHS
1122
+ "8.0;8.6;8.9;9.0a;10.0f;12.0f" "${CUDA_ARCHS}")
1123
endif()
1124
if(FUSED_KDA_DECODE_ARCHS)
1125
set(FUSED_KDA_DECODE_SRC
@@ -1131,6 +1133,18 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
1133
message(STATUS
1134
"Building fused KDA decode for archs: ${FUSED_KDA_DECODE_ARCHS}")
1135
endif()
1136
+ if(FUSED_GDN_DECODE_ARCHS)
1137
+ set(FUSED_GDN_DECODE_SRC
1138
+ "csrc/libtorch_stable/gdn/fused_gdn_decode_kernel.cu")
1139
+ set_gencode_flags_for_srcs(
1140
+ SRCS "${FUSED_GDN_DECODE_SRC}"
1141
+ CUDA_ARCHS "${FUSED_GDN_DECODE_ARCHS}")
1142
+ set_property(SOURCE ${FUSED_GDN_DECODE_SRC} APPEND PROPERTY
1143
+ COMPILE_OPTIONS "$<$<COMPILE_LANGUAGE:CUDA>:--use_fast_math>")
1144
+ list(APPEND VLLM_STABLE_EXT_SRC "${FUSED_GDN_DECODE_SRC}")
1145
+ message(STATUS
1146
+ "Building fused GDN decode for archs: ${FUSED_GDN_DECODE_ARCHS}")
1147
+ endif()
1148
1149
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
1150
cuda_archs_loose_intersection(KIMI_K3_ATTN_RES_ARCHS
@@ -1213,6 +1227,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
1227
target_compile_definitions(_C_stable_libtorch PRIVATE
1228
VLLM_ENABLE_FUSED_KDA_DECODE=1)
1229
endif()
1230
+ if(FUSED_GDN_DECODE_ARCHS)
1231
+ target_compile_definitions(_C_stable_libtorch PRIVATE
1232
+ VLLM_ENABLE_FUSED_GDN_DECODE=1)
1233
+ endif()
1234
if(KIMI_K3_ATTN_RES_ARCHS)
1235
target_compile_definitions(_C_stable_libtorch PRIVATE
1236
VLLM_ENABLE_KIMI_K3_ATTN_RES=1)
csrc/libtorch_stable/gdn/fused_gdn_decode_kernel.cu
new
+557
@@ -0,0 +1,557 @@
1
+/*
2
+ * SPDX-License-Identifier: Apache-2.0
3
+ * SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4
+ */
5
+
6
+#include <cstdint>
7
+#include <cuda_bf16.h>
8
+#include <cuda_fp16.h>
9
+#include <cuda_runtime.h>
10
+
11
+#include "../torch_utils.h"
12
+#include "../../cuda_compat.h"
13
+
14
+namespace {
15
+
16
+template <typename StateT>
17
+__device__ __forceinline__ void cp_async_16b(StateT* smem_ptr,
18
+ const StateT* gmem_ptr) {
19
+ const uint32_t smem_addr =
20
+ static_cast<uint32_t>(__cvta_generic_to_shared(smem_ptr));
21
+ asm volatile("cp.async.cg.shared.global [%0], [%1], 16;\n"
22
+ :
23
+ : "r"(smem_addr), "l"(gmem_ptr));
24
+}
25
+
26
+__device__ __forceinline__ void cp_async_commit() {
27
+ asm volatile("cp.async.commit_group;\n" ::);
28
+}
29
+
30
+__device__ __forceinline__ void cp_async_wait_all() {
31
+ asm volatile("cp.async.wait_all;\n" ::: "memory");
32
+}
33
+
34
+template <typename StateT, int ChunkV, int DimK, int Stages>
35
+__device__ __forceinline__ void copy_state_chunk(StateT* shared_state,
36
+ const StateT* state, int chunk,
37
+ int thread, int threads) {
38
+ constexpr int kElementsPerCopy = 16 / sizeof(StateT);
39
+ constexpr int kCopiesPerChunk = ChunkV * DimK / kElementsPerCopy;
40
+ const int stage = chunk % Stages;
41
+ for (int copy = thread; copy < kCopiesPerChunk; copy += threads) {
42
+ const int element = copy * kElementsPerCopy;
43
+ cp_async_16b(shared_state + stage * ChunkV * DimK + element,
44
+ state + chunk * ChunkV * DimK + element);
45
+ }
46
+ cp_async_commit();
47
+}
48
+
49
+template <typename StateT>
50
+__device__ __forceinline__ float4 load_state4(const StateT* state);
51
+
52
+template <>
53
+__device__ __forceinline__ float4 load_state4<float>(const float* state) {
54
+ return *reinterpret_cast<const float4*>(state);
55
+}
56
+
57
+template <>
58
+__device__ __forceinline__ float4
59
+load_state4<__nv_bfloat16>(const __nv_bfloat16* state) {
60
+ const __nv_bfloat162 lo = *reinterpret_cast<const __nv_bfloat162*>(state);
61
+ const __nv_bfloat162 hi = *reinterpret_cast<const __nv_bfloat162*>(state + 2);
62
+ return make_float4(__bfloat162float(lo.x), __bfloat162float(lo.y),
63
+ __bfloat162float(hi.x), __bfloat162float(hi.y));
64
+}
65
+
66
+template <typename StateT>
67
+__device__ __forceinline__ void store_state4(StateT* state, float4 value);
68
+
69
+template <>
70
+__device__ __forceinline__ void store_state4<float>(float* state,
71
+ float4 value) {
72
+ *reinterpret_cast<float4*>(state) = value;
73
+}
74
+
75
+template <>
76
+__device__ __forceinline__ void store_state4<__nv_bfloat16>(
77
+ __nv_bfloat16* state, float4 value) {
78
+ *reinterpret_cast<__nv_bfloat162*>(state) =
79
+ __floats2bfloat162_rn(value.x, value.y);
80
+ *reinterpret_cast<__nv_bfloat162*>(state + 2) =
81
+ __floats2bfloat162_rn(value.z, value.w);
82
+}
83
+
84
+constexpr int kDimK = 128;
85
+constexpr int kDimV = 128;
86
+constexpr int kThreads = 256;
87
+constexpr int kWarps = kThreads / 32;
88
+constexpr int kChunkV = 32;
89
+constexpr int kNumChunks = kDimV / kChunkV;
90
+constexpr int kRowsPerWarp = kChunkV / kWarps;
91
+constexpr int kMaxMtpTokens = 8;
92
+constexpr int kDtBiasFloat32 = 0;
93
+constexpr int kDtBiasBFloat16 = 1;
94
+constexpr int kDtBiasFloat16 = 2;
95
+
96
+struct GdnDecodeStrides {
97
+ int64_t mixed_row;
98
+ int64_t a_row;
99
+ int64_t b_row;
100
+ int64_t gate_row;
101
+ int64_t state_slot;
102
+};
103
+
104
+__device__ __forceinline__ float sigmoid_fast(float x) {
105
+ return 1.0f / (1.0f + __expf(-x));
106
+}
107
+
108
+__device__ __forceinline__ float silu_fast(float x) {
109
+ return x * sigmoid_fast(x);
110
+}
111
+
112
+__device__ __forceinline__ float softplus_fast(float x) {
113
+ return x > 20.0f ? x : log1pf(__expf(x));
114
+}
115
+
116
+__device__ __forceinline__ float load_dt_bias(const void* dt_bias, int head,
117
+ int dt_bias_type) {
118
+ if (dt_bias_type == kDtBiasBFloat16) {
119
+ return __bfloat162float(static_cast<const __nv_bfloat16*>(dt_bias)[head]);
120
+ }
121
+ if (dt_bias_type == kDtBiasFloat16) {
122
+ return __half2float(static_cast<const __half*>(dt_bias)[head]);
123
+ }
124
+ return static_cast<const float*>(dt_bias)[head];
125
+}
126
+
127
+__device__ __forceinline__ float warp_reduce_sum(float value) {
128
+#pragma unroll
129
+ for (int offset = 16; offset > 0; offset >>= 1) {
130
+ value += __shfl_xor_sync(0xffffffffu, value, offset);
131
+ }
132
+ return value;
133
+}
134
+
135
+struct Sum2 {
136
+ float x;
137
+ float y;
138
+};
139
+
140
+__device__ __forceinline__ Sum2 warp_reduce_sum_pair(float x, float y) {
141
+#pragma unroll
142
+ for (int offset = 16; offset > 0; offset >>= 1) {
143
+ x += __shfl_xor_sync(0xffffffffu, x, offset);
144
+ y += __shfl_xor_sync(0xffffffffu, y, offset);
145
+ }
146
+ return {x, y};
147
+}
148
+
149
+template <typename StateT>
150
+__global__ __launch_bounds__(kThreads, 2) void gdn_decode_post_conv_mtp_kernel(
151
+ const __nv_bfloat16* __restrict__ mixed_qkv,
152
+ const __nv_bfloat16* __restrict__ a, const __nv_bfloat16* __restrict__ b,
153
+ const float* __restrict__ a_log, const void* __restrict__ dt_bias,
154
+ const int* __restrict__ state_indices, const int* __restrict__ cu_seqlens,
155
+ const int* __restrict__ num_accepted_tokens, StateT* __restrict__ state,
156
+ const __nv_bfloat16* __restrict__ output_gate,
157
+ const void* __restrict__ norm_weight, __nv_bfloat16* __restrict__ out,
158
+ int H, int HV, int state_indices_width, int dt_bias_type,
159
+ bool norm_weight_is_bf16, float scale, float norm_eps,
160
+ GdnDecodeStrides strides) {
161
+ const int request = blockIdx.x;
162
+ const int value_head = blockIdx.y;
163
+ const int tid = threadIdx.x;
164
+ const int lane = tid & 31;
165
+ const int warp = tid >> 5;
166
+ const int bos = cu_seqlens[request];
167
+ const int eos = cu_seqlens[request + 1];
168
+ const int num_tokens = eos - bos;
169
+ if (num_tokens <= 0) {
170
+ return;
171
+ }
172
+
173
+ const int accepted = num_accepted_tokens[request];
174
+ const int source_slot =
175
+ accepted > 0 && accepted <= state_indices_width
176
+ ? state_indices[request * state_indices_width + accepted - 1]
177
+ : 0;
178
+ if (source_slot <= 0 || num_tokens > kMaxMtpTokens) {
179
+ for (int linear = tid; linear < num_tokens * kDimV; linear += kThreads) {
180
+ const int token = bos + linear / kDimV;
181
+ const int value = linear % kDimV;
182
+ const int64_t out_offset =
183
+ (static_cast<int64_t>(token) * HV + value_head) * kDimV + value;
184
+ out[out_offset] = __float2bfloat16(0.0f);
185
+ }
186
+ return;
187
+ }
188
+
189
+ const int key_head = value_head / 8;
190
+ __shared__ StateT shared_state[2][kChunkV][kDimK];
191
+ __shared__ float shared_q[kMaxMtpTokens][kDimK];
192
+ __shared__ float shared_k[kMaxMtpTokens][kDimK];
193
+ __shared__ __nv_bfloat16 shared_v[kMaxMtpTokens][kDimV];
194
+ __shared__ __nv_bfloat16 shared_out[kMaxMtpTokens][kDimV];
195
+ __shared__ float shared_decay[kMaxMtpTokens];
196
+ __shared__ float shared_beta[kMaxMtpTokens];
197
+
198
+ StateT* source_state =
199
+ state + static_cast<int64_t>(source_slot) * strides.state_slot +
200
+ value_head * kDimV * kDimK;
201
+ copy_state_chunk<StateT, kChunkV, kDimK, 2>(&shared_state[0][0][0],
202
+ source_state, 0, tid, kThreads);
203
+
204
+ if (warp < num_tokens) {
205
+ const int t = warp;
206
+ const int token = bos + t;
207
+ const int64_t mixed_base = static_cast<int64_t>(token) * strides.mixed_row;
208
+ float q_values[4];
209
+ float k_values[4];
210
+ float q_square = 0.0f;
211
+ float k_square = 0.0f;
212
+#pragma unroll
213
+ for (int i = 0; i < 4; ++i) {
214
+ const int dim = lane + i * 32;
215
+ q_values[i] =
216
+ __bfloat162float(mixed_qkv[mixed_base + key_head * kDimK + dim]);
217
+ k_values[i] = __bfloat162float(
218
+ mixed_qkv[mixed_base + H * kDimK + key_head * kDimK + dim]);
219
+ shared_v[t][dim] =
220
+ mixed_qkv[mixed_base + 2 * H * kDimK + value_head * kDimV + dim];
221
+ q_square += q_values[i] * q_values[i];
222
+ k_square += k_values[i] * k_values[i];
223
+ }
224
+ const Sum2 qk_sums = warp_reduce_sum_pair(q_square, k_square);
225
+ const float q_scale = __shfl_sync(
226
+ 0xffffffffu, lane == 0 ? rsqrtf(qk_sums.x + 1.0e-6f) * scale : 0.0f, 0);
227
+ const float k_scale = __shfl_sync(
228
+ 0xffffffffu, lane == 0 ? rsqrtf(qk_sums.y + 1.0e-6f) : 0.0f, 0);
229
+#pragma unroll
230
+ for (int i = 0; i < 4; ++i) {
231
+ const int dim = lane + i * 32;
232
+ shared_q[t][dim] = q_values[i] * q_scale;
233
+ shared_k[t][dim] = k_values[i] * k_scale;
234
+ }
235
+ if (lane == 0) {
236
+ const float a_value = __bfloat162float(
237
+ a[static_cast<int64_t>(token) * strides.a_row + value_head]);
238
+ const float b_value = __bfloat162float(
239
+ b[static_cast<int64_t>(token) * strides.b_row + value_head]);
240
+ const float g = -__expf(a_log[value_head]) *
241
+ softplus_fast(a_value + load_dt_bias(dt_bias, value_head,
242
+ dt_bias_type));
243
+ shared_decay[t] = __expf(g);
244
+ shared_beta[t] = sigmoid_fast(b_value);
245
+ }
246
+ }
247
+ __syncthreads();
248
+
249
+ const int k_base = lane * 4;
250
+ int rows[kRowsPerWarp];
251
+#pragma unroll
252
+ for (int row = 0; row < kRowsPerWarp; ++row) {
253
+ rows[row] = warp + row * kWarps;
254
+ }
255
+
256
+#pragma unroll
257
+ for (int chunk = 0; chunk < kNumChunks; ++chunk) {
258
+ cp_async_wait_all();
259
+ __syncthreads();
260
+ if (chunk + 1 < kNumChunks) {
261
+ copy_state_chunk<StateT, kChunkV, kDimK, 2>(
262
+ &shared_state[0][0][0], source_state, chunk + 1, tid, kThreads);
263
+ }
264
+
265
+ float h[kRowsPerWarp][4];
266
+#pragma unroll
267
+ for (int row = 0; row < kRowsPerWarp; ++row) {
268
+ const float4 state_value =
269
+ load_state4(&shared_state[chunk & 1][rows[row]][k_base]);
270
+ h[row][0] = state_value.x;
271
+ h[row][1] = state_value.y;
272
+ h[row][2] = state_value.z;
273
+ h[row][3] = state_value.w;
274
+ }
275
+
276
+ for (int t = 0; t < num_tokens; ++t) {
277
+ const float4 q4 = *reinterpret_cast<const float4*>(&shared_q[t][k_base]);
278
+ const float4 k4 = *reinterpret_cast<const float4*>(&shared_k[t][k_base]);
279
+ const float q_values[4] = {q4.x, q4.y, q4.z, q4.w};
280
+ const float k_values[4] = {k4.x, k4.y, k4.z, k4.w};
281
+
282
+ float dot_hk[kRowsPerWarp] = {0.0f, 0.0f, 0.0f, 0.0f};
283
+#pragma unroll
284
+ for (int row = 0; row < kRowsPerWarp; ++row) {
285
+#pragma unroll
286
+ for (int i = 0; i < 4; ++i) {
287
+ h[row][i] *= shared_decay[t];
288
+ dot_hk[row] += h[row][i] * k_values[i];
289
+ }
290
+ }
291
+ const Sum2 dot_hk_01 = warp_reduce_sum_pair(dot_hk[0], dot_hk[1]);
292
+ const Sum2 dot_hk_23 = warp_reduce_sum_pair(dot_hk[2], dot_hk[3]);
293
+ const float reduced_hk[kRowsPerWarp] = {dot_hk_01.x, dot_hk_01.y,
294
+ dot_hk_23.x, dot_hk_23.y};
295
+
296
+ float dot_hq[kRowsPerWarp] = {0.0f, 0.0f, 0.0f, 0.0f};
297
+#pragma unroll
298
+ for (int row = 0; row < kRowsPerWarp; ++row) {
299
+ const int value = chunk * kChunkV + rows[row];
300
+ const float delta =
301
+ (__bfloat162float(shared_v[t][value]) - reduced_hk[row]) *
302
+ shared_beta[t];
303
+#pragma unroll
304
+ for (int i = 0; i < 4; ++i) {
305
+ h[row][i] += k_values[i] * delta;
306
+ dot_hq[row] += h[row][i] * q_values[i];
307
+ }
308
+ }
309
+ const Sum2 dot_hq_01 = warp_reduce_sum_pair(dot_hq[0], dot_hq[1]);
310
+ const Sum2 dot_hq_23 = warp_reduce_sum_pair(dot_hq[2], dot_hq[3]);
311
+ if (lane == 0) {
312
+ shared_out[t][chunk * kChunkV + rows[0]] =
313
+ __float2bfloat16(dot_hq_01.x);
314
+ shared_out[t][chunk * kChunkV + rows[1]] =
315
+ __float2bfloat16(dot_hq_01.y);
316
+ shared_out[t][chunk * kChunkV + rows[2]] =
317
+ __float2bfloat16(dot_hq_23.x);
318
+ shared_out[t][chunk * kChunkV + rows[3]] =
319
+ __float2bfloat16(dot_hq_23.y);
320
+ }
321
+
322
+ const int destination_slot =
323
+ state_indices[request * state_indices_width + t];
324
+ if (destination_slot > 0) {
325
+ StateT* destination_state =
326
+ state +
327
+ static_cast<int64_t>(destination_slot) * strides.state_slot +
328
+ value_head * kDimV * kDimK;
329
+#pragma unroll
330
+ for (int row = 0; row < kRowsPerWarp; ++row) {
331
+ const int value = chunk * kChunkV + rows[row];
332
+ store_state4(destination_state + value * kDimK + k_base,
333
+ make_float4(h[row][0], h[row][1], h[row][2], h[row][3]));
334
+ }
335
+ }
336
+ }
337
+ }
338
+ __syncthreads();
339
+
340
+ if (warp < num_tokens) {
341
+ const int t = warp;
342
+ float output_values[4];
343
+ float sum_square = 0.0f;
344
+#pragma unroll
345
+ for (int i = 0; i < 4; ++i) {
346
+ const int value = lane + i * 32;
347
+ output_values[i] = __bfloat162float(shared_out[t][value]);
348
+ sum_square += output_values[i] * output_values[i];
349
+ }
350
+ sum_square = warp_reduce_sum(sum_square);
351
+ const float rstd =
352
+ rsqrtf(sum_square / static_cast<float>(kDimV) + norm_eps);
353
+ const int token = bos + t;
354
+#pragma unroll
355
+ for (int i = 0; i < 4; ++i) {
356
+ const int value = lane + i * 32;
357
+ const float gate = silu_fast(__bfloat162float(
358
+ output_gate[static_cast<int64_t>(token) * strides.gate_row +
359
+ value_head * kDimV + value]));
360
+ const float weight =
361
+ norm_weight_is_bf16
362
+ ? __bfloat162float(
363
+ static_cast<const __nv_bfloat16*>(norm_weight)[value])
364
+ : static_cast<const float*>(norm_weight)[value];
365
+ const int64_t out_offset =
366
+ (static_cast<int64_t>(token) * HV + value_head) * kDimV + value;
367
+ out[out_offset] =
368
+ __float2bfloat16(output_values[i] * rstd * weight * gate);
369
+ }
370
+ }
371
+}
372
+
373
+template <typename StateT>
374
+void launch_gdn_decode_post_conv_mtp(
375
+ torch::stable::Tensor const& mixed_qkv, torch::stable::Tensor const& a_log,
376
+ torch::stable::Tensor const& dt_bias,
377
+ torch::stable::Tensor const& state_indices,
378
+ torch::stable::Tensor const& cu_seqlens,
379
+ torch::stable::Tensor const& num_accepted_tokens,
380
+ torch::stable::Tensor& state, torch::stable::Tensor const& norm_weight,
381
+ torch::stable::Tensor& out, const __nv_bfloat16* a, const __nv_bfloat16* b,
382
+ const __nv_bfloat16* output_gate, int num_key_heads, int num_value_heads,
383
+ double scale, double norm_eps, GdnDecodeStrides strides) {
384
+ using torch::headeronly::ScalarType;
385
+
386
+ const auto dt_bias_scalar_type = dt_bias.scalar_type();
387
+ const int dt_bias_type =
388
+ dt_bias_scalar_type == ScalarType::Float
389
+ ? kDtBiasFloat32
390
+ : (dt_bias_scalar_type == ScalarType::BFloat16 ? kDtBiasBFloat16
391
+ : kDtBiasFloat16);
392
+ torch::stable::accelerator::DeviceGuard const device_guard(
393
+ mixed_qkv.get_device_index());
394
+ const cudaStream_t stream =
395
+ get_current_cuda_stream(mixed_qkv.get_device_index());
396
+ const int num_requests = static_cast<int>(state_indices.size(0));
397
+ const dim3 grid(num_requests, num_value_heads);
398
+ gdn_decode_post_conv_mtp_kernel<StateT><<<grid, kThreads, 0, stream>>>(
399
+ static_cast<const __nv_bfloat16*>(mixed_qkv.data_ptr()), a, b,
400
+ static_cast<const float*>(a_log.data_ptr()), dt_bias.data_ptr(),
401
+ static_cast<const int*>(state_indices.data_ptr()),
402
+ static_cast<const int*>(cu_seqlens.data_ptr()),
403
+ static_cast<const int*>(num_accepted_tokens.data_ptr()),
404
+ static_cast<StateT*>(state.data_ptr()), output_gate,
405
+ norm_weight.data_ptr(), static_cast<__nv_bfloat16*>(out.data_ptr()),
406
+ num_key_heads, num_value_heads, static_cast<int>(state_indices.size(1)),
407
+ dt_bias_type, norm_weight.scalar_type() == ScalarType::BFloat16,
408
+ static_cast<float>(scale), static_cast<float>(norm_eps), strides);
409
+ const cudaError_t error = cudaGetLastError();
410
+ STD_TORCH_CHECK(error == cudaSuccess,
411
+ "GDN decode MTP post-conv kernel launch failed: ",
412
+ cudaGetErrorString(error));
413
+}
414
+
415
+} // namespace
416
+
417
+void fused_gdn_decode_post_conv_mtp(
418
+ torch::stable::Tensor const& mixed_qkv, torch::stable::Tensor const& a,
419
+ torch::stable::Tensor const& b, torch::stable::Tensor const& a_log,
420
+ torch::stable::Tensor const& dt_bias,
421
+ torch::stable::Tensor const& state_indices,
422
+ torch::stable::Tensor const& cu_seqlens,
423
+ torch::stable::Tensor const& num_accepted_tokens,
424
+ torch::stable::Tensor& state, torch::stable::Tensor const& output_gate,
425
+ torch::stable::Tensor const& norm_weight, torch::stable::Tensor& out,
426
+ double scale, double norm_eps) {
427
+ using torch::headeronly::ScalarType;
428
+
429
+ STD_TORCH_CHECK(
430
+ mixed_qkv.is_cuda() && mixed_qkv.scalar_type() == ScalarType::BFloat16,
431
+ "mixed_qkv must be a CUDA bfloat16 tensor");
432
+ STD_TORCH_CHECK(a.is_cuda() && a.scalar_type() == ScalarType::BFloat16,
433
+ "a must be a CUDA bfloat16 tensor");
434
+ STD_TORCH_CHECK(b.is_cuda() && b.scalar_type() == ScalarType::BFloat16,
435
+ "b must be a CUDA bfloat16 tensor");
436
+ STD_TORCH_CHECK(a_log.is_cuda() && a_log.scalar_type() == ScalarType::Float,
437
+ "A_log must be a CUDA float32 tensor");
438
+ const auto dt_bias_scalar_type = dt_bias.scalar_type();
439
+ STD_TORCH_CHECK(
440
+ dt_bias.is_cuda() && (dt_bias_scalar_type == ScalarType::Float ||
441
+ dt_bias_scalar_type == ScalarType::BFloat16 ||
442
+ dt_bias_scalar_type == ScalarType::Half),
443
+ "dt_bias must be a CUDA float32, bfloat16, or float16 tensor");
444
+ STD_TORCH_CHECK(
445
+ state_indices.is_cuda() && state_indices.scalar_type() == ScalarType::Int,
446
+ "state_indices must be a CUDA int32 tensor");
447
+ STD_TORCH_CHECK(
448
+ cu_seqlens.is_cuda() && cu_seqlens.scalar_type() == ScalarType::Int,
449
+ "cu_seqlens must be a CUDA int32 tensor");
450
+ STD_TORCH_CHECK(num_accepted_tokens.is_cuda() &&
451
+ num_accepted_tokens.scalar_type() == ScalarType::Int,
452
+ "num_accepted_tokens must be a CUDA int32 tensor");
453
+ const auto state_scalar_type = state.scalar_type();
454
+ STD_TORCH_CHECK(
455
+ state.is_cuda() && (state_scalar_type == ScalarType::Float ||
456
+ state_scalar_type == ScalarType::BFloat16),
457
+ "state must be a CUDA float32 or bfloat16 tensor");
458
+ STD_TORCH_CHECK(output_gate.is_cuda() &&
459
+ output_gate.scalar_type() == ScalarType::BFloat16,
460
+ "output_gate must be a CUDA bfloat16 tensor");
461
+ STD_TORCH_CHECK(norm_weight.is_cuda() &&
462
+ (norm_weight.scalar_type() == ScalarType::Float ||
463
+ norm_weight.scalar_type() == ScalarType::BFloat16),
464
+ "norm_weight must be a CUDA float32 or bfloat16 tensor");
465
+ STD_TORCH_CHECK(out.is_cuda() && out.scalar_type() == ScalarType::BFloat16,
466
+ "out must be a CUDA bfloat16 tensor");
467
+
468
+ STD_TORCH_CHECK(mixed_qkv.dim() == 2,
469
+ "mixed_qkv must have shape [L, 2 * H * 128 + HV * 128]");
470
+ const int num_tokens = static_cast<int>(mixed_qkv.size(0));
471
+ STD_TORCH_CHECK(num_tokens > 0,
472
+ "GDN decode MTP fusion requires at least one token");
473
+ STD_TORCH_CHECK(
474
+ state.dim() == 4 && state.size(2) == kDimV && state.size(3) == kDimK,
475
+ "state must have shape [slots, HV, 128, 128]");
476
+ const int num_value_heads = static_cast<int>(state.size(1));
477
+ const int64_t key_width =
478
+ mixed_qkv.size(1) - static_cast<int64_t>(num_value_heads) * kDimV;
479
+ STD_TORCH_CHECK(key_width > 0 && key_width % (2 * kDimK) == 0,
480
+ "mixed_qkv width is inconsistent with state");
481
+ const int num_key_heads = static_cast<int>(key_width / (2 * kDimK));
482
+ STD_TORCH_CHECK(num_value_heads == 8 * num_key_heads,
483
+ "GDN decode MTP fusion requires HV/H=8");
484
+
485
+ STD_TORCH_CHECK(state_indices.dim() == 2 && state_indices.size(0) > 0 &&
486
+ state_indices.size(1) > 0 &&
487
+ state_indices.size(1) <= kMaxMtpTokens,
488
+ "state_indices must have shape [N, S] with 1 <= S <= 8");
489
+ const int num_requests = static_cast<int>(state_indices.size(0));
490
+ STD_TORCH_CHECK(
491
+ cu_seqlens.dim() == 1 && cu_seqlens.numel() == num_requests + 1,
492
+ "cu_seqlens must have N + 1 elements");
493
+ STD_TORCH_CHECK(num_accepted_tokens.dim() == 1 &&
494
+ num_accepted_tokens.numel() == num_requests,
495
+ "num_accepted_tokens must have N elements");
496
+ STD_TORCH_CHECK(
497
+ a.dim() == 2 && a.size(0) == num_tokens && a.size(1) == num_value_heads,
498
+ "a must have shape [L, HV]");
499
+ STD_TORCH_CHECK(
500
+ b.dim() == 2 && b.size(0) == num_tokens && b.size(1) == num_value_heads,
501
+ "b must have shape [L, HV]");
502
+ STD_TORCH_CHECK(a_log.is_contiguous() && a_log.numel() == num_value_heads,
503
+ "A_log must be contiguous with HV elements");
504
+ STD_TORCH_CHECK(dt_bias.is_contiguous() && dt_bias.numel() == num_value_heads,
505
+ "dt_bias must be contiguous with HV elements");
506
+ STD_TORCH_CHECK(state_indices.is_contiguous(),
507
+ "state_indices must be contiguous");
508
+ STD_TORCH_CHECK(cu_seqlens.is_contiguous(), "cu_seqlens must be contiguous");
509
+ STD_TORCH_CHECK(num_accepted_tokens.is_contiguous(),
510
+ "num_accepted_tokens must be contiguous");
511
+ STD_TORCH_CHECK(output_gate.dim() == 3 && output_gate.size(0) == num_tokens &&
512
+ output_gate.size(1) == num_value_heads &&
513
+ output_gate.size(2) == kDimV,
514
+ "output_gate must have shape [L, HV, 128]");
515
+ STD_TORCH_CHECK(norm_weight.is_contiguous() && norm_weight.numel() == kDimV,
516
+ "norm_weight must be contiguous with 128 elements");
517
+ STD_TORCH_CHECK(out.dim() == 3 && out.size(0) == num_tokens &&
518
+ out.size(1) == num_value_heads && out.size(2) == kDimV,
519
+ "out must have shape [L, HV, 128]");
520
+ STD_TORCH_CHECK(mixed_qkv.stride(1) == 1,
521
+ "mixed_qkv channels must be contiguous");
522
+ STD_TORCH_CHECK(a.stride(1) == 1 && b.stride(1) == 1,
523
+ "a and b heads must be contiguous");
524
+ STD_TORCH_CHECK(state.stride(0) >= num_value_heads * kDimV * kDimK &&
525
+ state.stride(1) == kDimV * kDimK &&
526
+ state.stride(2) == kDimK && state.stride(3) == 1,
527
+ "state must have contiguous [HV, 128, 128] slot contents");
528
+ const int state_elements_per_copy =
529
+ state_scalar_type == ScalarType::Float ? 4 : 8;
530
+ STD_TORCH_CHECK(reinterpret_cast<uintptr_t>(state.data_ptr()) % 16 == 0 &&
531
+ state.stride(0) % state_elements_per_copy == 0,
532
+ "state slots must preserve 16-byte alignment");
533
+ STD_TORCH_CHECK(output_gate.stride(2) == 1 && output_gate.stride(1) == kDimV,
534
+ "output_gate head rows must be contiguous");
535
+ STD_TORCH_CHECK(out.is_contiguous(), "out must be contiguous");
536
+ STD_TORCH_CHECK(norm_eps >= 0.0, "norm_eps must be non-negative");
537
+
538
+ const GdnDecodeStrides strides{mixed_qkv.stride(0), a.stride(0), b.stride(0),
539
+ output_gate.stride(0), state.stride(0)};
540
+ const auto* a_ptr = static_cast<const __nv_bfloat16*>(a.data_ptr());
541
+ const auto* b_ptr = static_cast<const __nv_bfloat16*>(b.data_ptr());
542
+ const auto* output_gate_ptr =
543
+ static_cast<const __nv_bfloat16*>(output_gate.data_ptr());
544
+ if (state_scalar_type == ScalarType::Float) {
545
+ launch_gdn_decode_post_conv_mtp<float>(
546
+ mixed_qkv, a_log, dt_bias, state_indices, cu_seqlens,
547
+ num_accepted_tokens, state, norm_weight, out, a_ptr, b_ptr,
548
+ output_gate_ptr, num_key_heads, num_value_heads, scale, norm_eps,
549
+ strides);
550
+ } else {
551
+ launch_gdn_decode_post_conv_mtp<__nv_bfloat16>(
552
+ mixed_qkv, a_log, dt_bias, state_indices, cu_seqlens,
553
+ num_accepted_tokens, state, norm_weight, out, a_ptr, b_ptr,
554
+ output_gate_ptr, num_key_heads, num_value_heads, scale, norm_eps,
555
+ strides);
556
+ }
557
+}
csrc/libtorch_stable/ops.h
+12
@@ -400,6 +400,18 @@ void fused_kda_decode(
400
torch::stable::Tensor& out, std::optional<double> lower_bound,
401
std::optional<torch::stable::Tensor> output_gate,
402
std::optional<torch::stable::Tensor> norm_weight, double norm_eps);
403
+
404
+void fused_gdn_decode_post_conv_mtp(
405
+ torch::stable::Tensor const& mixed_qkv, torch::stable::Tensor const& a,
406
+ torch::stable::Tensor const& b, torch::stable::Tensor const& a_log,
407
+ torch::stable::Tensor const& dt_bias,
408
+ torch::stable::Tensor const& state_indices,
409
+ torch::stable::Tensor const& cu_seqlens,
410
+ torch::stable::Tensor const& num_accepted_tokens,
411
+ torch::stable::Tensor& state, torch::stable::Tensor const& output_gate,
412
+ torch::stable::Tensor const& norm_weight, torch::stable::Tensor& out,
413
+ double scale, double norm_eps);
414
+
415
#endif
416
417
#ifdef VLLM_ENABLE_KIMI_K3_ATTN_RES
csrc/libtorch_stable/torch_bindings.cpp
+14
@@ -534,6 +534,15 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
534
"Tensor? norm_weight=None, float norm_eps=1e-5) -> ()");
535
#endif
536
537
+#ifdef VLLM_ENABLE_FUSED_GDN_DECODE
538
+ ops.def(
539
+ "fused_gdn_decode_post_conv_mtp("
540
+ "Tensor mixed_qkv, Tensor a, Tensor b, Tensor A_log, Tensor dt_bias, "
541
+ "Tensor state_indices, Tensor cu_seqlens, Tensor num_accepted_tokens, "
542
+ "Tensor! state, Tensor output_gate, Tensor norm_weight, Tensor! out, "
543
+ "float scale, float norm_eps=1e-5) -> ()");
544
+#endif
545
+
546
#ifdef VLLM_ENABLE_KIMI_K3_ATTN_RES
547
ops.def(
548
"kimi_k3_attn_res("
@@ -797,6 +806,11 @@ STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, ops) {
806
ops.impl("fused_kda_decode", TORCH_BOX(&fused_kda_decode));
807
#endif
808
809
+#ifdef VLLM_ENABLE_FUSED_GDN_DECODE
810
+ ops.impl("fused_gdn_decode_post_conv_mtp",
811
+ TORCH_BOX(&fused_gdn_decode_post_conv_mtp));
812
+#endif
813
+
814
#ifdef VLLM_ENABLE_KIMI_K3_ATTN_RES
815
ops.impl("kimi_k3_attn_res", TORCH_BOX(&kimi_k3_attn_res));
816
#endif
tests/compile/test_config.py
+5
@@ -252,6 +252,11 @@ def test_splitting_ops_dynamic():
252
# populated when the engine decides to use piecewise compilation.
253
assert config.compilation_config.cudagraph_mode == CUDAGraphMode.FULL_AND_PIECEWISE
254
assert config.compilation_config.splitting_ops_contain_attention()
255
+ splitting_ops = config.compilation_config.splitting_ops
256
+ assert splitting_ops is not None
257
+ assert {
258
+ "vllm::qwen_gdn_attention_core_fused_norm_packed",
259
+ } <= set(splitting_ops)
260
261
# When use_inductor_graph_partition=True
262
config = VllmConfig(
tests/kernels/mamba/test_gdn_fused_mtp.py
new
+345
@@ -0,0 +1,345 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Model-path tests for fused Qwen3.5 GDN MTP decode."""
4
+
5
+from __future__ import annotations
6
+
7
+import types
8
+from unittest.mock import patch
9
+
10
+import pytest
11
+import torch
12
+
13
+from vllm.platforms import current_platform
14
+
15
+if not (current_platform.is_cuda() and current_platform.has_device_capability(80)):
16
+ pytest.skip(
17
+ reason="Fused GDN MTP decode requires CUDA compute capability 8.0+.",
18
+ allow_module_level=True,
19
+ )
20
+
21
+from tests.v1.attention.utils import ( # noqa: E402
22
+ BatchSpec,
23
+ create_common_attn_metadata,
24
+ create_vllm_config,
25
+)
26
+from vllm.config import SpeculativeConfig, set_current_vllm_config # noqa: E402
27
+from vllm.model_executor.layers.mamba.gdn import qwen_gdn_linear_attn # noqa: E402
28
+from vllm.model_executor.layers.mamba.gdn.qwen_gdn_linear_attn import ( # noqa: E402
29
+ ChunkGatedDeltaRule,
30
+ QwenGatedDeltaNetAttention,
31
+)
32
+from vllm.model_executor.layers.mamba.mamba_utils import ( # noqa: E402
33
+ MambaStateShapeCalculator,
34
+)
35
+from vllm.third_party.flash_linear_attention.ops.layernorm_guard import ( # noqa: E402
36
+ rmsnorm_fn,
37
+)
38
+from vllm.utils.torch_utils import _encode_layer_name # noqa: E402
39
+from vllm.v1.attention.backends.gdn_attn import ( # noqa: E402
40
+ GDNAttentionMetadataBuilder,
41
+)
42
+from vllm.v1.kv_cache_interface import MambaSpec # noqa: E402
43
+
44
+NUM_SPEC = 3
45
+SPEC_TOKENS = NUM_SPEC + 1
46
+H = 1
47
+HV = 8
48
+K = 128
49
+V = 128
50
+CONV_KERNEL = 4
51
+CONV_DIM = 2 * H * K + HV * V
52
+BLOCK_SIZE = 16
53
+PREFIX = "model.layers.0.linear_attn"
54
+EPS = 1e-6
55
+
56
+
57
+class _TestGatedNorm:
58
+ def __init__(self, weight: torch.Tensor) -> None:
59
+ self.weight = weight
60
+ self.bias = None
61
+ self.eps = EPS
62
+ self.group_size = None
63
+ self.norm_before_gate = True
64
+ self.activation = "silu"
65
+
66
+ def __call__(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
67
+ return rmsnorm_fn(
68
+ x,
69
+ self.weight,
70
+ None,
71
+ z=z,
72
+ eps=self.eps,
73
+ norm_before_gate=True,
74
+ activation=self.activation,
75
+ )
76
+
77
+
78
+def _make_vllm_config():
79
+ config = create_vllm_config(
80
+ model_name="Qwen/Qwen3.5-0.8B",
81
+ block_size=BLOCK_SIZE,
82
+ hf_config_override={"linear_key_head_dim": K},
83
+ )
84
+ config.additional_config = {"gdn_prefill_backend": "cutedsl"}
85
+ config.cache_config.mamba_cache_mode = "none"
86
+ config.speculative_config = SpeculativeConfig(
87
+ method="ngram", num_speculative_tokens=NUM_SPEC
88
+ )
89
+ return config
90
+
91
+
92
+def _build_layer(
93
+ vllm_config,
94
+ conv_state: torch.Tensor,
95
+ ssm_state: torch.Tensor,
96
+ a_log: torch.Tensor,
97
+ dt_bias: torch.Tensor,
98
+ conv_weight: torch.Tensor,
99
+ norm_weight: torch.Tensor,
100
+):
101
+ layer = types.SimpleNamespace(
102
+ prefix=PREFIX,
103
+ enable_packed_recurrent_decode=False,
104
+ disable_tp_for_ba_proj=False,
105
+ tp_size=1,
106
+ num_k_heads=H,
107
+ num_v_heads=HV,
108
+ head_k_dim=K,
109
+ head_v_dim=V,
110
+ key_dim=K,
111
+ value_dim=HV * V,
112
+ activation="silu",
113
+ A_log=a_log,
114
+ dt_bias=dt_bias,
115
+ conv1d=types.SimpleNamespace(weight=conv_weight, bias=None),
116
+ kv_cache=(conv_state, ssm_state),
117
+ norm=_TestGatedNorm(norm_weight),
118
+ layer_norm_epsilon=EPS,
119
+ gdn_decode_kernel="cuda",
120
+ )
121
+ with set_current_vllm_config(vllm_config):
122
+ layer.chunk_gated_delta_rule = ChunkGatedDeltaRule()
123
+ for name in (
124
+ "rearrange_mixed_qkv",
125
+ "_forward_core",
126
+ "_forward_core_decode_spec_post_conv_fused_norm",
127
+ "_forward_core_decode_spec_fused_norm",
128
+ "_can_use_fused_gdn_mtp_decode",
129
+ "_rms_norm_gated_cuda",
130
+ "_forward_core_fused_norm",
131
+ "_forward_core_fused_norm_packed",
132
+ "split_ba",
133
+ ):
134
+ setattr(
135
+ layer,
136
+ name,
137
+ types.MethodType(getattr(QwenGatedDeltaNetAttention, name), layer),
138
+ )
139
+ return layer
140
+
141
+
142
+@torch.inference_mode()
143
+def test_fused_forward_uses_packed_entrypoint() -> None:
144
+ """Fused mode keeps projected QKVZ and BA packed through the model op."""
145
+ device = torch.device("cuda")
146
+ num_tokens = 3
147
+ hidden_states = torch.empty(num_tokens, 1, dtype=torch.bfloat16, device=device)
148
+ mixed_qkvz = torch.randn(
149
+ num_tokens, CONV_DIM + HV * V, dtype=torch.bfloat16, device=device
150
+ )
151
+ ba = torch.randn(num_tokens, 2 * HV, dtype=torch.bfloat16, device=device)
152
+ layer = types.SimpleNamespace(
153
+ prefix=PREFIX,
154
+ enable_fused_gdn_decode=True,
155
+ norm=types.SimpleNamespace(
156
+ weight=torch.empty(V, dtype=torch.bfloat16, device=device)
157
+ ),
158
+ num_v_heads=HV,
159
+ tp_size=1,
160
+ head_v_dim=V,
161
+ in_proj_qkvz=lambda _: (mixed_qkvz, None),
162
+ in_proj_ba=lambda _: (ba, None),
163
+ out_proj=lambda x: (x, None),
164
+ )
165
+ layer.forward_cuda = types.MethodType(
166
+ QwenGatedDeltaNetAttention.forward_cuda, layer
167
+ )
168
+
169
+ def packed_op(
170
+ actual_qkvz: torch.Tensor,
171
+ actual_ba: torch.Tensor,
172
+ output: torch.Tensor,
173
+ *,
174
+ layer_name: str,
175
+ ) -> None:
176
+ assert actual_qkvz is mixed_qkvz
177
+ assert actual_ba is ba
178
+ assert layer_name == _encode_layer_name(PREFIX)
179
+ output.fill_(1)
180
+
181
+ with (
182
+ patch.object(
183
+ torch.ops.vllm,
184
+ "qwen_gdn_attention_core_fused_norm_packed",
185
+ side_effect=packed_op,
186
+ ) as packed_mock,
187
+ patch.object(torch.ops.vllm, "qwen_gdn_attention_core") as triton_mock,
188
+ ):
189
+ output = layer.forward_cuda(hidden_states)
190
+
191
+ packed_mock.assert_called_once()
192
+ triton_mock.assert_not_called()
193
+ torch.testing.assert_close(output, torch.ones_like(output))
194
+
195
+
196
+@pytest.mark.parametrize(
197
+ "seq_lens,query_lens,draft_tokens,expected_fused_calls",
198
+ [
199
+ pytest.param([128], [SPEC_TOKENS], [NUM_SPEC], 1, id="pure-mtp"),
200
+ pytest.param(
201
+ [128, 96],
202
+ [SPEC_TOKENS, 64],
203
+ [NUM_SPEC, -1],
204
+ 0,
205
+ id="mixed-mtp-falls-back",
206
+ ),
207
+ pytest.param([96], [64], [-1], 0, id="pure-prefill"),
208
+ pytest.param([128], [1], [-1], 0, id="pure-decode"),
209
+ ],
210
+)
211
+@torch.inference_mode()
212
+def test_fused_model_path_matches_reference(
213
+ seq_lens: list[int],
214
+ query_lens: list[int],
215
+ draft_tokens: list[int],
216
+ expected_fused_calls: int,
217
+) -> None:
218
+ """Fused MTP and its mixed/prefill/decode fallbacks match the reference."""
219
+ torch.manual_seed(1)
220
+ device = torch.device("cuda")
221
+ vllm_config = _make_vllm_config()
222
+ builder = GDNAttentionMetadataBuilder(
223
+ kv_cache_spec=MambaSpec(
224
+ block_size=BLOCK_SIZE,
225
+ shapes=((16, 64),),
226
+ dtypes=(torch.float16,),
227
+ num_speculative_blocks=NUM_SPEC,
228
+ ),
229
+ layer_names=[PREFIX],
230
+ vllm_config=vllm_config,
231
+ device=device,
232
+ )
233
+ batch = BatchSpec(seq_lens=seq_lens, query_lens=query_lens)
234
+ common = create_common_attn_metadata(
235
+ batch, BLOCK_SIZE, device, arange_block_indices=True
236
+ )
237
+ common.block_table_tensor.add_(1)
238
+ with set_current_vllm_config(vllm_config):
239
+ metadata = builder.build(
240
+ common_prefix_len=0,
241
+ common_attn_metadata=common,
242
+ num_accepted_tokens=torch.ones(
243
+ batch.batch_size, dtype=torch.int32, device=device
244
+ ),
245
+ num_decode_draft_tokens_cpu=torch.tensor(draft_tokens, dtype=torch.int32),
246
+ )
247
+
248
+ state_indices = [
249
+ indices
250
+ for indices in (
251
+ metadata.spec_state_indices_tensor,
252
+ metadata.non_spec_state_indices_tensor,
253
+ )
254
+ if indices is not None and indices.numel() > 0
255
+ ]
256
+ pool_size = max(int(indices.max().item()) for indices in state_indices) + 1
257
+ conv_state_shape, temporal_state_shape = (
258
+ MambaStateShapeCalculator.gated_delta_net_state_shape(
259
+ 1, H, HV, K, V, CONV_KERNEL, NUM_SPEC
260
+ )
261
+ )
262
+ conv_state_seed = 0.05 * torch.randn(
263
+ pool_size, *conv_state_shape, dtype=torch.bfloat16, device=device
264
+ )
265
+ ssm_state_seed = 0.01 * torch.randn(
266
+ pool_size, *temporal_state_shape, dtype=torch.float32, device=device
267
+ )
268
+ a_log = 0.1 * torch.randn(HV, dtype=torch.float32, device=device)
269
+ dt_bias = 0.1 * torch.randn(HV, dtype=torch.float32, device=device)
270
+ conv_weight = 0.1 * torch.randn(
271
+ CONV_DIM, 1, CONV_KERNEL, dtype=torch.bfloat16, device=device
272
+ )
273
+ norm_weight = torch.randn(V, dtype=torch.float32, device=device)
274
+ num_tokens = batch.compute_num_tokens()
275
+ mixed_qkv = 0.1 * torch.randn(
276
+ num_tokens, CONV_DIM, dtype=torch.bfloat16, device=device
277
+ )
278
+ b = 0.1 * torch.randn(num_tokens, HV, dtype=torch.bfloat16, device=device)
279
+ a = 0.1 * torch.randn_like(b)
280
+ output_gate = 0.1 * torch.randn(
281
+ num_tokens, HV, V, dtype=torch.bfloat16, device=device
282
+ )
283
+ mixed_qkvz = torch.cat((mixed_qkv, output_gate.flatten(1)), dim=-1)
284
+ ba = torch.cat((b, a), dim=-1)
285
+ context = types.SimpleNamespace(attn_metadata={PREFIX: metadata})
286
+
287
+ reference_layer = _build_layer(
288
+ vllm_config,
289
+ conv_state_seed.clone(),
290
+ ssm_state_seed.clone(),
291
+ a_log,
292
+ dt_bias,
293
+ conv_weight,
294
+ norm_weight,
295
+ )
296
+ reference_out = torch.zeros_like(output_gate)
297
+ with patch.object(
298
+ qwen_gdn_linear_attn, "get_forward_context", return_value=context
299
+ ):
300
+ reference_layer._forward_core(
301
+ mixed_qkv=mixed_qkv.clone(),
302
+ b=b,
303
+ a=a,
304
+ core_attn_out=reference_out,
305
+ )
306
+ reference_out = reference_layer.norm(reference_out, output_gate)
307
+
308
+ fused_layer = _build_layer(
309
+ vllm_config,
310
+ conv_state_seed.clone(),
311
+ ssm_state_seed.clone(),
312
+ a_log,
313
+ dt_bias,
314
+ conv_weight,
315
+ norm_weight,
316
+ )
317
+ context.no_compile_layers = {PREFIX: fused_layer}
318
+ fused_out = torch.zeros_like(output_gate)
319
+ fused_op = qwen_gdn_linear_attn.ops.fused_gdn_decode_post_conv_mtp
320
+ with (
321
+ patch.object(qwen_gdn_linear_attn, "get_forward_context", return_value=context),
322
+ patch.object(
323
+ qwen_gdn_linear_attn.ops,
324
+ "fused_gdn_decode_post_conv_mtp",
325
+ wraps=fused_op,
326
+ ) as fused_mock,
327
+ ):
328
+ torch.ops.vllm.qwen_gdn_attention_core_fused_norm_packed(
329
+ mixed_qkvz.clone(),
330
+ ba,
331
+ fused_out,
332
+ layer_name=_encode_layer_name(PREFIX),
333
+ )
334
+
335
+ assert fused_mock.call_count == expected_fused_calls
336
+ torch.testing.assert_close(
337
+ fused_layer.kv_cache[0], reference_layer.kv_cache[0], atol=0, rtol=0
338
+ )
339
+ torch.testing.assert_close(fused_out, reference_out, atol=3e-2, rtol=3e-2)
340
+ torch.testing.assert_close(
341
+ fused_layer.kv_cache[1],
342
+ reference_layer.kv_cache[1],
343
+ atol=3e-2,
344
+ rtol=3e-2,
345
+ )
tests/kernels/test_fused_gdn_post_conv.py
+139
-3
@@ -1,18 +1,25 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Tests for fused_gdn_prefill_post_conv kernel.
3
+"""Tests for GDN post-convolution kernels.
4
5
-Verifies that the fused kernel matches the reference:
6
- split → rearrange → contiguous → l2norm → gating
5
+The prefill tests cover preparation and the MTP tests cover recurrent state
6
+updates plus output normalization and gating.
7
"""
8
9
import pytest
10
import torch
11
import torch.nn.functional as F
12
13
+from vllm import _custom_ops as ops
14
+from vllm.third_party.flash_linear_attention.ops import (
15
+ fused_sigmoid_gating_delta_rule_update,
16
+)
17
from vllm.third_party.flash_linear_attention.ops.fused_gdn_prefill_post_conv import (
18
fused_post_conv_prep,
19
)
20
+from vllm.third_party.flash_linear_attention.ops.layernorm_guard import (
21
+ rmsnorm_fn,
22
+)
23
24
25
def reference_post_conv(
@@ -207,3 +214,132 @@ def test_fused_post_conv_l0():
214
)
215
assert q.shape == (0, H, K)
216
assert g.shape == (0, HV)
217
+
218
+
219
+@pytest.mark.parametrize(
220
+ "tp_size,query_lengths,state_dtype,norm_dtype",
221
+ [
222
+ pytest.param(16, (4, 4), torch.bfloat16, torch.bfloat16, id="tp16-bf16"),
223
+ pytest.param(4, (4, 4), torch.float32, torch.float32, id="tp4-fp32"),
224
+ pytest.param(16, (4, 2, 0), torch.bfloat16, torch.float32, id="tp16-ragged"),
225
+ pytest.param(4, (4, 2, 0), torch.float32, torch.bfloat16, id="tp4-ragged"),
226
+ pytest.param(16, (8,), torch.float32, torch.bfloat16, id="tp16-max"),
227
+ pytest.param(4, (8,), torch.bfloat16, torch.float32, id="tp4-max"),
228
+ ],
229
+)
230
+@torch.inference_mode()
231
+def test_fused_gdn_decode_post_conv_mtp_ratio8(
232
+ tp_size: int,
233
+ query_lengths: tuple[int, ...],
234
+ state_dtype: torch.dtype,
235
+ norm_dtype: torch.dtype,
236
+) -> None:
237
+ if torch.cuda.get_device_capability() < (8, 0):
238
+ pytest.skip("fused GDN decode MTP requires compute capability 8.0+")
239
+ if not hasattr(torch.ops._C, "fused_gdn_decode_post_conv_mtp"):
240
+ pytest.skip("fused GDN decode MTP op is not built")
241
+
242
+ torch.manual_seed(0)
243
+ device = "cuda"
244
+ H = 16 // tp_size
245
+ HV = 128 // tp_size
246
+ K = V = 128
247
+ num_reqs = len(query_lengths)
248
+ state_width = max(query_lengths)
249
+ num_tokens = sum(query_lengths)
250
+ num_slots = num_reqs * state_width + 1
251
+ scale = K**-0.5
252
+ eps = 1e-6
253
+
254
+ mixed_qkv = torch.randn(
255
+ num_tokens,
256
+ 2 * H * K + HV * V,
257
+ dtype=torch.bfloat16,
258
+ device=device,
259
+ )
260
+ query, key, value = torch.split(
261
+ mixed_qkv,
262
+ [H * K, H * K, HV * V],
263
+ dim=-1,
264
+ )
265
+ query = query.view(1, num_tokens, H, K)
266
+ key = key.view(1, num_tokens, H, K)
267
+ value = value.view(1, num_tokens, HV, V)
268
+ ba = torch.randn(num_tokens, 2 * HV, dtype=torch.bfloat16, device=device)
269
+ b, a = ba.chunk(2, dim=-1)
270
+ assert not a.is_contiguous()
271
+ assert not b.is_contiguous()
272
+ A_log = 0.5 * torch.randn(HV, dtype=torch.float32, device=device)
273
+ dt_bias = 0.1 * torch.randn(HV, dtype=torch.float32, device=device)
274
+ output_gate = torch.randn(num_tokens, HV, V, dtype=torch.bfloat16, device=device)
275
+ norm_weight = torch.randn(V, dtype=norm_dtype, device=device)
276
+ state_ref = (
277
+ 0.01 * torch.randn(num_slots, HV, V, K, dtype=torch.float32, device=device)
278
+ ).to(state_dtype)
279
+ state_actual = state_ref.clone()
280
+ state_indices = torch.arange(1, num_slots, dtype=torch.int32, device=device).view(
281
+ num_reqs, state_width
282
+ )
283
+ cu_seqlens = torch.tensor(
284
+ [0, *torch.tensor(query_lengths).cumsum(0).tolist()],
285
+ dtype=torch.int32,
286
+ device=device,
287
+ )
288
+ num_accepted_tokens = torch.ones(num_reqs, dtype=torch.int32, device=device)
289
+ if query_lengths[-1] == 0:
290
+ state_indices[-1].zero_()
291
+
292
+ for step, accepted_tokens in enumerate((1, min(2, state_width), state_width)):
293
+ num_accepted_tokens.fill_(accepted_tokens)
294
+ if query_lengths[-1] == 0:
295
+ num_accepted_tokens[-1] = 1
296
+ raw_ref, _ = fused_sigmoid_gating_delta_rule_update(
297
+ A_log=A_log,
298
+ a=a,
299
+ b=b,
300
+ dt_bias=dt_bias,
301
+ q=query,
302
+ k=key,
303
+ v=value,
304
+ initial_state=state_ref,
305
+ inplace_final_state=True,
306
+ cu_seqlens=cu_seqlens,
307
+ ssm_state_indices=state_indices,
308
+ num_accepted_tokens=num_accepted_tokens,
309
+ scale=scale,
310
+ use_qk_l2norm_in_kernel=True,
311
+ )
312
+ expected = rmsnorm_fn(
313
+ raw_ref.squeeze(0),
314
+ norm_weight,
315
+ None,
316
+ z=output_gate,
317
+ eps=eps,
318
+ norm_before_gate=True,
319
+ activation="silu",
320
+ )
321
+ actual = ops.fused_gdn_decode_post_conv_mtp(
322
+ mixed_qkv=mixed_qkv,
323
+ a=a,
324
+ b=b,
325
+ A_log=A_log,
326
+ dt_bias=dt_bias,
327
+ state_indices=state_indices,
328
+ cu_seqlens=cu_seqlens,
329
+ num_accepted_tokens=num_accepted_tokens,
330
+ state=state_actual,
331
+ output_gate=output_gate,
332
+ norm_weight=norm_weight,
333
+ out=torch.empty_like(output_gate),
334
+ scale=scale,
335
+ norm_eps=eps,
336
+ )
337
+
338
+ output_error = (actual.float() - expected.float()).norm()
339
+ output_relative_l2 = output_error / expected.float().norm().clamp_min(1e-20)
340
+ assert output_relative_l2 < 5e-4, (
341
+ f"MTP output relative L2 mismatch at step {step}: "
342
+ f"{output_relative_l2.item():.6g}"
343
+ )
344
+
345
+ torch.testing.assert_close(state_actual, state_ref, atol=3e-2, rtol=3e-2)
tests/test_envs.py
+15
@@ -251,6 +251,21 @@ class TestEnvWithChoices:
251
env_func()
252
253
254
+def test_gdn_decode_kernel_env(monkeypatch: pytest.MonkeyPatch):
255
+ env_func = environment_variables["VLLM_GDN_DECODE_KERNEL"]
256
+ monkeypatch.delenv("VLLM_GDN_DECODE_KERNEL", raising=False)
257
+ assert env_func() == "cuda"
258
+
259
+ for value in ("cuda", "triton"):
260
+ monkeypatch.setenv("VLLM_GDN_DECODE_KERNEL", value)
261
+ assert env_func() == value
262
+
263
+ for value in ("fused", "invalid"):
264
+ monkeypatch.setenv("VLLM_GDN_DECODE_KERNEL", value)
265
+ with pytest.raises(ValueError, match="VLLM_GDN_DECODE_KERNEL"):
266
+ env_func()
267
+
268
+
269
class TestEnvListWithChoices:
270
"""Test cases for env_list_with_choices function."""
271
vllm/_custom_ops.py
+37
@@ -2777,6 +2777,43 @@ def fused_kda_decode(
2777
return out
2778
2779
2780
+def fused_gdn_decode_post_conv_mtp(
2781
+ mixed_qkv: torch.Tensor,
2782
+ a: torch.Tensor,
2783
+ b: torch.Tensor,
2784
+ A_log: torch.Tensor,
2785
+ dt_bias: torch.Tensor,
2786
+ state_indices: torch.Tensor,
2787
+ cu_seqlens: torch.Tensor,
2788
+ num_accepted_tokens: torch.Tensor,
2789
+ state: torch.Tensor,
2790
+ output_gate: torch.Tensor,
2791
+ norm_weight: torch.Tensor,
2792
+ out: torch.Tensor | None = None,
2793
+ scale: float = 128**-0.5,
2794
+ norm_eps: float = 1e-5,
2795
+) -> torch.Tensor:
2796
+ if out is None:
2797
+ out = torch.empty_like(output_gate)
2798
+ torch.ops._C.fused_gdn_decode_post_conv_mtp(
2799
+ mixed_qkv,
2800
+ a,
2801
+ b,
2802
+ A_log,
2803
+ dt_bias,
2804
+ state_indices,
2805
+ cu_seqlens,
2806
+ num_accepted_tokens,
2807
+ state,
2808
+ output_gate,
2809
+ norm_weight,
2810
+ out,
2811
+ scale,
2812
+ norm_eps,
2813
+ )
2814
+ return out
2815
+
2816
+
2817
def concat_and_cache_mla(
2818
kv_c: torch.Tensor,
2819
k_pe: torch.Tensor,
vllm/config/compilation.py
+1
@@ -769,6 +769,7 @@ class CompilationConfig:
769
"vllm::short_conv",
770
"vllm::linear_attention",
771
"vllm::qwen_gdn_attention_core",
772
+ "vllm::qwen_gdn_attention_core_fused_norm_packed",
773
"vllm::gdn_attention_core_xpu",
774
"vllm::olmo_hybrid_gdn_full_forward",
775
"vllm::sparse_attn_indexer",
vllm/envs.py
+10
@@ -127,6 +127,7 @@ if TYPE_CHECKING:
127
VLLM_DISABLED_KERNELS: list[str] = []
128
VLLM_USE_HW_AGNOSTIC: bool = False
129
VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE: bool = True
130
+ VLLM_GDN_DECODE_KERNEL: Literal["cuda", "triton"] = "cuda"
131
VLLM_DISABLE_PYNCCL: bool = False
132
VLLM_USE_OINK_OPS: bool = False
133
VLLM_MXFP8_EMULATION_DEQUANT_AT_LOAD: bool = True
@@ -1209,6 +1210,15 @@ environment_variables: dict[str, Callable[[], Any]] = {
1210
"VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE": lambda: bool(
1211
int(os.getenv("VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE", "1"))
1212
),
1213
+ # Select the GDN MTP decode implementation. "cuda" uses the fused decode
1214
+ # kernel where supported and falls back to "triton" otherwise; setting it
1215
+ # explicitly to "cuda" raises when unsupported.
1216
+ "VLLM_GDN_DECODE_KERNEL": env_with_choices(
1217
+ "VLLM_GDN_DECODE_KERNEL",
1218
+ "cuda",
1219
+ ["cuda", "triton"],
1220
+ case_sensitive=False,
1221
+ ),
1222
# Disable pynccl (using torch.distributed instead)
1223
"VLLM_DISABLE_PYNCCL": lambda: (
1224
os.getenv("VLLM_DISABLE_PYNCCL", "False").lower() in ("true", "1")
vllm/model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py
+303
-4
@@ -2,12 +2,14 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Inference-only Qwen3-Next/Qwen3.5 model."""
4
5
+import os
6
from typing import Literal
7
8
import torch
9
from einops import rearrange
10
from torch import nn
11
12
+from vllm import _custom_ops as ops
13
from vllm import envs
14
from vllm._aiter_ops import rocm_aiter_ops
15
from vllm.config import (
@@ -84,6 +86,9 @@ if GDN_AITER_TRITON_AVAILABLE:
86
87
logger = init_logger(__name__)
88
89
+MAX_FUSED_GDN_MTP_TOKENS = 8
90
+FUSED_GDN_STATE_DTYPES = (torch.float32, torch.bfloat16)
91
+
92
93
def _resolve_gdn_prefill_backend(
94
vllm_config: VllmConfig,
@@ -484,12 +489,50 @@ class QwenGatedDeltaNetAttention(GatedDeltaNetAttention):
489
self.enable_packed_recurrent_decode = (
490
envs.VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE
491
)
492
+ self.gdn_decode_kernel = envs.VLLM_GDN_DECODE_KERNEL.strip().lower()
493
+ if self.gdn_decode_kernel == "cuda":
494
+ reason = self._fused_gdn_decode_unsupported_reason(vllm_config)
495
+ if reason is not None:
496
+ if "VLLM_GDN_DECODE_KERNEL" in os.environ:
497
+ raise ValueError(
498
+ f"VLLM_GDN_DECODE_KERNEL=cuda is not supported: {reason}"
499
+ )
500
+ logger.info_once(
501
+ "Falling back to the Triton GDN decode path: %s", reason
502
+ )
503
+ self.gdn_decode_kernel = "triton"
504
+ self.enable_fused_gdn_decode = self.gdn_decode_kernel == "cuda"
505
+ logger.info_once("GDN decode kernel: %s", self.gdn_decode_kernel)
506
507
compilation_config = get_current_vllm_config().compilation_config
508
if prefix in compilation_config.static_forward_context:
509
raise ValueError(f"Duplicate layer name: {prefix}")
510
compilation_config.static_forward_context[prefix] = self
511
512
+ def _fused_gdn_decode_unsupported_reason(
513
+ self, vllm_config: VllmConfig
514
+ ) -> str | None:
515
+ conv_state_dtype, recurrent_state_dtype = self.get_state_dtype()
516
+ if (
517
+ self.gqa_interleaved_layout
518
+ or self.head_k_dim != 128
519
+ or self.head_v_dim != 128
520
+ or self.norm.activation != "silu"
521
+ or vllm_config.model_config.dtype != torch.bfloat16
522
+ or conv_state_dtype != torch.bfloat16
523
+ or recurrent_state_dtype not in FUSED_GDN_STATE_DTYPES
524
+ or not current_platform.has_device_capability(80)
525
+ ):
526
+ return (
527
+ "the fused CUDA kernel requires a BF16 GDN model with "
528
+ "K=V=128, SiLU gating, non-interleaved GQA layout, BF16 "
529
+ "convolution cache, BF16 or FP32 recurrent state, and a "
530
+ "GPU with compute capability 8.0+"
531
+ )
532
+ if not hasattr(torch.ops._C, "fused_gdn_decode_post_conv_mtp"):
533
+ return "torch.ops._C.fused_gdn_decode_post_conv_mtp is not built"
534
+ return None
535
+
536
def create_qkvz_proj(
537
self,
538
hidden_size: int,
@@ -846,6 +889,26 @@ class QwenGatedDeltaNetAttention(GatedDeltaNetAttention):
889
mixed_qkvz, _ = self.in_proj_qkvz(hidden_states)
890
ba, _ = self.in_proj_ba(hidden_states)
891
892
+ use_fused_gdn_decode = (
893
+ self.enable_fused_gdn_decode
894
+ and hidden_states.dtype == torch.bfloat16
895
+ and self.norm.weight.dtype in (torch.bfloat16, torch.float32)
896
+ )
897
+ if use_fused_gdn_decode:
898
+ core_attn_out = torch.zeros(
899
+ (num_tokens, self.num_v_heads // self.tp_size, self.head_v_dim),
900
+ dtype=hidden_states.dtype,
901
+ device=hidden_states.device,
902
+ )
903
+ torch.ops.vllm.qwen_gdn_attention_core_fused_norm_packed(
904
+ mixed_qkvz,
905
+ ba,
906
+ core_attn_out,
907
+ layer_name=_encode_layer_name(self.prefix),
908
+ )
909
+ output, _ = self.out_proj(core_attn_out.flatten(-2))
910
+ return output
911
+
912
if self.gqa_interleaved_layout:
913
# Qwen3-Next: unpack the interleaved GQA layout
914
query, key, value, z, b, a = self.fix_query_key_value_ordering(
@@ -862,8 +925,6 @@ class QwenGatedDeltaNetAttention(GatedDeltaNetAttention):
925
mixed_qkv, z = mixed_qkvz.split([qkv_size, z_size], dim=-1)
926
z = z.reshape(z.size(0), -1, self.head_v_dim)
927
b, a = self.split_ba(ba)
865
- b = b.contiguous()
866
- a = a.contiguous()
928
929
# ============================================================
930
# Part 2: Core Attention (Custom Op)
@@ -878,8 +939,8 @@ class QwenGatedDeltaNetAttention(GatedDeltaNetAttention):
939
940
torch.ops.vllm.qwen_gdn_attention_core(
941
mixed_qkv,
881
- b,
882
- a,
942
+ b.contiguous(),
943
+ a.contiguous(),
944
core_attn_out,
945
layer_name=_encode_layer_name(self.prefix),
946
)
@@ -1622,6 +1683,211 @@ class QwenGatedDeltaNetAttention(GatedDeltaNetAttention):
1683
)
1684
return
1685
1686
+ def _forward_core_decode_spec_fused_norm(
1687
+ self,
1688
+ mixed_qkv: torch.Tensor,
1689
+ b: torch.Tensor,
1690
+ a: torch.Tensor,
1691
+ output_gate: torch.Tensor,
1692
+ core_attn_out: torch.Tensor,
1693
+ attn_metadata: GDNAttentionMetadata,
1694
+ ) -> None:
1695
+ state_indices = attn_metadata.spec_state_indices_tensor
1696
+ cu_seqlens = attn_metadata.spec_query_start_loc
1697
+ num_accepted_tokens = attn_metadata.num_accepted_tokens
1698
+ assert state_indices is not None
1699
+ assert cu_seqlens is not None
1700
+ assert num_accepted_tokens is not None
1701
+
1702
+ num_requests = attn_metadata.num_spec_decodes
1703
+ num_actual_tokens = attn_metadata.num_actual_tokens
1704
+ conv_state = (
1705
+ self.kv_cache[0]
1706
+ if is_conv_state_dim_first()
1707
+ else self.kv_cache[0].transpose(-1, -2)
1708
+ )
1709
+ conv_weights = self.conv1d.weight.view(
1710
+ self.conv1d.weight.size(0), self.conv1d.weight.size(2)
1711
+ )
1712
+ mixed_qkv = causal_conv1d_update(
1713
+ mixed_qkv[:num_actual_tokens],
1714
+ conv_state,
1715
+ conv_weights,
1716
+ self.conv1d.bias,
1717
+ self.activation,
1718
+ conv_state_indices=state_indices[:num_requests, 0],
1719
+ num_accepted_tokens=num_accepted_tokens[:num_requests],
1720
+ query_start_loc=cu_seqlens[: num_requests + 1],
1721
+ max_query_len=state_indices.size(1),
1722
+ validate_data=False,
1723
+ )
1724
+ self._forward_core_decode_spec_post_conv_fused_norm(
1725
+ mixed_qkv=mixed_qkv,
1726
+ b=b[:num_actual_tokens],
1727
+ a=a[:num_actual_tokens],
1728
+ output_gate=output_gate[:num_actual_tokens],
1729
+ core_attn_out=core_attn_out[:num_actual_tokens],
1730
+ attn_metadata=attn_metadata,
1731
+ )
1732
+
1733
+ def _forward_core_decode_spec_post_conv_fused_norm(
1734
+ self,
1735
+ mixed_qkv: torch.Tensor,
1736
+ b: torch.Tensor,
1737
+ a: torch.Tensor,
1738
+ output_gate: torch.Tensor,
1739
+ core_attn_out: torch.Tensor,
1740
+ attn_metadata: GDNAttentionMetadata,
1741
+ ) -> None:
1742
+ state_indices = attn_metadata.spec_state_indices_tensor
1743
+ cu_seqlens = attn_metadata.spec_query_start_loc
1744
+ num_accepted_tokens = attn_metadata.num_accepted_tokens
1745
+ assert state_indices is not None
1746
+ assert cu_seqlens is not None
1747
+ assert num_accepted_tokens is not None
1748
+
1749
+ num_requests = attn_metadata.num_spec_decodes
1750
+ ops.fused_gdn_decode_post_conv_mtp(
1751
+ mixed_qkv=mixed_qkv,
1752
+ a=a,
1753
+ b=b,
1754
+ A_log=self.A_log,
1755
+ dt_bias=self.dt_bias,
1756
+ state_indices=state_indices[:num_requests],
1757
+ cu_seqlens=cu_seqlens[: num_requests + 1],
1758
+ num_accepted_tokens=num_accepted_tokens[:num_requests],
1759
+ state=self.kv_cache[1],
1760
+ output_gate=output_gate,
1761
+ norm_weight=self.norm.weight,
1762
+ out=core_attn_out,
1763
+ scale=self.head_k_dim**-0.5,
1764
+ norm_eps=self.layer_norm_epsilon,
1765
+ )
1766
+
1767
+ def _forward_core_fused_norm_packed(
1768
+ self,
1769
+ mixed_qkvz: torch.Tensor,
1770
+ ba: torch.Tensor,
1771
+ core_attn_out: torch.Tensor,
1772
+ ) -> None:
1773
+ forward_context = get_forward_context()
1774
+ attn_metadata_raw = forward_context.attn_metadata
1775
+ qkv_size = (self.key_dim * 2 + self.value_dim) // self.tp_size
1776
+ if attn_metadata_raw is None:
1777
+ self._warmup_prefill_kernels(mixed_qkvz[:, :qkv_size], 0)
1778
+ return
1779
+
1780
+ assert isinstance(attn_metadata_raw, dict)
1781
+ attn_metadata = attn_metadata_raw[self.prefix] # type: ignore[index]
1782
+ assert isinstance(attn_metadata, GDNAttentionMetadata)
1783
+ mixed_qkv, output_gate_flat = mixed_qkvz.split(
1784
+ [qkv_size, self.value_dim // self.tp_size], dim=-1
1785
+ )
1786
+ output_gate = output_gate_flat.reshape(
1787
+ output_gate_flat.size(0), -1, self.head_v_dim
1788
+ )
1789
+ b, a = self.split_ba(ba)
1790
+ self._forward_core_fused_norm(
1791
+ mixed_qkv=mixed_qkv,
1792
+ b=b,
1793
+ a=a,
1794
+ output_gate=output_gate,
1795
+ core_attn_out=core_attn_out,
1796
+ )
1797
+
1798
+ def _can_use_fused_gdn_mtp_decode(
1799
+ self, attn_metadata: GDNAttentionMetadata
1800
+ ) -> bool:
1801
+ state_indices = attn_metadata.spec_state_indices_tensor
1802
+ return (
1803
+ attn_metadata.spec_sequence_masks is not None
1804
+ and attn_metadata.num_decodes == 0
1805
+ and attn_metadata.num_spec_decodes > 0
1806
+ and self.kv_cache[1].dtype in FUSED_GDN_STATE_DTYPES
1807
+ and self.gdn_decode_kernel == "cuda"
1808
+ and self.num_v_heads == 8 * self.num_k_heads
1809
+ and state_indices is not None
1810
+ and state_indices.size(1) <= MAX_FUSED_GDN_MTP_TOKENS
1811
+ and hasattr(torch.ops._C, "fused_gdn_decode_post_conv_mtp")
1812
+ )
1813
+
1814
+ def _rms_norm_gated_cuda(
1815
+ self,
1816
+ x: torch.Tensor,
1817
+ output_gate: torch.Tensor,
1818
+ out: torch.Tensor,
1819
+ ) -> None:
1820
+ from vllm.third_party.flash_linear_attention.ops.layernorm_guard import (
1821
+ layer_norm_fwd,
1822
+ )
1823
+
1824
+ x_shape = x.shape
1825
+ assert output_gate.shape == x_shape
1826
+ assert out.shape == x_shape
1827
+ x_2d = x.reshape(-1, x_shape[-1])
1828
+ output_gate_2d = output_gate.reshape(-1, x_shape[-1])
1829
+ out_2d = out.reshape(-1, x_shape[-1])
1830
+ assert x_2d.stride(-1) == 1
1831
+ assert output_gate_2d.stride(-1) == 1
1832
+ assert out_2d.stride(-1) == 1
1833
+ layer_norm_fwd(
1834
+ x_2d,
1835
+ self.norm.weight.contiguous(),
1836
+ self.norm.bias,
1837
+ self.norm.eps,
1838
+ z=output_gate_2d,
1839
+ out=out_2d,
1840
+ group_size=(
1841
+ x_shape[-1] if self.norm.group_size is None else self.norm.group_size
1842
+ ),
1843
+ norm_before_gate=self.norm.norm_before_gate,
1844
+ is_rms_norm=True,
1845
+ activation=self.norm.activation,
1846
+ )
1847
+
1848
+ def _forward_core_fused_norm(
1849
+ self,
1850
+ mixed_qkv: torch.Tensor,
1851
+ b: torch.Tensor,
1852
+ a: torch.Tensor,
1853
+ output_gate: torch.Tensor,
1854
+ core_attn_out: torch.Tensor,
1855
+ ) -> None:
1856
+ forward_context = get_forward_context()
1857
+ attn_metadata_raw = forward_context.attn_metadata
1858
+ if attn_metadata_raw is None:
1859
+ self._warmup_prefill_kernels(mixed_qkv, 0)
1860
+ return
1861
+
1862
+ assert isinstance(attn_metadata_raw, dict)
1863
+ attn_metadata = attn_metadata_raw[self.prefix] # type: ignore[index]
1864
+ assert isinstance(attn_metadata, GDNAttentionMetadata)
1865
+ if (
1866
+ self._can_use_fused_gdn_mtp_decode(attn_metadata)
1867
+ and attn_metadata.num_prefills == 0
1868
+ ):
1869
+ self._forward_core_decode_spec_fused_norm(
1870
+ mixed_qkv=mixed_qkv,
1871
+ b=b,
1872
+ a=a,
1873
+ output_gate=output_gate,
1874
+ core_attn_out=core_attn_out,
1875
+ attn_metadata=attn_metadata,
1876
+ )
1877
+ return
1878
+ self._forward_core(
1879
+ mixed_qkv=mixed_qkv,
1880
+ b=b.contiguous(),
1881
+ a=a.contiguous(),
1882
+ core_attn_out=core_attn_out,
1883
+ )
1884
+ num_actual_tokens = attn_metadata.num_actual_tokens
1885
+ self._rms_norm_gated_cuda(
1886
+ core_attn_out[:num_actual_tokens],
1887
+ output_gate[:num_actual_tokens],
1888
+ core_attn_out[:num_actual_tokens],
1889
+ )
1890
+
1891
1892
def qwen_gdn_attention_core(
1893
qkv_or_qkvz: torch.Tensor,
@@ -1683,6 +1949,39 @@ direct_register_custom_op(
1949
)
1950
1951
1952
+def qwen_gdn_attention_core_fused_norm_packed(
1953
+ mixed_qkvz: torch.Tensor,
1954
+ ba: torch.Tensor,
1955
+ core_attn_out: torch.Tensor,
1956
+ layer_name: LayerNameType,
1957
+) -> None:
1958
+ layer_name = _resolve_layer_name(layer_name)
1959
+ forward_context: ForwardContext = get_forward_context()
1960
+ self = forward_context.no_compile_layers[layer_name]
1961
+ self._forward_core_fused_norm_packed(
1962
+ mixed_qkvz=mixed_qkvz,
1963
+ ba=ba,
1964
+ core_attn_out=core_attn_out,
1965
+ )
1966
+
1967
+
1968
+def gdn_attention_core_fused_norm_packed_fake(
1969
+ mixed_qkvz: torch.Tensor,
1970
+ ba: torch.Tensor,
1971
+ core_attn_out: torch.Tensor,
1972
+ layer_name: LayerNameType,
1973
+) -> None:
1974
+ return
1975
+
1976
+
1977
+direct_register_custom_op(
1978
+ op_name="qwen_gdn_attention_core_fused_norm_packed",
1979
+ op_func=qwen_gdn_attention_core_fused_norm_packed,
1980
+ mutates_args=["core_attn_out"],
1981
+ fake_impl=gdn_attention_core_fused_norm_packed_fake,
1982
+)
1983
+
1984
+
1985
@triton.jit
1986
def fused_gdn_gating_kernel(
1987
g,