[ROCm][Perf] Kimi-K3 Fused kernel for KDA decode (#50654)
Signed-off-by: kliuae <kuanfu.liu@embeddedllm.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: TJian <tunjian.tan@embeddedllm.com>
kliuae committed
Aug 12, 2026 at 11:39 UTC
7f9173dfa24d70846361d867dfe32f7440875460
6 files changed
+1592
-9
CMakeLists.txt
+22
@@ -1164,6 +1164,24 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
1164
# if CUDA endif
1165
endif()
1166
1167
+ # HIP counterpart of the FUSED_KDA_DECODE block above: same `fused_kda_decode`
1168
+ # op, same VLLM_ENABLE_FUSED_KDA_DECODE guard in torch_bindings.cpp. Only
1169
+ # measured on gfx950, so it is only built when gfx950 is in the arch list.
1170
+ # A multi-arch build that includes gfx950 still emits this source for every
1171
+ # arch in that list. The runtime gate in kda_decode.py keeps the kernel off
1172
+ # non-gfx950 devices.
1173
+ if(VLLM_GPU_LANG STREQUAL "HIP")
1174
+ set(FUSED_KDA_DECODE_HIP_ARCHS ${VLLM_GPU_ARCHES})
1175
+ list(FILTER FUSED_KDA_DECODE_HIP_ARCHS INCLUDE REGEX "gfx950")
1176
+ if(FUSED_KDA_DECODE_HIP_ARCHS)
1177
+ set(FUSED_KDA_DECODE_HIP_SRC
1178
+ "csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel_rocm.cu")
1179
+ list(APPEND VLLM_STABLE_EXT_SRC "${FUSED_KDA_DECODE_HIP_SRC}")
1180
+ message(STATUS
1181
+ "Building fused KDA decode for HIP archs: ${FUSED_KDA_DECODE_HIP_ARCHS}")
1182
+ endif()
1183
+ endif()
1184
+
1185
message(STATUS "Enabling C_stable extension.")
1186
list(REMOVE_DUPLICATES VLLM_STABLE_EXT_SRC)
1187
define_extension_target(
@@ -1204,6 +1222,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
1222
CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1)
1223
elseif(VLLM_GPU_LANG STREQUAL "HIP")
1224
target_compile_definitions(_C_stable_libtorch PRIVATE USE_ROCM)
1225
+ if(FUSED_KDA_DECODE_HIP_SRC)
1226
+ target_compile_definitions(_C_stable_libtorch PRIVATE
1227
+ VLLM_ENABLE_FUSED_KDA_DECODE=1)
1228
+ endif()
1229
endif()
1230
1231
# On ROCm, _C_stable_libtorch calls raw HIP APIs (e.g. hipGetDevice in
benchmarks/kernels/benchmark_kimi_k3_kda_decode.py
new
+291
@@ -0,0 +1,291 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Microbenchmark for the fused Kimi-K3 KDA decode kernel on ROCm.
4
+
5
+Compares the single fused launch against the three-kernel Triton chain it
6
+replaces (packed causal conv1d update -> recurrent delta-rule decode -> gated
7
+output RMSNorm), at Kimi-K3 KDA shapes: 96 heads x 128, conv width 4,
8
+gate_lower_bound -5.0. Per-step figures scale the per-layer number by the 69
9
+KDA layers in the model.
10
+
11
+The recurrent state dominates the traffic (num_heads x 128 x 128 fp32 read and
12
+written per token), so the reported bandwidth is the useful metric: the fusion
13
+removes launches and the intermediate QKV / core-output round trips, not the
14
+state traffic itself.
15
+
16
+Example:
17
+ python benchmarks/kernels/benchmark_kimi_k3_kda_decode.py \
18
+ --tokens 1 8 32 64 128 --heads 12
19
+"""
20
+
21
+import argparse
22
+import functools
23
+
24
+import torch
25
+
26
+from vllm.triton_utils import triton
27
+
28
+HEAD_DIM = 128
29
+CONV_WIDTH = 4
30
+GATE_LOWER_BOUND = -5.0
31
+NORM_EPS = 1e-5
32
+NUM_KDA_LAYERS = 69
33
+DTYPE = torch.bfloat16
34
+
35
+
36
+def _bench(fn) -> float:
37
+ return triton.testing.do_bench(fn, warmup=50, rep=300, return_mode="median")
38
+
39
+
40
+def _bench_graph_layers(calls: list) -> float:
41
+ """Per-layer milliseconds for a graph holding one call per KDA layer."""
42
+ stream = torch.cuda.Stream()
43
+ stream.wait_stream(torch.cuda.current_stream())
44
+ with torch.cuda.stream(stream):
45
+ for call in calls[:3]:
46
+ call()
47
+ torch.cuda.current_stream().wait_stream(stream)
48
+ graph = torch.cuda.CUDAGraph()
49
+ with torch.cuda.graph(graph):
50
+ for call in calls:
51
+ call()
52
+ total = triton.testing.do_bench(
53
+ graph.replay, warmup=50, rep=300, return_mode="median"
54
+ )
55
+ return total / len(calls)
56
+
57
+
58
+def _bench_graph(fn, repeats: int = NUM_KDA_LAYERS) -> float:
59
+ """Per-call milliseconds under CUDA-graph replay, as decode actually runs.
60
+
61
+ Eager timings credit a fusion for every Python dispatch it removes; inside a
62
+ captured graph those are gone, so this is the number that decides whether
63
+ the kernel is worth it in the server. The graph holds `repeats` calls (one
64
+ per KDA layer) because a one-call graph is swamped by the ~19 us HIP
65
+ graph-launch overhead, which a real 93-layer graph amortises away.
66
+ """
67
+ stream = torch.cuda.Stream()
68
+ stream.wait_stream(torch.cuda.current_stream())
69
+ with torch.cuda.stream(stream):
70
+ for _ in range(3):
71
+ fn()
72
+ torch.cuda.current_stream().wait_stream(stream)
73
+ graph = torch.cuda.CUDAGraph()
74
+ with torch.cuda.graph(graph):
75
+ for _ in range(repeats):
76
+ fn()
77
+ total = triton.testing.do_bench(
78
+ graph.replay, warmup=50, rep=300, return_mode="median"
79
+ )
80
+ return total / repeats
81
+
82
+
83
+class Inputs:
84
+ def __init__(self, num_tokens: int, num_heads: int) -> None:
85
+ torch.manual_seed(0)
86
+ device = "cuda"
87
+ dim = num_heads * HEAD_DIM
88
+ num_slots = num_tokens + 8
89
+ self.num_heads = num_heads
90
+ self.num_tokens = num_tokens
91
+ self.mixed_qkv = torch.randn(num_tokens, 3 * dim, device=device, dtype=DTYPE)
92
+ self.conv_weights = torch.randn(
93
+ 3 * dim, CONV_WIDTH, device=device, dtype=torch.float32
94
+ )
95
+ self.decode_conv1d_weight = torch.stack(
96
+ [
97
+ self.conv_weights[i * dim : (i + 1) * dim].transpose(0, 1).contiguous()
98
+ for i in range(3)
99
+ ]
100
+ )
101
+ self.conv_state = torch.randn(
102
+ num_slots, CONV_WIDTH - 1, 3 * dim, device=device, dtype=DTYPE
103
+ )
104
+ self.recurrent_state = torch.randn(
105
+ num_slots, num_heads, HEAD_DIM, HEAD_DIM, device=device, dtype=torch.float32
106
+ )
107
+ self.g1 = torch.randn(
108
+ 1, num_tokens, num_heads, HEAD_DIM, device=device, dtype=DTYPE
109
+ )
110
+ self.g2 = torch.randn(
111
+ num_tokens, num_heads, HEAD_DIM, device=device, dtype=DTYPE
112
+ )
113
+ self.beta = torch.randn(1, num_tokens, num_heads, device=device, dtype=DTYPE)
114
+ self.A_log = torch.randn(num_heads, device=device, dtype=torch.float32)
115
+ self.dt_bias = torch.randn(dim, device=device, dtype=torch.float32)
116
+ self.norm_weight_bf16 = torch.ones(HEAD_DIM, device=device, dtype=DTYPE)
117
+ self.decode_norm_weight = self.norm_weight_bf16.float()
118
+ # Slots start at 1: slot 0 is NULL_BLOCK_ID, which the fused kernel
119
+ # treats as a padded row and skips, so timing it measures nothing.
120
+ self.state_indices = torch.arange(
121
+ 1, num_tokens + 1, device=device, dtype=torch.int32
122
+ )
123
+ self.conv_state_t = self.conv_state.transpose(-1, -2)
124
+ self.out = torch.empty(
125
+ 1, num_tokens, num_heads, HEAD_DIM, device=device, dtype=DTYPE
126
+ )
127
+ self.conv_out = torch.empty_like(self.mixed_qkv)
128
+
129
+ def state_bytes(self) -> int:
130
+ """Recurrent-state traffic, identical for both implementations."""
131
+ return self.num_tokens * self.num_heads * HEAD_DIM * HEAD_DIM * 4 * 2
132
+
133
+
134
+def _gated_rmsnorm(
135
+ x: torch.Tensor, gate: torch.Tensor, weight: torch.Tensor, eps: float
136
+) -> torch.Tensor:
137
+ x_float = x.float()
138
+ variance = x_float.pow(2).mean(dim=-1, keepdim=True)
139
+ normed = x_float * torch.rsqrt(variance + eps) * weight.float()
140
+ return (normed * torch.sigmoid(gate.float())).to(x.dtype)
141
+
142
+
143
+def _triton_gated_norm(inp: Inputs, core_attn_out: torch.Tensor) -> torch.Tensor:
144
+ from vllm.third_party.flash_linear_attention.ops.kda import rms_norm_gated
145
+
146
+ return rms_norm_gated(
147
+ core_attn_out,
148
+ inp.g2,
149
+ inp.norm_weight_bf16,
150
+ None,
151
+ activation="sigmoid",
152
+ eps=NORM_EPS,
153
+ )
154
+
155
+
156
+def triton_chain(inp: Inputs, fused_norm: bool = False) -> None:
157
+ from vllm.model_executor.layers.mamba.ops.causal_conv1d import (
158
+ causal_conv1d_update,
159
+ )
160
+ from vllm.models.kimi_k3.amd.ops.third_party.kda import (
161
+ fused_recurrent_kda_packed_decode,
162
+ )
163
+
164
+ causal_conv1d_update(
165
+ inp.mixed_qkv,
166
+ inp.conv_state_t,
167
+ inp.conv_weights,
168
+ None,
169
+ activation="silu",
170
+ conv_state_indices=inp.state_indices,
171
+ validate_data=False,
172
+ out=inp.conv_out,
173
+ )
174
+ core_attn_out, _ = fused_recurrent_kda_packed_decode(
175
+ mixed_qkv=inp.conv_out,
176
+ raw_g=inp.g1,
177
+ raw_beta=inp.beta,
178
+ A_log=inp.A_log,
179
+ dt_bias=inp.dt_bias,
180
+ lower_bound=GATE_LOWER_BOUND,
181
+ initial_state=inp.recurrent_state,
182
+ state_indices=inp.state_indices,
183
+ )
184
+ if fused_norm:
185
+ inp.out.copy_(_triton_gated_norm(inp, core_attn_out))
186
+ else:
187
+ inp.out.copy_(
188
+ _gated_rmsnorm(core_attn_out, inp.g2, inp.norm_weight_bf16, NORM_EPS)
189
+ )
190
+
191
+
192
+def fused(inp: Inputs) -> None:
193
+ from vllm import _custom_ops as ops
194
+
195
+ ops.fused_kda_decode(
196
+ x=inp.mixed_qkv,
197
+ weight=inp.decode_conv1d_weight,
198
+ bias=None,
199
+ conv_state=inp.conv_state_t,
200
+ raw_g=inp.g1,
201
+ raw_beta=inp.beta,
202
+ A_log=inp.A_log,
203
+ dt_bias=inp.dt_bias,
204
+ state_indices=inp.state_indices,
205
+ state=inp.recurrent_state,
206
+ out=inp.out,
207
+ lower_bound=GATE_LOWER_BOUND,
208
+ output_gate=inp.g2,
209
+ norm_weight=inp.decode_norm_weight,
210
+ norm_eps=NORM_EPS,
211
+ )
212
+
213
+
214
+def main() -> None:
215
+ parser = argparse.ArgumentParser(description=__doc__)
216
+ parser.add_argument("--tokens", type=int, nargs="+", default=[1, 8, 32, 64, 128])
217
+ parser.add_argument(
218
+ "--graph",
219
+ action="store_true",
220
+ help="time under CUDA-graph replay instead of eager dispatch",
221
+ )
222
+ parser.add_argument(
223
+ "--layers",
224
+ type=int,
225
+ default=1,
226
+ help=(
227
+ "with --graph, give each of N layers its own state buffers so the "
228
+ "recurrent state cannot stay resident in Infinity Cache. "
229
+ f"Use {NUM_KDA_LAYERS} for a production-shaped step (needs "
230
+ "~0.9 GB per layer at 128 tokens / 12 heads)."
231
+ ),
232
+ )
233
+ parser.add_argument(
234
+ "--heads",
235
+ type=int,
236
+ nargs="+",
237
+ default=[12],
238
+ help="KDA heads per rank (96 total / TP size)",
239
+ )
240
+ args = parser.parse_args()
241
+
242
+ if not hasattr(torch.ops._C, "fused_kda_decode"):
243
+ raise SystemExit("vLLM was built without the fused KDA decode kernel")
244
+
245
+ props = torch.cuda.get_device_properties(0)
246
+ bench = _bench_graph if args.graph else _bench
247
+ mode = "cuda-graph replay" if args.graph else "eager dispatch"
248
+ print(f"device: {props.name} ({props.gcnArchName}) timing: {mode}")
249
+ print(
250
+ f"{'heads':>6} {'tokens':>7} {'eager-norm':>11} {'triton-norm':>12} "
251
+ f"{'fused us':>9} {'speedup':>8} {'state TB/s':>11} {'saved ms/step':>14}"
252
+ )
253
+ for num_heads in args.heads:
254
+ for num_tokens in args.tokens:
255
+ if args.graph and args.layers > 1:
256
+ layers = [Inputs(num_tokens, num_heads) for _ in range(args.layers)]
257
+ eager_ms = _bench_graph_layers(
258
+ [functools.partial(triton_chain, i, False) for i in layers]
259
+ )
260
+ triton_ms = _bench_graph_layers(
261
+ [functools.partial(triton_chain, i, True) for i in layers]
262
+ )
263
+ fused_ms = _bench_graph_layers(
264
+ [functools.partial(fused, i) for i in layers]
265
+ )
266
+ inp = layers[0]
267
+ del layers
268
+ torch.accelerator.empty_cache()
269
+ else:
270
+ inp = Inputs(num_tokens, num_heads)
271
+ eager_ms = bench(functools.partial(triton_chain, inp, False))
272
+ triton_ms = bench(functools.partial(triton_chain, inp, True))
273
+ fused_ms = bench(functools.partial(fused, inp))
274
+ bandwidth = inp.state_bytes() * 1e3 / fused_ms / 1e12
275
+ saved = (triton_ms - fused_ms) * NUM_KDA_LAYERS
276
+ print(
277
+ f"{num_heads:>6} {num_tokens:>7} {eager_ms * 1e3:>11.2f} "
278
+ f"{triton_ms * 1e3:>12.2f} {fused_ms * 1e3:>9.2f} "
279
+ f"{triton_ms / fused_ms:>7.2f}x {bandwidth:>10.2f} {saved:>14.3f}"
280
+ )
281
+ print(
282
+ f"\n'eager-norm' is the chain as it runs today (FusedRMSNormGated falls "
283
+ f"back to ~10 eager ops when custom_ops are off);\n'triton-norm' uses the "
284
+ f"Triton rms_norm_gated kernel, which is the honest baseline for this "
285
+ f"fusion.\nspeedup and saved ms/step are against 'triton-norm', over "
286
+ f"{NUM_KDA_LAYERS} KDA layers per forward pass."
287
+ )
288
+
289
+
290
+if __name__ == "__main__":
291
+ main()
csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel_rocm.cu
new
+800
@@ -0,0 +1,800 @@
1
+/*
2
+ * SPDX-License-Identifier: Apache-2.0
3
+ * SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4
+ *
5
+ * ROCm port of the fused Kimi-K3 KDA decode kernel.
6
+ *
7
+ * Fuses, per (sequence, value head), the causal conv1d update over Q/K/V, the
8
+ * gated delta-rule recurrence, and the gated output RMSNorm -- the work the
9
+ * ROCm path otherwise spends three Triton launches and two copies on, in each
10
+ * of the 69 KDA layers.
11
+ *
12
+ * The recurrent state is [128, 128] fp32 per (sequence, head), read and written
13
+ * once per token, so the kernel is bound by that 128 KiB of HBM traffic and the
14
+ * shape is chosen to reach streaming bandwidth rather than to minimize FLOPs.
15
+ */
16
+
17
+#include <cstdint>
18
+#include <array>
19
+#include <optional>
20
+
21
+#include <hip/hip_runtime.h>
22
+#include <hip/hip_bf16.h>
23
+
24
+#include <torch/headeronly/core/ScalarType.h>
25
+
26
+#include "../torch_utils.h"
27
+
28
+namespace {
29
+
30
+using bf16_t = __hip_bfloat16;
31
+// HIP's float4 is a class type, which the non-temporal builtins reject.
32
+using f32x4 = float __attribute__((ext_vector_type(4)));
33
+
34
+constexpr int kDimK = 128;
35
+constexpr int kDimV = 128;
36
+constexpr int kKernelWidth = 4;
37
+constexpr int kConvStateWidth = kKernelWidth - 1;
38
+constexpr int kThreads = 256;
39
+// One state row is split across kLanes lanes, each holding kElems = 8 floats in
40
+// two float4s. 16 lanes is the widest group a pure-DPP reduction can span
41
+// (row_mirror is xor 15), and it keeps 8 loads per lane in flight.
42
+constexpr int kLanes = 16;
43
+constexpr int kElems = kDimK / kLanes;
44
+constexpr int kVec = kElems / 4;
45
+constexpr int kRows = kThreads / kLanes;
46
+constexpr int kIters = kDimV / kRows;
47
+constexpr int kGroups = kThreads / 16;
48
+// s_reduce layout: [3 * kGroups) cross-group partials, then the three results,
49
+// then the epilogue slot, then the per-row-group sums of squares.
50
+constexpr int kSumsqSlot = 3 * kGroups + 3;
51
+constexpr int kPartialBase = 3 * kGroups + 4;
52
+
53
+struct KdaDecodeStrides {
54
+ int64_t x_row;
55
+ int64_t beta_row;
56
+ int64_t onorm_row;
57
+ int64_t conv_slot;
58
+ int64_t state_slot;
59
+};
60
+
61
+__device__ __forceinline__ float bf16_load(const bf16_t* ptr, int64_t idx) {
62
+ return __bfloat162float(ptr[idx]);
63
+}
64
+
65
+__device__ __forceinline__ float bf16_load(const float* ptr, int64_t idx) {
66
+ return ptr[idx];
67
+}
68
+
69
+template <int kChannels>
70
+__device__ __forceinline__ float conv_weight_load(const float* ptr, int channel,
71
+ int width) {
72
+ return ptr[width * kChannels + channel];
73
+}
74
+
75
+__device__ __forceinline__ bf16_t bf16_store(float value) {
76
+ return __float2bfloat16(value);
77
+}
78
+
79
+__device__ __forceinline__ float sigmoid_fast(float x) {
80
+ return 1.0f / (1.0f + __expf(-x));
81
+}
82
+
83
+__device__ __forceinline__ float silu_fast(float x) {
84
+ return x * sigmoid_fast(x);
85
+}
86
+
87
+__device__ __forceinline__ float softplus_fast(float x) {
88
+ return x > 20.0f ? x : log1pf(__expf(x));
89
+}
90
+
91
+__device__ __forceinline__ f32x4 load_state(const float* p) {
92
+ return __builtin_nontemporal_load(reinterpret_cast<const f32x4*>(p));
93
+}
94
+
95
+__device__ __forceinline__ void store_state(float* p, f32x4 v) {
96
+ __builtin_nontemporal_store(v, reinterpret_cast<f32x4*>(p));
97
+}
98
+
99
+// DPP row permutations within a wavefront: quad_perm[1,0,3,2] is xor 1,
100
+// quad_perm[2,3,0,1] is xor 2, row_half_mirror is xor 7, row_mirror is xor 15.
101
+template <int kCtrl>
102
+__device__ __forceinline__ float dpp_add(float x) {
103
+ const int shuffled = __builtin_amdgcn_update_dpp(
104
+ 0, __builtin_bit_cast(int, x), kCtrl, 0xf, 0xf, /*bound_ctrl=*/true);
105
+ return x + __builtin_bit_cast(float, shuffled);
106
+}
107
+
108
+// Sum across each aligned group of 16 lanes; every lane ends with the total.
109
+__device__ __forceinline__ float row_reduce(float x) {
110
+ x = dpp_add<0xb1>(x);
111
+ x = dpp_add<0x4e>(x);
112
+ x = dpp_add<0x141>(x);
113
+ x = dpp_add<0x140>(x);
114
+ return x;
115
+}
116
+
117
+struct Sum3 {
118
+ float x, y, z;
119
+};
120
+
121
+// DPP within each 16-lane group, one LDS round trip across the kGroups groups.
122
+__device__ __forceinline__ Sum3 block_reduce_sum3(float x, float y, float z,
123
+ float* scratch) {
124
+ const int lane = threadIdx.x & 15;
125
+ const int grp = threadIdx.x / 16;
126
+ x = row_reduce(x);
127
+ y = row_reduce(y);
128
+ z = row_reduce(z);
129
+ if (lane == 0) {
130
+ scratch[grp] = x;
131
+ scratch[kGroups + grp] = y;
132
+ scratch[2 * kGroups + grp] = z;
133
+ }
134
+ __syncthreads();
135
+ if (threadIdx.x < kGroups) {
136
+ const float bx = row_reduce(scratch[lane]);
137
+ const float by = row_reduce(scratch[kGroups + lane]);
138
+ const float bz = row_reduce(scratch[2 * kGroups + lane]);
139
+ if (lane == 0) {
140
+ scratch[3 * kGroups] = bx;
141
+ scratch[3 * kGroups + 1] = by;
142
+ scratch[3 * kGroups + 2] = bz;
143
+ }
144
+ }
145
+ __syncthreads();
146
+ return {scratch[3 * kGroups], scratch[3 * kGroups + 1],
147
+ scratch[3 * kGroups + 2]};
148
+}
149
+
150
+template <bool kApplyOnorm, int kFixedHeads, bool kUpdateConvState,
151
+ bool kUseLowerBound, bool kApplyBetaSigmoid>
152
+__global__ __launch_bounds__(kThreads) void kda_decode_fusion_kernel(
153
+ const bf16_t* __restrict__ x_q, const bf16_t* __restrict__ x_k,
154
+ const bf16_t* __restrict__ x_v, const float* __restrict__ w_q_t,
155
+ const float* __restrict__ w_k_t, const float* __restrict__ w_v_t,
156
+ const float* __restrict__ bias_q, const float* __restrict__ bias_k,
157
+ const float* __restrict__ bias_v, bf16_t* __restrict__ cs_q,
158
+ bf16_t* __restrict__ cs_k, bf16_t* __restrict__ cs_v,
159
+ const float* __restrict__ a_log, const bf16_t* __restrict__ g,
160
+ const float* __restrict__ dt_bias, const bf16_t* __restrict__ beta,
161
+ const bf16_t* __restrict__ onorm_g, const float* __restrict__ onorm_weight,
162
+ const int* __restrict__ ssm_state_indices, float* __restrict__ state,
163
+ bf16_t* __restrict__ out, float lower_bound, float scale, float onorm_eps,
164
+ KdaDecodeStrides strides) {
165
+ const int tid = threadIdx.x;
166
+
167
+ // Heads are the fast grid axis so that adjacent workgroups walk adjacent
168
+ // 64 KiB state slices of the same slot.
169
+ const int i_hv = blockIdx.x;
170
+ const int i_n = blockIdx.y;
171
+ const int i_h = i_hv;
172
+ const int bos = i_n;
173
+ const int slot = ssm_state_indices == nullptr ? i_n : ssm_state_indices[i_n];
174
+
175
+ constexpr int kLocalDim = kFixedHeads * kDimK;
176
+ // gdn_attn.py fills the tail of a CUDA-graph decode batch with
177
+ // NULL_BLOCK_ID (0). Both kernels this one replaces skip it: the Triton
178
+ // recurrent kernel zeroes the output and returns, and causal_conv1d_update
179
+ // leaves the conv state alone. Without this, every padded row would read and
180
+ // rewrite slot 0's state for nothing. `slot` is block-uniform, so returning
181
+ // here is safe with respect to the __syncthreads() calls below.
182
+ if (slot <= 0) {
183
+ if (tid < kDimV) {
184
+ out[static_cast<int64_t>(i_n) * kLocalDim + i_hv * kDimV + tid] =
185
+ bf16_store(0.0f);
186
+ }
187
+ return;
188
+ }
189
+ constexpr int kPackedDim = 3 * kLocalDim;
190
+ const int hk_off = i_h * kDimK;
191
+ const int hv_off = i_hv * kDimV;
192
+ float* const state_for_slot = state + slot * strides.state_slot;
193
+ const int64_t conv_slot_offset = slot * strides.conv_slot;
194
+ bf16_t* const cs_q_for_slot = cs_q + conv_slot_offset;
195
+ bf16_t* const cs_k_for_slot = cs_k + conv_slot_offset;
196
+ bf16_t* const cs_v_for_slot = cs_v + conv_slot_offset;
197
+
198
+ __shared__ float s_q[kDimK];
199
+ __shared__ float s_k[kDimK];
200
+ __shared__ float s_decay[kDimK];
201
+ __shared__ float s_v[kDimV];
202
+ __shared__ float s_o[kDimV];
203
+ __shared__ float s_reduce[kPartialBase + kRows];
204
+ float pre_onorm_gate = 0.0f;
205
+ float pre_onorm_weight = 0.0f;
206
+
207
+ const int rlane = tid & (kLanes - 1);
208
+ const int rgroup = tid / kLanes;
209
+ float* const row_base =
210
+ state_for_slot + static_cast<int64_t>(hv_off) * kDimK + 4 * rlane;
211
+
212
+ // Nothing in the prologue feeds these addresses, so issue the whole state
213
+ // slice now and let it fly under the conv1d, the RMS reduction and the two
214
+ // barriers that follow.
215
+ f32x4 h_raw[kIters][kVec];
216
+#pragma unroll
217
+ for (int t = 0; t < kIters; ++t) {
218
+ const float* src =
219
+ row_base + static_cast<int64_t>(t * kRows + rgroup) * kDimK;
220
+#pragma unroll
221
+ for (int j = 0; j < kVec; ++j) {
222
+ h_raw[t][j] = load_state(src + j * 4 * kLanes);
223
+ }
224
+ }
225
+
226
+ // Q and K stay in registers: the RMS reduction below only ever reads each
227
+ // thread's own element, so it needs no barrier ahead of it.
228
+ float qv = 0.0f;
229
+ float kv = 0.0f;
230
+ if (tid < kDimK) {
231
+ const int k = tid;
232
+ const int hk = hk_off + k;
233
+ // A_log is uniform across the block; the scalar unit hoists this load.
234
+ const float exp_a = __expf(a_log[i_h]);
235
+
236
+ float q_acc = bias_q == nullptr ? 0.0f : bf16_load(bias_q, hk);
237
+ float k_acc = bias_k == nullptr ? 0.0f : bf16_load(bias_k, hk);
238
+ if constexpr (kUpdateConvState) {
239
+ bf16_t q_shift0 = __float2bfloat16(0.0f);
240
+ bf16_t q_shift1 = __float2bfloat16(0.0f);
241
+ bf16_t k_shift0 = __float2bfloat16(0.0f);
242
+ bf16_t k_shift1 = __float2bfloat16(0.0f);
243
+#pragma unroll
244
+ for (int w = 0; w < kConvStateWidth; ++w) {
245
+ const bf16_t q_state = cs_q_for_slot[hk + w * kPackedDim];
246
+ const bf16_t k_state = cs_k_for_slot[hk + w * kPackedDim];
247
+ q_acc += __bfloat162float(q_state) *
248
+ conv_weight_load<kLocalDim>(w_q_t, hk, w);
249
+ k_acc += __bfloat162float(k_state) *
250
+ conv_weight_load<kLocalDim>(w_k_t, hk, w);
251
+ if (w == 1) {
252
+ q_shift0 = q_state;
253
+ k_shift0 = k_state;
254
+ } else if (w == 2) {
255
+ q_shift1 = q_state;
256
+ k_shift1 = k_state;
257
+ }
258
+ }
259
+ const int64_t xq_idx = bos * strides.x_row + i_h * kDimK + k;
260
+ const bf16_t q_new = x_q[xq_idx];
261
+ const bf16_t k_new = x_k[xq_idx];
262
+ q_acc += __bfloat162float(q_new) *
263
+ conv_weight_load<kLocalDim>(w_q_t, hk, kKernelWidth - 1);
264
+ k_acc += __bfloat162float(k_new) *
265
+ conv_weight_load<kLocalDim>(w_k_t, hk, kKernelWidth - 1);
266
+
267
+ cs_q_for_slot[hk] = q_shift0;
268
+ cs_q_for_slot[hk + kPackedDim] = q_shift1;
269
+ cs_q_for_slot[hk + 2 * kPackedDim] = q_new;
270
+ cs_k_for_slot[hk] = k_shift0;
271
+ cs_k_for_slot[hk + kPackedDim] = k_shift1;
272
+ cs_k_for_slot[hk + 2 * kPackedDim] = k_new;
273
+ } else {
274
+#pragma unroll
275
+ for (int w = 0; w < kConvStateWidth; ++w) {
276
+ const int cs_idx = hk + w * kPackedDim;
277
+ q_acc += bf16_load(cs_q_for_slot, cs_idx) *
278
+ conv_weight_load<kLocalDim>(w_q_t, hk, w);
279
+ k_acc += bf16_load(cs_k_for_slot, cs_idx) *
280
+ conv_weight_load<kLocalDim>(w_k_t, hk, w);
281
+ }
282
+ q_acc += bf16_load(x_q, bos * strides.x_row + i_h * kDimK + k) *
283
+ conv_weight_load<kLocalDim>(w_q_t, hk, kKernelWidth - 1);
284
+ k_acc += bf16_load(x_k, bos * strides.x_row + i_h * kDimK + k) *
285
+ conv_weight_load<kLocalDim>(w_k_t, hk, kKernelWidth - 1);
286
+ }
287
+
288
+ qv = silu_fast(q_acc);
289
+ kv = silu_fast(k_acc);
290
+
291
+ const int64_t gate_idx = bos * kLocalDim + i_hv * kDimK + k;
292
+ const float g_raw = bf16_load(g, gate_idx) + dt_bias[hk];
293
+ if constexpr (kUseLowerBound) {
294
+ s_decay[k] = __expf(lower_bound * sigmoid_fast(exp_a * g_raw));
295
+ } else {
296
+ s_decay[k] = __expf(-exp_a * softplus_fast(g_raw));
297
+ }
298
+ }
299
+
300
+ if (tid >= kThreads - kDimV) {
301
+ const int v = tid - (kThreads - kDimV);
302
+ const int hvv = hv_off + v;
303
+
304
+ float v_acc = bias_v == nullptr ? 0.0f : bf16_load(bias_v, hvv);
305
+ if constexpr (kUpdateConvState) {
306
+ bf16_t v_shift0 = __float2bfloat16(0.0f);
307
+ bf16_t v_shift1 = __float2bfloat16(0.0f);
308
+#pragma unroll
309
+ for (int w = 0; w < kConvStateWidth; ++w) {
310
+ const bf16_t v_state = cs_v_for_slot[hvv + w * kPackedDim];
311
+ v_acc += __bfloat162float(v_state) *
312
+ conv_weight_load<kLocalDim>(w_v_t, hvv, w);
313
+ if (w == 1) {
314
+ v_shift0 = v_state;
315
+ } else if (w == 2) {
316
+ v_shift1 = v_state;
317
+ }
318
+ }
319
+ const int64_t xv_idx = bos * strides.x_row + i_hv * kDimV + v;
320
+ const bf16_t v_new = x_v[xv_idx];
321
+ v_acc += __bfloat162float(v_new) *
322
+ conv_weight_load<kLocalDim>(w_v_t, hvv, kKernelWidth - 1);
323
+ cs_v_for_slot[hvv] = v_shift0;
324
+ cs_v_for_slot[hvv + kPackedDim] = v_shift1;
325
+ cs_v_for_slot[hvv + 2 * kPackedDim] = v_new;
326
+ } else {
327
+#pragma unroll
328
+ for (int w = 0; w < kConvStateWidth; ++w) {
329
+ const int cs_idx = hvv + w * kPackedDim;
330
+ v_acc += bf16_load(cs_v_for_slot, cs_idx) *
331
+ conv_weight_load<kLocalDim>(w_v_t, hvv, w);
332
+ }
333
+ v_acc += bf16_load(x_v, bos * strides.x_row + i_hv * kDimV + v) *
334
+ conv_weight_load<kLocalDim>(w_v_t, hvv, kKernelWidth - 1);
335
+ }
336
+ s_v[v] = silu_fast(v_acc);
337
+
338
+ if constexpr (kApplyOnorm) {
339
+ const int64_t gate_idx = i_n * strides.onorm_row + i_hv * kDimV + v;
340
+ pre_onorm_gate = sigmoid_fast(bf16_load(onorm_g, gate_idx));
341
+ pre_onorm_weight = onorm_weight[v];
342
+ }
343
+ }
344
+
345
+ // Block-uniform address, so this is a scalar load.
346
+ const float beta_raw = bf16_load(beta, bos * strides.beta_row + i_hv);
347
+ const float beta_v = kApplyBetaSigmoid ? sigmoid_fast(beta_raw) : beta_raw;
348
+
349
+ // One reduction yields both RMS norms and the raw q.k that folds the rank-1
350
+ // state update into the output below.
351
+ const Sum3 sums = block_reduce_sum3(qv * qv, kv * kv, qv * kv, s_reduce);
352
+ const float rq = rsqrtf(sums.x + 1.0e-6f) * scale;
353
+ const float rk = rsqrtf(sums.y + 1.0e-6f);
354
+ const float kq = sums.z * rq * rk;
355
+ if (tid < kDimK) {
356
+ s_q[tid] = qv * rq;
357
+ s_k[tid] = kv * rk;
358
+ }
359
+ __syncthreads();
360
+
361
+ float r_q[kElems], r_k[kElems], r_decay[kElems];
362
+#pragma unroll
363
+ for (int j = 0; j < kVec; ++j) {
364
+ const int kb = 4 * rlane + j * 4 * kLanes;
365
+ const float4 q4 = *reinterpret_cast<const float4*>(s_q + kb);
366
+ const float4 k4 = *reinterpret_cast<const float4*>(s_k + kb);
367
+ const float4 d4 = *reinterpret_cast<const float4*>(s_decay + kb);
368
+ r_q[4 * j + 0] = q4.x;
369
+ r_q[4 * j + 1] = q4.y;
370
+ r_q[4 * j + 2] = q4.z;
371
+ r_q[4 * j + 3] = q4.w;
372
+ r_k[4 * j + 0] = k4.x;
373
+ r_k[4 * j + 1] = k4.y;
374
+ r_k[4 * j + 2] = k4.z;
375
+ r_k[4 * j + 3] = k4.w;
376
+ r_decay[4 * j + 0] = d4.x;
377
+ r_decay[4 * j + 1] = d4.y;
378
+ r_decay[4 * j + 2] = d4.z;
379
+ r_decay[4 * j + 3] = d4.w;
380
+ }
381
+
382
+ // Per row v, with h' = h * decay:
383
+ // u = h'.k vn = (v - u) * beta
384
+ // h'' = h' + k * vn o = h''.q = h'.q + vn * (k.q)
385
+ // Folding the rank-1 update into the output lets h'.k and h'.q share one
386
+ // reduction and keeps the state store off the second one's critical path.
387
+ float sumsq_local = 0.0f;
388
+#pragma unroll
389
+ for (int t = 0; t < kIters; ++t) {
390
+ float h[kElems];
391
+#pragma unroll
392
+ for (int j = 0; j < kVec; ++j) {
393
+#pragma unroll
394
+ for (int e = 0; e < 4; ++e) {
395
+ h[4 * j + e] = h_raw[t][j][e] * r_decay[4 * j + e];
396
+ }
397
+ }
398
+ // Four partial sums per dot product: at decode the grid rarely fills the
399
+ // wave slots, so the serial FMA chain is exposed latency, not throughput.
400
+ float part_k[4] = {0.0f, 0.0f, 0.0f, 0.0f};
401
+ float part_q[4] = {0.0f, 0.0f, 0.0f, 0.0f};
402
+#pragma unroll
403
+ for (int e = 0; e < kElems; ++e) {
404
+ part_k[e & 3] += h[e] * r_k[e];
405
+ part_q[e & 3] += h[e] * r_q[e];
406
+ }
407
+ const float dot_hk =
408
+ row_reduce((part_k[0] + part_k[1]) + (part_k[2] + part_k[3]));
409
+ const float dot_hq =
410
+ row_reduce((part_q[0] + part_q[1]) + (part_q[2] + part_q[3]));
411
+
412
+ const int v = t * kRows + rgroup;
413
+ const float v_new = (s_v[v] - dot_hk) * beta_v;
414
+ float* dst = row_base + static_cast<int64_t>(v) * kDimK;
415
+#pragma unroll
416
+ for (int j = 0; j < kVec; ++j) {
417
+ f32x4 updated;
418
+#pragma unroll
419
+ for (int e = 0; e < 4; ++e) {
420
+ updated[e] = h[4 * j + e] + r_k[4 * j + e] * v_new;
421
+ }
422
+ store_state(dst + j * 4 * kLanes, updated);
423
+ }
424
+ if (rlane == 0) {
425
+ const float o = dot_hq + v_new * kq;
426
+ s_o[v] = o;
427
+ sumsq_local += o * o;
428
+ }
429
+ }
430
+
431
+ if constexpr (kApplyOnorm) {
432
+ // Each row-group leader already holds the squares of every output it
433
+ // produced, so only the kRows group totals cross LDS.
434
+ if (rlane == 0) s_reduce[kPartialBase + rgroup] = sumsq_local;
435
+ __syncthreads();
436
+ if (tid < 16) {
437
+ float total = 0.0f;
438
+#pragma unroll
439
+ for (int i = 0; i < kRows / 16; ++i) {
440
+ total += s_reduce[kPartialBase + tid + i * 16];
441
+ }
442
+ total = row_reduce(total);
443
+ if (tid == 0) s_reduce[kSumsqSlot] = total;
444
+ }
445
+ __syncthreads();
446
+ const float rstd =
447
+ rsqrtf(s_reduce[kSumsqSlot] / static_cast<float>(kDimV) + onorm_eps);
448
+ if (tid >= kThreads - kDimV) {
449
+ const int v = tid - (kThreads - kDimV);
450
+ const int64_t out_idx = i_n * kLocalDim + i_hv * kDimV + v;
451
+ out[out_idx] =
452
+ bf16_store(s_o[v] * rstd * pre_onorm_weight * pre_onorm_gate);
453
+ }
454
+ } else {
455
+ __syncthreads();
456
+ if (tid < kDimV) {
457
+ const int64_t out_idx = i_n * kLocalDim + i_hv * kDimV + tid;
458
+ out[out_idx] = bf16_store(s_o[tid]);
459
+ }
460
+ }
461
+}
462
+
463
+template <int kHeads, bool kApplyOnorm, bool kUpdateConvState,
464
+ bool kUseLowerBound, bool kApplyBetaSigmoid>
465
+void launch_kda_decode_raw(
466
+ const void* x_q, const void* x_k, const void* x_v, const void* w_q_t,
467
+ const void* w_k_t, const void* w_v_t, const void* bias_q,
468
+ const void* bias_k, const void* bias_v, void* cs_q, void* cs_k, void* cs_v,
469
+ const float* a_log, const void* g, const float* dt_bias, const void* beta,
470
+ const void* onorm_g, const float* onorm_weight,
471
+ const int* ssm_state_indices, float* state, void* out, int B,
472
+ float lower_bound, float scale, float onorm_eps, KdaDecodeStrides strides,
473
+ hipStream_t stream) {
474
+ kda_decode_fusion_kernel<kApplyOnorm, kHeads, kUpdateConvState,
475
+ kUseLowerBound, kApplyBetaSigmoid>
476
+ <<<dim3(kHeads, B), dim3(kThreads), 0, stream>>>(
477
+ reinterpret_cast<const bf16_t*>(x_q),
478
+ reinterpret_cast<const bf16_t*>(x_k),
479
+ reinterpret_cast<const bf16_t*>(x_v),
480
+ reinterpret_cast<const float*>(w_q_t),
481
+ reinterpret_cast<const float*>(w_k_t),
482
+ reinterpret_cast<const float*>(w_v_t),
483
+ reinterpret_cast<const float*>(bias_q),
484
+ reinterpret_cast<const float*>(bias_k),
485
+ reinterpret_cast<const float*>(bias_v),
486
+ reinterpret_cast<bf16_t*>(cs_q), reinterpret_cast<bf16_t*>(cs_k),
487
+ reinterpret_cast<bf16_t*>(cs_v), a_log,
488
+ reinterpret_cast<const bf16_t*>(g), dt_bias,
489
+ reinterpret_cast<const bf16_t*>(beta),
490
+ reinterpret_cast<const bf16_t*>(onorm_g), onorm_weight,
491
+ ssm_state_indices, state, reinterpret_cast<bf16_t*>(out), lower_bound,
492
+ scale, onorm_eps, strides);
493
+}
494
+
495
+struct KdaDecodeLaunchParams {
496
+ const void* x_q;
497
+ const void* x_k;
498
+ const void* x_v;
499
+ const void* w_q_t;
500
+ const void* w_k_t;
501
+ const void* w_v_t;
502
+ const void* bias_q;
503
+ const void* bias_k;
504
+ const void* bias_v;
505
+ void* cs_q;
506
+ void* cs_k;
507
+ void* cs_v;
508
+ const float* a_log;
509
+ const void* g;
510
+ const float* dt_bias;
511
+ const void* beta;
512
+ const void* onorm_g;
513
+ const float* onorm_weight;
514
+ const int* ssm_state_indices;
515
+ float* state;
516
+ void* out;
517
+ int B;
518
+ int H;
519
+ bool update_conv_cache;
520
+ float lower_bound;
521
+ float scale;
522
+ float onorm_eps;
523
+ KdaDecodeStrides strides;
524
+ hipStream_t stream;
525
+};
526
+
527
+template <int kHeads, bool kApplyOnorm, bool kUseLowerBound,
528
+ bool kApplyBetaSigmoid>
529
+void dispatch_kda_decode_conv(const KdaDecodeLaunchParams& p) {
530
+#define LAUNCH_KDA_DECODE(UPDATE_CONV) \
531
+ launch_kda_decode_raw<kHeads, kApplyOnorm, UPDATE_CONV, kUseLowerBound, \
532
+ kApplyBetaSigmoid>( \
533
+ p.x_q, p.x_k, p.x_v, p.w_q_t, p.w_k_t, p.w_v_t, p.bias_q, p.bias_k, \
534
+ p.bias_v, p.cs_q, p.cs_k, p.cs_v, p.a_log, p.g, p.dt_bias, p.beta, \
535
+ p.onorm_g, p.onorm_weight, p.ssm_state_indices, p.state, p.out, p.B, \
536
+ p.lower_bound, p.scale, p.onorm_eps, p.strides, p.stream)
537
+ if (p.update_conv_cache) {
538
+ LAUNCH_KDA_DECODE(true);
539
+ } else {
540
+ LAUNCH_KDA_DECODE(false);
541
+ }
542
+#undef LAUNCH_KDA_DECODE
543
+}
544
+
545
+template <bool kApplyOnorm, bool kUseLowerBound, bool kApplyBetaSigmoid>
546
+void dispatch_kda_decode_heads(const KdaDecodeLaunchParams& p) {
547
+ switch (p.H) {
548
+ case 12:
549
+ dispatch_kda_decode_conv<12, kApplyOnorm, kUseLowerBound,
550
+ kApplyBetaSigmoid>(p);
551
+ break;
552
+ case 24:
553
+ dispatch_kda_decode_conv<24, kApplyOnorm, kUseLowerBound,
554
+ kApplyBetaSigmoid>(p);
555
+ break;
556
+ case 48:
557
+ dispatch_kda_decode_conv<48, kApplyOnorm, kUseLowerBound,
558
+ kApplyBetaSigmoid>(p);
559
+ break;
560
+ case 96:
561
+ dispatch_kda_decode_conv<96, kApplyOnorm, kUseLowerBound,
562
+ kApplyBetaSigmoid>(p);
563
+ break;
564
+ default:
565
+ STD_TORCH_CHECK(false, "Unsupported number of heads: ", p.H);
566
+ }
567
+}
568
+
569
+template <bool kApplyOnorm, bool kUseLowerBound>
570
+void dispatch_kda_decode_beta(const KdaDecodeLaunchParams& p,
571
+ bool apply_beta_sigmoid) {
572
+ if (apply_beta_sigmoid) {
573
+ dispatch_kda_decode_heads<kApplyOnorm, kUseLowerBound, true>(p);
574
+ } else {
575
+ dispatch_kda_decode_heads<kApplyOnorm, kUseLowerBound, false>(p);
576
+ }
577
+}
578
+
579
+template <bool kApplyOnorm>
580
+void dispatch_kda_decode_decay(const KdaDecodeLaunchParams& p,
581
+ bool use_lower_bound, bool apply_beta_sigmoid) {
582
+ if (use_lower_bound) {
583
+ dispatch_kda_decode_beta<kApplyOnorm, true>(p, apply_beta_sigmoid);
584
+ } else {
585
+ dispatch_kda_decode_beta<kApplyOnorm, false>(p, apply_beta_sigmoid);
586
+ }
587
+}
588
+
589
+void dispatch_kda_decode_features(const KdaDecodeLaunchParams& p,
590
+ bool apply_onorm, bool use_lower_bound,
591
+ bool apply_beta_sigmoid) {
592
+ if (apply_onorm) {
593
+ dispatch_kda_decode_decay<true>(p, use_lower_bound, apply_beta_sigmoid);
594
+ } else {
595
+ dispatch_kda_decode_decay<false>(p, use_lower_bound, apply_beta_sigmoid);
596
+ }
597
+}
598
+
599
+} // namespace
600
+
601
+void fused_kda_decode(
602
+ torch::stable::Tensor const& x, torch::stable::Tensor const& weight,
603
+ std::optional<torch::stable::Tensor> bias,
604
+ torch::stable::Tensor& conv_state, torch::stable::Tensor const& raw_g,
605
+ torch::stable::Tensor const& raw_beta, torch::stable::Tensor const& a_log,
606
+ torch::stable::Tensor const& dt_bias,
607
+ torch::stable::Tensor const& state_indices, torch::stable::Tensor& state,
608
+ torch::stable::Tensor& out, std::optional<double> lower_bound,
609
+ std::optional<torch::stable::Tensor> output_gate,
610
+ std::optional<torch::stable::Tensor> norm_weight, double norm_eps) {
611
+ using torch::headeronly::ScalarType;
612
+ constexpr int kHeadDim = 128;
613
+ constexpr int kConvWidth = 4;
614
+
615
+ STD_TORCH_CHECK(x.is_cuda() && x.scalar_type() == ScalarType::BFloat16,
616
+ "x must be a GPU bfloat16 tensor");
617
+ STD_TORCH_CHECK(weight.is_cuda() && weight.scalar_type() == ScalarType::Float,
618
+ "weight must be a GPU float32 tensor");
619
+ STD_TORCH_CHECK(
620
+ conv_state.is_cuda() && conv_state.scalar_type() == ScalarType::BFloat16,
621
+ "conv_state must be a GPU bfloat16 tensor");
622
+ STD_TORCH_CHECK(
623
+ raw_g.is_cuda() && raw_g.scalar_type() == ScalarType::BFloat16,
624
+ "raw_g must be a GPU bfloat16 tensor");
625
+ STD_TORCH_CHECK(
626
+ raw_beta.is_cuda() && raw_beta.scalar_type() == ScalarType::BFloat16,
627
+ "raw_beta must be a GPU bfloat16 tensor");
628
+ STD_TORCH_CHECK(a_log.is_cuda() && a_log.scalar_type() == ScalarType::Float,
629
+ "A_log must be a GPU float32 tensor");
630
+ STD_TORCH_CHECK(
631
+ dt_bias.is_cuda() && dt_bias.scalar_type() == ScalarType::Float,
632
+ "dt_bias must be a GPU float32 tensor");
633
+ STD_TORCH_CHECK(state.is_cuda() && state.scalar_type() == ScalarType::Float,
634
+ "state must be a GPU float32 tensor");
635
+ STD_TORCH_CHECK(out.is_cuda() && out.scalar_type() == ScalarType::BFloat16,
636
+ "out must be a GPU bfloat16 tensor");
637
+ STD_TORCH_CHECK(
638
+ state_indices.is_cuda() && state_indices.scalar_type() == ScalarType::Int,
639
+ "state_indices must be a GPU int32 tensor");
640
+
641
+ STD_TORCH_CHECK(x.dim() == 2, "x must have shape [B, 3 * H * 128]");
642
+ int const batch_size = static_cast<int>(x.size(0));
643
+ int64_t const qkv_width = x.size(1);
644
+ STD_TORCH_CHECK(qkv_width % (3 * kHeadDim) == 0,
645
+ "x must have shape [B, 3 * H * 128]");
646
+ int64_t const num_heads = qkv_width / (3 * kHeadDim);
647
+ STD_TORCH_CHECK(
648
+ num_heads == 12 || num_heads == 24 || num_heads == 48 || num_heads == 96,
649
+ "H must be 12, 24, 48, or 96, got ", num_heads);
650
+ STD_TORCH_CHECK(batch_size > 0,
651
+ "KDA decode fusion requires at least one row");
652
+ // Rows are the y grid axis so that heads, which are contiguous in the state,
653
+ // vary fastest across workgroups.
654
+ STD_TORCH_CHECK(batch_size <= 65535, "KDA decode fusion supports at most ",
655
+ 65535, " rows, got ", batch_size);
656
+ int const dim = num_heads * kHeadDim;
657
+
658
+ STD_TORCH_CHECK(weight.dim() == 3 && weight.is_contiguous() &&
659
+ weight.size(0) == 3 && weight.size(1) == kConvWidth &&
660
+ weight.size(2) == dim,
661
+ "weight must have shape [3, 4, H * 128]");
662
+ STD_TORCH_CHECK(conv_state.dim() == 3 && conv_state.size(1) == 3 * dim &&
663
+ conv_state.size(2) == kConvWidth - 1,
664
+ "conv_state must have shape [slots, 3 * H * 128, 3]");
665
+ STD_TORCH_CHECK(raw_g.dim() == 4 && raw_g.size(0) == 1 &&
666
+ raw_g.size(1) == batch_size &&
667
+ raw_g.size(2) == num_heads && raw_g.size(3) == kHeadDim,
668
+ "raw_g must have shape [1, B, H, 128]");
669
+ STD_TORCH_CHECK(raw_beta.dim() == 3 && raw_beta.size(0) == 1 &&
670
+ raw_beta.size(1) == batch_size &&
671
+ raw_beta.size(2) == num_heads,
672
+ "raw_beta must have shape [1, B, H]");
673
+ STD_TORCH_CHECK(a_log.is_contiguous() && a_log.numel() == num_heads,
674
+ "A_log must be contiguous with H elements");
675
+ STD_TORCH_CHECK(dt_bias.is_contiguous() && dt_bias.numel() == dim,
676
+ "dt_bias must be contiguous with H * 128 elements");
677
+ STD_TORCH_CHECK(
678
+ state_indices.is_contiguous() && state_indices.numel() == batch_size,
679
+ "state_indices must be contiguous with B elements");
680
+ STD_TORCH_CHECK(state.dim() == 4 && state.size(1) == num_heads &&
681
+ state.size(2) == kHeadDim && state.size(3) == kHeadDim,
682
+ "state must have shape [slots, H, 128, 128]");
683
+ STD_TORCH_CHECK(out.dim() == 4 && out.size(0) == 1 &&
684
+ out.size(1) == batch_size && out.size(2) == num_heads &&
685
+ out.size(3) == kHeadDim,
686
+ "out must have shape [1, B, H, 128]");
687
+ STD_TORCH_CHECK(x.stride(1) == 1,
688
+ "x must be contiguous in its channel dimension");
689
+ STD_TORCH_CHECK(conv_state.stride(0) >= 3 * dim * (kConvWidth - 1) &&
690
+ conv_state.stride(1) == 1 &&
691
+ conv_state.stride(2) == 3 * dim,
692
+ "conv_state must use the SD cache layout");
693
+ STD_TORCH_CHECK(state.stride(0) >= num_heads * kHeadDim * kHeadDim &&
694
+ state.stride(1) == kHeadDim * kHeadDim &&
695
+ state.stride(2) == kHeadDim && state.stride(3) == 1,
696
+ "state must have contiguous [H, 128, 128] slot contents");
697
+ STD_TORCH_CHECK(raw_g.is_contiguous(), "raw_g must be contiguous");
698
+ STD_TORCH_CHECK(raw_beta.stride(2) == 1,
699
+ "raw_beta must be contiguous in its head dimension");
700
+ STD_TORCH_CHECK(out.is_contiguous(), "out must be contiguous");
701
+
702
+ bool const apply_onorm = output_gate.has_value();
703
+ STD_TORCH_CHECK(apply_onorm == norm_weight.has_value(),
704
+ "output_gate and norm_weight must be provided together");
705
+ void const* output_gate_ptr = nullptr;
706
+ float const* norm_weight_ptr = nullptr;
707
+ int64_t output_gate_row_stride = 0;
708
+ if (apply_onorm) {
709
+ STD_TORCH_CHECK(output_gate->is_cuda() &&
710
+ output_gate->scalar_type() == ScalarType::BFloat16,
711
+ "output_gate must be a GPU bfloat16 tensor");
712
+ bool const gate_is_3d =
713
+ output_gate->dim() == 3 && output_gate->size(0) == batch_size &&
714
+ output_gate->size(1) == num_heads && output_gate->size(2) == kHeadDim;
715
+ bool const gate_is_4d =
716
+ output_gate->dim() == 4 && output_gate->size(0) == 1 &&
717
+ output_gate->size(1) == batch_size &&
718
+ output_gate->size(2) == num_heads && output_gate->size(3) == kHeadDim;
719
+ STD_TORCH_CHECK(gate_is_3d || gate_is_4d,
720
+ "output_gate must have shape [B, H, 128] or "
721
+ "[1, B, H, 128]");
722
+ int const row_dim = gate_is_3d ? 0 : 1;
723
+ STD_TORCH_CHECK(output_gate->stride(output_gate->dim() - 1) == 1,
724
+ "output_gate must be contiguous in its last dimension");
725
+ STD_TORCH_CHECK(output_gate->stride(row_dim + 1) == kHeadDim,
726
+ "output_gate must have contiguous head rows");
727
+ STD_TORCH_CHECK(norm_weight->is_cuda() &&
728
+ norm_weight->scalar_type() == ScalarType::Float,
729
+ "norm_weight must be a GPU float32 tensor");
730
+ STD_TORCH_CHECK(
731
+ norm_weight->is_contiguous() && norm_weight->numel() == kHeadDim,
732
+ "norm_weight must be contiguous with 128 elements");
733
+ STD_TORCH_CHECK(norm_eps >= 0.0, "norm_eps must be non-negative");
734
+ output_gate_ptr = output_gate->data_ptr();
735
+ norm_weight_ptr = static_cast<float const*>(norm_weight->data_ptr());
736
+ output_gate_row_stride = output_gate->stride(row_dim);
737
+ }
738
+
739
+ void const* bias_ptr = nullptr;
740
+ if (bias.has_value()) {
741
+ STD_TORCH_CHECK(bias->is_cuda() && bias->scalar_type() == ScalarType::Float,
742
+ "bias must be a GPU float32 tensor");
743
+ STD_TORCH_CHECK(bias->is_contiguous() && bias->numel() == 3 * dim,
744
+ "bias must be contiguous with 3 * H * 128 elements");
745
+ bias_ptr = bias->data_ptr();
746
+ }
747
+
748
+ auto const* x_ptr = static_cast<char const*>(x.data_ptr());
749
+ auto const* weight_ptr = static_cast<char const*>(weight.data_ptr());
750
+ auto* conv_ptr = static_cast<char*>(conv_state.data_ptr());
751
+ auto const* bias_bytes = static_cast<char const*>(bias_ptr);
752
+ int64_t const segment_bytes = dim * sizeof(bf16_t);
753
+ int64_t const weight_segment_bytes =
754
+ dim * kConvWidth * static_cast<int64_t>(sizeof(float));
755
+ int64_t const conv_segment_bytes =
756
+ dim * conv_state.stride(1) * sizeof(bf16_t);
757
+ int64_t const bias_segment_bytes = dim * sizeof(float);
758
+ bool const use_lower_bound = lower_bound.has_value();
759
+ float const lower_bound_value =
760
+ use_lower_bound ? static_cast<float>(*lower_bound) : 0.0f;
761
+
762
+ torch::stable::accelerator::DeviceGuard const device_guard(
763
+ x.get_device_index());
764
+ KdaDecodeLaunchParams const params{
765
+ x_ptr,
766
+ x_ptr + segment_bytes,
767
+ x_ptr + 2 * segment_bytes,
768
+ weight_ptr,
769
+ weight_ptr + weight_segment_bytes,
770
+ weight_ptr + 2 * weight_segment_bytes,
771
+ bias_bytes,
772
+ bias_bytes == nullptr ? nullptr : bias_bytes + bias_segment_bytes,
773
+ bias_bytes == nullptr ? nullptr : bias_bytes + 2 * bias_segment_bytes,
774
+ conv_ptr,
775
+ conv_ptr + conv_segment_bytes,
776
+ conv_ptr + 2 * conv_segment_bytes,
777
+ static_cast<float const*>(a_log.data_ptr()),
778
+ raw_g.data_ptr(),
779
+ static_cast<float const*>(dt_bias.data_ptr()),
780
+ raw_beta.data_ptr(),
781
+ output_gate_ptr,
782
+ norm_weight_ptr,
783
+ static_cast<int const*>(state_indices.data_ptr()),
784
+ static_cast<float*>(state.data_ptr()),
785
+ out.data_ptr(),
786
+ batch_size,
787
+ static_cast<int>(num_heads),
788
+ true,
789
+ lower_bound_value,
790
+ 0.08838834764831845f,
791
+ static_cast<float>(norm_eps),
792
+ KdaDecodeStrides{x.stride(0), raw_beta.stride(1), output_gate_row_stride,
793
+ conv_state.stride(0), state.stride(0)},
794
+ get_current_cuda_stream(x.get_device_index())};
795
+ dispatch_kda_decode_features(params, apply_onorm, use_lower_bound, true);
796
+ hipError_t const error = hipGetLastError();
797
+ STD_TORCH_CHECK(
798
+ error == hipSuccess,
799
+ "Kimi K3 KDA decode kernel launch failed: ", hipGetErrorString(error));
800
+}
tests/models/kimi_k3/test_amd_kda_decode.py
new
+291
@@ -0,0 +1,291 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""The ROCm fused KDA decode kernel must match the Triton chain it replaces.
4
+
5
+The fused kernel folds the packed causal conv1d update, the gated delta-rule
6
+recurrence and the gated output RMSNorm into one launch, and updates both the
7
+conv state and the recurrent state in place. Every one of those outputs is
8
+compared against the three-kernel path the AMD layer falls back to.
9
+"""
10
+
11
+import pytest
12
+import torch
13
+
14
+from vllm.platforms import current_platform
15
+
16
+
17
+def _on_gfx950() -> bool:
18
+ if not current_platform.is_rocm():
19
+ return False
20
+ from vllm.platforms.rocm import on_gfx950
21
+
22
+ return on_gfx950()
23
+
24
+
25
+pytestmark = pytest.mark.skipif(
26
+ not _on_gfx950(),
27
+ reason="The fused KDA decode kernel is only built for gfx950",
28
+)
29
+
30
+# Kimi-K3 KDA: 96 heads x 128, conv width 4, gate_lower_bound -5.0.
31
+HEAD_DIM = 128
32
+CONV_WIDTH = 4
33
+GATE_LOWER_BOUND = -5.0
34
+NORM_EPS = 1e-5
35
+DTYPE = torch.bfloat16
36
+
37
+
38
+def _requires_kernel() -> None:
39
+ if not hasattr(torch.ops._C, "fused_kda_decode"):
40
+ pytest.skip("vLLM was built without the fused KDA decode kernel")
41
+
42
+
43
+class KdaDecodeInputs:
44
+ """One decode step of a Kimi-K3 KDA layer, in the layer's own layout."""
45
+
46
+ def __init__(
47
+ self,
48
+ num_tokens: int,
49
+ num_heads: int,
50
+ num_slots: int,
51
+ seed: int = 0,
52
+ ) -> None:
53
+ torch.manual_seed(seed)
54
+ device = "cuda"
55
+ dim = num_heads * HEAD_DIM
56
+ self.num_heads = num_heads
57
+ self.dim = dim
58
+
59
+ self.mixed_qkv = (
60
+ torch.randn(num_tokens, 3 * dim, device=device, dtype=DTYPE) * 0.5
61
+ )
62
+ # conv1d weight as the layer holds it: [3 * dim, width] fp32.
63
+ self.conv_weights = (
64
+ torch.randn(3 * dim, CONV_WIDTH, device=device, dtype=torch.float32) * 0.3
65
+ )
66
+ # Width-major fp32 copy the fused kernel indexes: [3, width, dim].
67
+ self.decode_conv1d_weight = torch.stack(
68
+ [
69
+ self.conv_weights[i * dim : (i + 1) * dim].transpose(0, 1).contiguous()
70
+ for i in range(3)
71
+ ]
72
+ )
73
+ # SD cache layout: [slots, width - 1, 3 * dim]; the layer transposes it.
74
+ self.conv_state = (
75
+ torch.randn(num_slots, CONV_WIDTH - 1, 3 * dim, device=device, dtype=DTYPE)
76
+ * 0.5
77
+ )
78
+ self.recurrent_state = (
79
+ torch.randn(
80
+ num_slots,
81
+ num_heads,
82
+ HEAD_DIM,
83
+ HEAD_DIM,
84
+ device=device,
85
+ dtype=torch.float32,
86
+ )
87
+ * 0.1
88
+ )
89
+ self.g1 = (
90
+ torch.randn(1, num_tokens, num_heads, HEAD_DIM, device=device, dtype=DTYPE)
91
+ * 0.5
92
+ )
93
+ self.g2 = (
94
+ torch.randn(num_tokens, num_heads, HEAD_DIM, device=device, dtype=DTYPE)
95
+ * 0.5
96
+ )
97
+ self.beta = torch.randn(1, num_tokens, num_heads, device=device, dtype=DTYPE)
98
+ self.A_log = torch.randn(num_heads, device=device, dtype=torch.float32) * 0.5
99
+ self.dt_bias = torch.randn(dim, device=device, dtype=torch.float32) * 0.1
100
+ self.norm_weight_bf16 = 1 + 0.1 * torch.randn(
101
+ HEAD_DIM, device=device, dtype=DTYPE
102
+ )
103
+ self.decode_norm_weight = self.norm_weight_bf16.float()
104
+ # Distinct, shuffled slots: the kernel must honour the indirection.
105
+ # Slot 0 is NULL_BLOCK_ID and never allocated for live state, and the
106
+ # Triton reference short-circuits it, so draw from [1, num_slots).
107
+ assert num_slots > num_tokens, "need a spare slot to detect stray writes"
108
+ self.state_indices = (
109
+ torch.randperm(num_slots - 1, device=device)[:num_tokens] + 1
110
+ ).to(torch.int32)
111
+
112
+ def conv_state_view(self, state: torch.Tensor) -> torch.Tensor:
113
+ return state.transpose(-1, -2)
114
+
115
+
116
+def _gated_rmsnorm(
117
+ x: torch.Tensor, gate: torch.Tensor, weight: torch.Tensor, eps: float
118
+) -> torch.Tensor:
119
+ x_float = x.float()
120
+ variance = x_float.pow(2).mean(dim=-1, keepdim=True)
121
+ normed = x_float * torch.rsqrt(variance + eps) * weight.float()
122
+ return (normed * torch.sigmoid(gate.float())).to(x.dtype)
123
+
124
+
125
+def _run_triton_chain(
126
+ inp: KdaDecodeInputs,
127
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
128
+ """conv1d update -> recurrent decode -> gated norm, as the layer runs it."""
129
+ from vllm.model_executor.layers.mamba.ops.causal_conv1d import (
130
+ causal_conv1d_update,
131
+ )
132
+ from vllm.models.kimi_k3.amd.ops.third_party.kda import (
133
+ fused_recurrent_kda_packed_decode,
134
+ )
135
+
136
+ conv_state = inp.conv_state.clone()
137
+ recurrent_state = inp.recurrent_state.clone()
138
+ conv_out = torch.empty_like(inp.mixed_qkv)
139
+ causal_conv1d_update(
140
+ inp.mixed_qkv,
141
+ inp.conv_state_view(conv_state),
142
+ inp.conv_weights,
143
+ None,
144
+ activation="silu",
145
+ conv_state_indices=inp.state_indices,
146
+ validate_data=True,
147
+ out=conv_out,
148
+ )
149
+ core_attn_out, _ = fused_recurrent_kda_packed_decode(
150
+ mixed_qkv=conv_out,
151
+ raw_g=inp.g1,
152
+ raw_beta=inp.beta,
153
+ A_log=inp.A_log,
154
+ dt_bias=inp.dt_bias,
155
+ lower_bound=GATE_LOWER_BOUND,
156
+ initial_state=recurrent_state,
157
+ state_indices=inp.state_indices,
158
+ )
159
+ out = _gated_rmsnorm(core_attn_out, inp.g2, inp.norm_weight_bf16, NORM_EPS)
160
+ return out, conv_state, recurrent_state
161
+
162
+
163
+def _run_fused(
164
+ inp: KdaDecodeInputs,
165
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
166
+ from vllm import _custom_ops as ops
167
+
168
+ conv_state = inp.conv_state.clone()
169
+ recurrent_state = inp.recurrent_state.clone()
170
+ num_tokens = inp.mixed_qkv.shape[0]
171
+ out = torch.empty(
172
+ 1,
173
+ num_tokens,
174
+ inp.num_heads,
175
+ HEAD_DIM,
176
+ device=inp.mixed_qkv.device,
177
+ dtype=DTYPE,
178
+ )
179
+ ops.fused_kda_decode(
180
+ x=inp.mixed_qkv,
181
+ weight=inp.decode_conv1d_weight,
182
+ bias=None,
183
+ conv_state=inp.conv_state_view(conv_state),
184
+ raw_g=inp.g1,
185
+ raw_beta=inp.beta,
186
+ A_log=inp.A_log,
187
+ dt_bias=inp.dt_bias,
188
+ state_indices=inp.state_indices,
189
+ state=recurrent_state,
190
+ out=out,
191
+ lower_bound=GATE_LOWER_BOUND,
192
+ output_gate=inp.g2,
193
+ norm_weight=inp.decode_norm_weight,
194
+ norm_eps=NORM_EPS,
195
+ )
196
+ return out, conv_state, recurrent_state
197
+
198
+
199
+@pytest.mark.parametrize("num_heads", [12, 24, 96])
200
+@pytest.mark.parametrize("num_tokens", [1, 7, 128])
201
+@torch.inference_mode()
202
+def test_fused_kda_decode_matches_triton_chain(num_heads: int, num_tokens: int) -> None:
203
+ _requires_kernel()
204
+ inp = KdaDecodeInputs(num_tokens, num_heads, num_slots=max(num_tokens, 4) + 3)
205
+
206
+ expected_out, expected_conv, expected_state = _run_triton_chain(inp)
207
+ actual_out, actual_conv, actual_state = _run_fused(inp)
208
+
209
+ # The fused kernel keeps the recurrent output in fp32 through the norm,
210
+ # while the Triton chain rounds it to BF16 in between.
211
+ torch.testing.assert_close(actual_out, expected_out, atol=3e-2, rtol=3e-2)
212
+ # The conv state is a pure shift-and-append: it must be bit-exact.
213
+ torch.testing.assert_close(actual_conv, expected_conv, atol=0, rtol=0)
214
+ torch.testing.assert_close(actual_state, expected_state, atol=2e-3, rtol=2e-3)
215
+
216
+
217
+@torch.inference_mode()
218
+def test_fused_kda_decode_leaves_untouched_slots_alone() -> None:
219
+ """Only the slots named by state_indices may be written."""
220
+ _requires_kernel()
221
+ num_tokens, num_heads, num_slots = 4, 12, 9
222
+ inp = KdaDecodeInputs(num_tokens, num_heads, num_slots, seed=3)
223
+
224
+ _, actual_conv, actual_state = _run_fused(inp)
225
+
226
+ touched = set(inp.state_indices.tolist())
227
+ untouched = [slot for slot in range(num_slots) if slot not in touched]
228
+ assert untouched, "test needs at least one unused slot"
229
+ torch.testing.assert_close(
230
+ actual_conv[untouched], inp.conv_state[untouched], atol=0, rtol=0
231
+ )
232
+ torch.testing.assert_close(
233
+ actual_state[untouched], inp.recurrent_state[untouched], atol=0, rtol=0
234
+ )
235
+
236
+
237
+@torch.inference_mode()
238
+def test_fused_kda_decode_without_output_norm() -> None:
239
+ """Omitting the gate/norm pair returns the raw recurrent output."""
240
+ _requires_kernel()
241
+ from vllm import _custom_ops as ops
242
+
243
+ inp = KdaDecodeInputs(5, 12, num_slots=8, seed=7)
244
+ _, _, expected_state = _run_triton_chain(inp)
245
+
246
+ conv_state = inp.conv_state.clone()
247
+ recurrent_state = inp.recurrent_state.clone()
248
+ out = torch.empty(1, 5, inp.num_heads, HEAD_DIM, device="cuda", dtype=DTYPE)
249
+ ops.fused_kda_decode(
250
+ x=inp.mixed_qkv,
251
+ weight=inp.decode_conv1d_weight,
252
+ bias=None,
253
+ conv_state=inp.conv_state_view(conv_state),
254
+ raw_g=inp.g1,
255
+ raw_beta=inp.beta,
256
+ A_log=inp.A_log,
257
+ dt_bias=inp.dt_bias,
258
+ state_indices=inp.state_indices,
259
+ state=recurrent_state,
260
+ out=out,
261
+ lower_bound=GATE_LOWER_BOUND,
262
+ )
263
+
264
+ torch.testing.assert_close(recurrent_state, expected_state, atol=2e-3, rtol=2e-3)
265
+ assert torch.isfinite(out).all()
266
+
267
+
268
+@torch.inference_mode()
269
+def test_fused_kda_decode_skips_null_block_padding() -> None:
270
+ """A CUDA-graph decode batch is padded with NULL_BLOCK_ID (0).
271
+
272
+ gdn_attn.py fills the tail of ``non_spec_state_indices_tensor`` with 0, and
273
+ the Triton chain zeroes those rows' output while leaving slot 0 alone. The
274
+ fused kernel must do the same rather than read-modify-write slot 0's state
275
+ once per padded row.
276
+ """
277
+ _requires_kernel()
278
+ num_real, num_padded, num_heads = 3, 5, 12
279
+ inp = KdaDecodeInputs(num_real + num_padded, num_heads, num_slots=12, seed=11)
280
+ inp.state_indices[num_real:] = 0
281
+
282
+ expected_out, expected_conv, expected_state = _run_triton_chain(inp)
283
+ actual_out, actual_conv, actual_state = _run_fused(inp)
284
+
285
+ torch.testing.assert_close(actual_out, expected_out, atol=3e-2, rtol=3e-2)
286
+ assert not actual_out[0, num_real:].any(), "padded rows must produce zeros"
287
+ # Slot 0 must be untouched by both paths.
288
+ torch.testing.assert_close(actual_conv[0], inp.conv_state[0], atol=0, rtol=0)
289
+ torch.testing.assert_close(actual_state[0], inp.recurrent_state[0], atol=0, rtol=0)
290
+ torch.testing.assert_close(actual_conv, expected_conv, atol=0, rtol=0)
291
+ torch.testing.assert_close(actual_state, expected_state, atol=2e-3, rtol=2e-3)
vllm/models/kimi_k3/amd/kda.py
+89
-9
@@ -5,10 +5,12 @@ import torch
5
from einops import rearrange
6
from torch import nn
7
8
+from vllm import _custom_ops as ops
9
from vllm.compilation.breakable_cudagraph import eager_break_during_capture
10
from vllm.config import VllmConfig
11
from vllm.distributed import divide
12
from vllm.forward_context import get_forward_context
13
+from vllm.logger import init_logger
14
from vllm.model_executor.layers.linear import (
15
ColumnParallelLinear,
16
RowParallelLinear,
@@ -38,6 +40,11 @@ from vllm.model_executor.layers.mamba.ops.gather_initial_states import (
40
)
41
from vllm.model_executor.model_loader.weight_utils import sharded_weight_loader
42
from vllm.model_executor.utils import set_weight_attrs
43
+from vllm.models.kimi_k3.amd.ops.kda_decode import (
44
+ is_fused_kda_decode_supported,
45
+ make_decode_conv1d_weight_loader,
46
+ make_decode_norm_weight_loader,
47
+)
48
from vllm.models.kimi_k3.amd.ops.third_party.kda import (
49
chunk_kda_with_fused_gate,
50
fused_recurrent_kda,
@@ -47,6 +54,8 @@ from vllm.third_party.flash_linear_attention.ops.kda import FusedRMSNormGated
54
from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
55
from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadata
56
57
+logger = init_logger(__name__)
58
+
59
60
class KimiK3DeltaAttention(GatedDeltaNetAttention):
61
def get_state_dtype(self) -> tuple[torch.dtype, torch.dtype]:
@@ -137,16 +146,44 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
146
)
147
self.conv1d.weight.data = self.conv1d.weight.data.unsqueeze(1)
148
delattr(self.conv1d.weight, "weight_loader")
140
- set_weight_attrs(
141
- self.conv1d.weight,
142
- {
143
- "weight_loader": _make_fused_conv1d_weight_loader(
144
- [self.projection_size] * 3,
145
- self.tp_size,
146
- self.tp_rank,
147
- )
148
- },
149
+ # ROCm can fuse the whole decode step (conv + recurrence + gated norm)
150
+ # into one kernel, which wants a width-major fp32 conv weight staged at
151
+ # load time. Everything else keeps the [channel, width] layout.
152
+ conv_state_dtype, _ = self.get_state_dtype()
153
+ decode_conv1d_weight = None
154
+ if is_fused_kda_decode_supported(
155
+ self.local_num_heads,
156
+ self.head_dim,
157
+ self.conv_size,
158
+ self.num_spec,
159
+ vllm_config.model_config.dtype,
160
+ conv_state_dtype,
161
+ ):
162
+ logger.info_once("Fused KDA decode kernel (conv+KDA+norm) is enabled.")
163
+ decode_conv1d_weight = torch.empty(
164
+ 3,
165
+ self.conv_size,
166
+ self.local_projection_size,
167
+ dtype=self.conv1d.weight.dtype,
168
+ device=self.conv1d.weight.device,
169
+ )
170
+ self.register_buffer(
171
+ "decode_conv1d_weight", decode_conv1d_weight, persistent=False
172
)
173
+ if decode_conv1d_weight is None:
174
+ conv1d_weight_loader = _make_fused_conv1d_weight_loader(
175
+ [self.projection_size] * 3,
176
+ self.tp_size,
177
+ self.tp_rank,
178
+ )
179
+ else:
180
+ conv1d_weight_loader = make_decode_conv1d_weight_loader(
181
+ [self.projection_size] * 3,
182
+ self.tp_size,
183
+ self.tp_rank,
184
+ decode_conv1d_weight,
185
+ )
186
+ set_weight_attrs(self.conv1d.weight, {"weight_loader": conv1d_weight_loader})
187
188
self.A_log = nn.Parameter(
189
torch.empty(self.local_num_heads, dtype=torch.float32)
@@ -175,6 +212,22 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
212
)
213
214
self.o_norm = FusedRMSNormGated(self.head_dim, activation="sigmoid")
215
+ decode_norm_weight = None
216
+ if decode_conv1d_weight is not None:
217
+ # Upcast once at load time; a BF16 norm weight slows the fused
218
+ # decode kernel's epilogue.
219
+ decode_norm_weight = torch.empty(
220
+ self.head_dim,
221
+ dtype=torch.float32,
222
+ device=self.o_norm.weight.device,
223
+ )
224
+ if hasattr(self.o_norm.weight, "weight_loader"):
225
+ delattr(self.o_norm.weight, "weight_loader")
226
+ set_weight_attrs(
227
+ self.o_norm.weight,
228
+ {"weight_loader": make_decode_norm_weight_loader(decode_norm_weight)},
229
+ )
230
+ self.register_buffer("decode_norm_weight", decode_norm_weight, persistent=False)
231
self.o_proj = RowParallelLinear(
232
self.projection_size,
233
self.hidden_size,
@@ -280,6 +333,33 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
333
if not is_conv_state_dim_first():
334
conv_state = conv_state.transpose(-1, -2)
335
336
+ if (
337
+ self.decode_conv1d_weight is not None
338
+ and self.decode_norm_weight is not None
339
+ and spec_sequence_masks is None
340
+ and m.num_prefills == 0
341
+ and m.num_decodes > 0
342
+ ):
343
+ assert non_spec_state_indices_tensor is not None
344
+ ops.fused_kda_decode(
345
+ x=mixed_qkv,
346
+ weight=self.decode_conv1d_weight,
347
+ bias=self.conv1d.bias,
348
+ conv_state=conv_state,
349
+ raw_g=g1,
350
+ raw_beta=beta,
351
+ A_log=self.A_log,
352
+ dt_bias=self.dt_bias,
353
+ state_indices=non_spec_state_indices_tensor[:num_actual_tokens],
354
+ state=recurrent_state,
355
+ out=core_attn_out[:, :num_actual_tokens],
356
+ lower_bound=self.gate_lower_bound,
357
+ output_gate=g2[:num_actual_tokens],
358
+ norm_weight=self.decode_norm_weight,
359
+ norm_eps=self.o_norm.eps,
360
+ )
361
+ return
362
+
363
conv_weights = self.conv1d.weight.view(
364
self.conv1d.weight.size(0), self.conv1d.weight.size(2)
365
)
vllm/models/kimi_k3/amd/ops/kda_decode.py
new
+99
@@ -0,0 +1,99 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""ROCm entry points for the fused Kimi-K3 KDA decode kernel.
4
+
5
+The kernel in ``csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel_rocm.cu``
6
+replaces, for a pure non-speculative decode batch, the three Triton launches
7
+and two copies the AMD KDA layer otherwise runs per layer: the packed causal
8
+conv1d update, the recurrent delta-rule step, and the gated output RMSNorm.
9
+
10
+The kernel wants a width-major conv weight and an fp32 norm weight, so both are
11
+staged once at load time by the weight loaders below.
12
+"""
13
+
14
+from collections.abc import Callable
15
+
16
+import torch
17
+
18
+from vllm.logger import init_logger
19
+from vllm.model_executor.layers.mamba.mamba_utils import is_conv_state_dim_first
20
+from vllm.model_executor.model_loader.weight_utils import default_weight_loader
21
+
22
+logger = init_logger(__name__)
23
+
24
+# Head counts the kernel is instantiated for (Kimi-K3 has 96 KDA heads, so this
25
+# covers TP1/2/4/8).
26
+SUPPORTED_NUM_HEADS = (12, 24, 48, 96)
27
+
28
+
29
+def is_fused_kda_decode_supported(
30
+ num_heads: int,
31
+ head_dim: int,
32
+ conv_width: int,
33
+ num_spec: int,
34
+ input_dtype: torch.dtype,
35
+ conv_state_dtype: torch.dtype,
36
+) -> bool:
37
+ """Whether the fused decode kernel can serve this layer on this device."""
38
+ from vllm.platforms.rocm import on_gfx950
39
+
40
+ if (
41
+ num_heads not in SUPPORTED_NUM_HEADS
42
+ or head_dim != 128
43
+ or conv_width != 4
44
+ or num_spec != 0
45
+ or input_dtype != torch.bfloat16
46
+ or conv_state_dtype != torch.bfloat16
47
+ or is_conv_state_dim_first()
48
+ or not hasattr(torch.ops._C, "fused_kda_decode")
49
+ ):
50
+ return False
51
+ # TODO: Verify on other archs; only measured on gfx950 for now
52
+ return on_gfx950()
53
+
54
+
55
+def make_decode_conv1d_weight_loader(
56
+ dims: list[int],
57
+ tp_size: int,
58
+ tp_rank: int,
59
+ decode_conv1d_weight: torch.Tensor | None,
60
+) -> Callable[..., None]:
61
+ """Load the packed conv1d weight, mirroring a width-major fp32 copy.
62
+
63
+ The fused kernel indexes the weight as ``[qkv, width, channel]`` so the
64
+ channel dimension is the contiguous one; the Triton prefill and fallback
65
+ decode kernels keep the ``[channel, width]`` layout.
66
+ """
67
+ sharded_dims = [dim // tp_size for dim in dims]
68
+
69
+ def weight_loader(
70
+ param: torch.Tensor,
71
+ loaded_weight: torch.Tensor,
72
+ loaded_shard_id: int,
73
+ ) -> None:
74
+ if loaded_weight.dim() == 2:
75
+ loaded_weight = loaded_weight.unsqueeze(1)
76
+ shard_size = sharded_dims[loaded_shard_id]
77
+ source_start = tp_rank * shard_size
78
+ target_start = sum(sharded_dims[:loaded_shard_id])
79
+ loaded_shard = loaded_weight[source_start : source_start + shard_size]
80
+ param.data[target_start : target_start + shard_size].copy_(loaded_shard)
81
+ if decode_conv1d_weight is not None and not param.is_meta:
82
+ decode_conv1d_weight[loaded_shard_id].copy_(
83
+ loaded_shard.squeeze(1).transpose(0, 1)
84
+ )
85
+
86
+ return weight_loader
87
+
88
+
89
+def make_decode_norm_weight_loader(
90
+ decode_norm_weight: torch.Tensor,
91
+) -> Callable[..., None]:
92
+ """Load the gated-norm weight, mirroring an fp32 copy for the kernel."""
93
+
94
+ def weight_loader(param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
95
+ default_weight_loader(param, loaded_weight)
96
+ if not param.is_meta:
97
+ decode_norm_weight.copy_(param.data)
98
+
99
+ return weight_loader