[Kimi K3][Kernel] Support DS conv-state layout in fused KDA decode kernel (#53396)

Signed-off-by: Canlin Guo <canlinguosdu@gmail.com>

Canlin Guo committed Aug 27, 2026 at 14:54 UTC aa640684cc95f1e4b14fdf38298ea8577a70de56
5 files changed +213 -106
benchmarks/kernels/benchmark_kimi_k3_kda_decode.py
+25 -8
@@ -81,7 +81,7 @@ def _bench_graph(fn, repeats: int = NUM_KDA_LAYERS) -> float:
81
82
83 class Inputs:
84 - def __init__(self, num_tokens: int, num_heads: int) -> None:
84 + def __init__(self, num_tokens: int, num_heads: int, conv_layout: str) -> None:
85 torch.manual_seed(0)
86 device = "cuda"
87 dim = num_heads * HEAD_DIM
@@ -98,9 +98,15 @@ class Inputs:
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 - )
101 + if conv_layout == "DS":
102 + self.conv_state_t = torch.randn(
103 + num_slots, 3 * dim, CONV_WIDTH - 1, device=device, dtype=DTYPE
104 + )
105 + else:
106 + conv_state = torch.randn(
107 + num_slots, CONV_WIDTH - 1, 3 * dim, device=device, dtype=DTYPE
108 + )
109 + self.conv_state_t = conv_state.transpose(-1, -2)
110 self.recurrent_state = torch.randn(
111 num_slots, num_heads, HEAD_DIM, HEAD_DIM, device=device, dtype=torch.float32
112 )
@@ -120,7 +126,6 @@ class Inputs:
126 self.state_indices = torch.arange(
127 1, num_tokens + 1, device=device, dtype=torch.int32
128 )
123 - self.conv_state_t = self.conv_state.transpose(-1, -2)
129 self.out = torch.empty(
130 1, num_tokens, num_heads, HEAD_DIM, device=device, dtype=DTYPE
131 )
@@ -237,6 +242,12 @@ def main() -> None:
242 default=[12],
243 help="KDA heads per rank (96 total / TP size)",
244 )
245 + parser.add_argument(
246 + "--layout",
247 + choices=("SD", "DS"),
248 + default="SD",
249 + help="physical conv-state cache layout",
250 + )
251 args = parser.parse_args()
252
253 if not hasattr(torch.ops._C, "fused_kda_decode"):
@@ -245,7 +256,10 @@ def main() -> None:
256 props = torch.cuda.get_device_properties(0)
257 bench = _bench_graph if args.graph else _bench
258 mode = "cuda-graph replay" if args.graph else "eager dispatch"
248 - print(f"device: {props.name} ({props.gcnArchName}) timing: {mode}")
259 + print(
260 + f"device: {props.name} ({props.gcnArchName}) timing: {mode} "
261 + f"layout: {args.layout}"
262 + )
263 print(
264 f"{'heads':>6} {'tokens':>7} {'eager-norm':>11} {'triton-norm':>12} "
265 f"{'fused us':>9} {'speedup':>8} {'state TB/s':>11} {'saved ms/step':>14}"
@@ -253,7 +267,10 @@ def main() -> None:
267 for num_heads in args.heads:
268 for num_tokens in args.tokens:
269 if args.graph and args.layers > 1:
256 - layers = [Inputs(num_tokens, num_heads) for _ in range(args.layers)]
270 + layers = [
271 + Inputs(num_tokens, num_heads, args.layout)
272 + for _ in range(args.layers)
273 + ]
274 eager_ms = _bench_graph_layers(
275 [functools.partial(triton_chain, i, False) for i in layers]
276 )
@@ -267,7 +284,7 @@ def main() -> None:
284 del layers
285 torch.accelerator.empty_cache()
286 else:
270 - inp = Inputs(num_tokens, num_heads)
287 + inp = Inputs(num_tokens, num_heads, args.layout)
288 eager_ms = bench(functools.partial(triton_chain, inp, False))
289 triton_ms = bench(functools.partial(triton_chain, inp, True))
290 fused_ms = bench(functools.partial(fused, inp))
csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel.cu
+91 -54
@@ -261,15 +261,16 @@ __device__ __forceinline__ Sum2 block_reduce_sum2_active_for(float x, float y,
261 return {scratch[0], scratch[1]};
262 }
263
264 -template <bool kApplyOnorm, bool kUseStaticDecodeLayout = false,
265 - int kFixedHeads = 0, int kFixedValueHeads = 0,
266 - bool kUseHeadGrid = false, bool kAccumulateOnormSumsq = false,
267 - bool kUseActiveQkReduction = false, bool kUseCacheGlobalStore = false,
268 - bool kComputeOutputBeforeStore = false, bool kSkipWarpSync = false,
269 - bool kPreloadOnormParams = false,
270 - bool kPrefetchNextStateChunk = false,
271 - bool kUseActiveOnormReduction = false, bool kUpdateConvState = false,
272 - bool kUseLowerBound = false, bool kApplyBetaSigmoid = true>
264 +template <
265 + bool kApplyOnorm, bool kUseStaticDecodeLayout = false, int kFixedHeads = 0,
266 + int kFixedValueHeads = 0, bool kUseHeadGrid = false,
267 + bool kAccumulateOnormSumsq = false, bool kUseActiveQkReduction = false,
268 + bool kUseCacheGlobalStore = false, bool kComputeOutputBeforeStore = false,
269 + bool kSkipWarpSync = false, bool kPreloadOnormParams = false,
270 + bool kPrefetchNextStateChunk = false, bool kUseActiveOnormReduction = false,
271 + bool kUpdateConvState = false, bool kUseLowerBound = false,
272 + bool kApplyBetaSigmoid = true, int kConvStateChannelStride = 1,
273 + int kConvStateTapStride = 3 * kFixedHeads * kDimK>
274 __global__
275 __launch_bounds__(kThreads, 2) void kda_decode_fusion_many_heads_kernel(
276 const __nv_bfloat16* __restrict__ x_q,
@@ -329,7 +330,6 @@ __launch_bounds__(kThreads, 2) void kda_decode_fusion_many_heads_kernel(
330 }
331
332 constexpr int kLocalDim = kFixedHeads * kDimK;
332 - constexpr int kPackedDim = 3 * kLocalDim;
333 const int hk_off = i_h * kDimK;
334 const int hv_off = i_hv * kDimV;
335 constexpr int hv_count = kFixedValueHeads;
@@ -368,8 +368,10 @@ __launch_bounds__(kThreads, 2) void kda_decode_fusion_many_heads_kernel(
368 __nv_bfloat16 k_shift1 = __float2bfloat16(0.0f);
369 #pragma unroll
370 for (int w = 0; w < kConvStateWidth; ++w) {
371 - const __nv_bfloat16 q_state = cs_q_for_slot[hk + w * kPackedDim];
372 - const __nv_bfloat16 k_state = cs_k_for_slot[hk + w * kPackedDim];
371 + const int cs_idx =
372 + hk * kConvStateChannelStride + w * kConvStateTapStride;
373 + const __nv_bfloat16 q_state = cs_q_for_slot[cs_idx];
374 + const __nv_bfloat16 k_state = cs_k_for_slot[cs_idx];
375 q_acc += __bfloat162float(q_state) *
376 conv_weight_load<kLocalDim>(w_q_t, hk, w);
377 k_acc += __bfloat162float(k_state) *
@@ -389,12 +391,16 @@ __launch_bounds__(kThreads, 2) void kda_decode_fusion_many_heads_kernel(
391 k_acc += __bfloat162float(k_new) *
392 conv_weight_load<kLocalDim>(w_k_t, hk, kKernelWidth - 1);
393
392 - cs_q_for_slot[hk] = q_shift0;
393 - cs_q_for_slot[hk + kPackedDim] = q_shift1;
394 - cs_q_for_slot[hk + 2 * kPackedDim] = q_new;
395 - cs_k_for_slot[hk] = k_shift0;
396 - cs_k_for_slot[hk + kPackedDim] = k_shift1;
397 - cs_k_for_slot[hk + 2 * kPackedDim] = k_new;
394 + cs_q_for_slot[hk * kConvStateChannelStride] = q_shift0;
395 + cs_q_for_slot[hk * kConvStateChannelStride + kConvStateTapStride] =
396 + q_shift1;
397 + cs_q_for_slot[hk * kConvStateChannelStride + 2 * kConvStateTapStride] =
398 + q_new;
399 + cs_k_for_slot[hk * kConvStateChannelStride] = k_shift0;
400 + cs_k_for_slot[hk * kConvStateChannelStride + kConvStateTapStride] =
401 + k_shift1;
402 + cs_k_for_slot[hk * kConvStateChannelStride + 2 * kConvStateTapStride] =
403 + k_new;
404
405 s_q[k] = silu_fast(q_acc);
406 s_k[k] = silu_fast(k_acc);
@@ -418,7 +424,8 @@ __launch_bounds__(kThreads, 2) void kda_decode_fusion_many_heads_kernel(
424 float k_acc = bias_k == nullptr ? 0.0f : bf16_load(bias_k, hk);
425 #pragma unroll
426 for (int w = 0; w < kConvStateWidth; ++w) {
421 - const int cs_idx = hk + w * kPackedDim;
427 + const int cs_idx =
428 + hk * kConvStateChannelStride + w * kConvStateTapStride;
429 q_acc += bf16_load(cs_q_for_slot, cs_idx) *
430 conv_weight_load<kLocalDim>(w_q_t, hk, w);
431 k_acc += bf16_load(cs_k_for_slot, cs_idx) *
@@ -453,7 +460,9 @@ __launch_bounds__(kThreads, 2) void kda_decode_fusion_many_heads_kernel(
460 __nv_bfloat16 v_shift1 = __float2bfloat16(0.0f);
461 #pragma unroll
462 for (int w = 0; w < kConvStateWidth; ++w) {
456 - const __nv_bfloat16 v_state = cs_v_for_slot[hvv + w * kPackedDim];
463 + const int cs_idx =
464 + hvv * kConvStateChannelStride + w * kConvStateTapStride;
465 + const __nv_bfloat16 v_state = cs_v_for_slot[cs_idx];
466 v_acc += __bfloat162float(v_state) *
467 conv_weight_load<kLocalDim>(w_v_t, hvv, w);
468 if (w == 1) {
@@ -465,9 +474,11 @@ __launch_bounds__(kThreads, 2) void kda_decode_fusion_many_heads_kernel(
474 const __nv_bfloat16 v_new = x_v[xv_idx];
475 v_acc += __bfloat162float(v_new) *
476 conv_weight_load<kLocalDim>(w_v_t, hvv, kKernelWidth - 1);
468 - cs_v_for_slot[hvv] = v_shift0;
469 - cs_v_for_slot[hvv + kPackedDim] = v_shift1;
470 - cs_v_for_slot[hvv + 2 * kPackedDim] = v_new;
477 + cs_v_for_slot[hvv * kConvStateChannelStride] = v_shift0;
478 + cs_v_for_slot[hvv * kConvStateChannelStride + kConvStateTapStride] =
479 + v_shift1;
480 + cs_v_for_slot[hvv * kConvStateChannelStride + 2 * kConvStateTapStride] =
481 + v_new;
482 s_v[v] = silu_fast(v_acc);
483
484 if constexpr (kApplyOnorm && kPreloadOnormParams) {
@@ -484,7 +495,8 @@ __launch_bounds__(kThreads, 2) void kda_decode_fusion_many_heads_kernel(
495 float v_acc = bias_v == nullptr ? 0.0f : bf16_load(bias_v, hvv);
496 #pragma unroll
497 for (int w = 0; w < kConvStateWidth; ++w) {
487 - const int cs_idx = hvv + w * kPackedDim;
498 + const int cs_idx =
499 + hvv * kConvStateChannelStride + w * kConvStateTapStride;
500 v_acc += bf16_load(cs_v_for_slot, cs_idx) *
501 conv_weight_load<kLocalDim>(w_v_t, hvv, w);
502 }
@@ -723,7 +735,8 @@ __launch_bounds__(kThreads, 2) void kda_decode_fusion_many_heads_kernel(
735 }
736
737 template <int kHeads, bool kApplyOnorm, bool kUpdateConvState,
726 - bool kUseLowerBound, bool kApplyBetaSigmoid>
738 + bool kUseLowerBound, bool kApplyBetaSigmoid,
739 + int kConvStateChannelStride, int kConvStateTapStride>
740 void launch_kda_decode_many_heads_raw(
741 const void* x_q, const void* x_k, const void* x_v, const void* w_q_t,
742 const void* w_k_t, const void* w_v_t, const void* bias_q,
@@ -736,7 +749,7 @@ void launch_kda_decode_many_heads_raw(
749 auto kernel = &kda_decode_fusion_many_heads_kernel<
750 kApplyOnorm, true, kHeads, kHeads, true, false, false, false, false,
751 false, true, true, true, kUpdateConvState, kUseLowerBound,
739 - kApplyBetaSigmoid>;
752 + kApplyBetaSigmoid, kConvStateChannelStride, kConvStateTapStride>;
753 cudaLaunchConfig_t config{};
754 config.gridDim = dim3(B, kHeads);
755 config.blockDim = dim3(kThreads);
@@ -776,26 +789,39 @@ void launch_kda_decode_many_heads_selected(
789 const float* a_log, const void* g, const float* dt_bias, const void* beta,
790 const void* onorm_g, const float* onorm_weight,
791 const int* ssm_state_indices, const int* cu_seqlens, float* state,
779 - void* out, int B, int H, int HV, bool update_conv_cache, float lower_bound,
780 - float scale, float onorm_eps, KdaDecodeStrides strides,
781 - cudaStream_t stream) {
782 -#define LAUNCH_KDA_DECODE(NUM_HEADS) \
783 - do { \
784 - if (update_conv_cache) { \
785 - launch_kda_decode_many_heads_raw<NUM_HEADS, kApplyOnorm, true, \
786 - kUseLowerBound, kApplyBetaSigmoid>( \
787 - x_q, x_k, x_v, w_q_t, w_k_t, w_v_t, bias_q, bias_k, bias_v, cs_q, \
788 - cs_k, cs_v, a_log, g, dt_bias, beta, onorm_g, onorm_weight, \
789 - ssm_state_indices, cu_seqlens, state, out, B, H, HV, lower_bound, \
790 - scale, onorm_eps, strides, stream); \
791 - } else { \
792 - launch_kda_decode_many_heads_raw<NUM_HEADS, kApplyOnorm, false, \
793 - kUseLowerBound, kApplyBetaSigmoid>( \
794 - x_q, x_k, x_v, w_q_t, w_k_t, w_v_t, bias_q, bias_k, bias_v, cs_q, \
795 - cs_k, cs_v, a_log, g, dt_bias, beta, onorm_g, onorm_weight, \
796 - ssm_state_indices, cu_seqlens, state, out, B, H, HV, lower_bound, \
797 - scale, onorm_eps, strides, stream); \
798 - } \
792 + void* out, int B, int H, int HV, bool update_conv_cache,
793 + bool conv_state_is_ds, float lower_bound, float scale, float onorm_eps,
794 + KdaDecodeStrides strides, cudaStream_t stream) {
795 +#define LAUNCH_KDA_DECODE_LAYOUT(NUM_HEADS, CS_CHANNEL_STRIDE, CS_TAP_STRIDE) \
796 + do { \
797 + if (update_conv_cache) { \
798 + launch_kda_decode_many_heads_raw<NUM_HEADS, kApplyOnorm, true, \
799 + kUseLowerBound, kApplyBetaSigmoid, \
800 + CS_CHANNEL_STRIDE, CS_TAP_STRIDE>( \
801 + x_q, x_k, x_v, w_q_t, w_k_t, w_v_t, bias_q, bias_k, bias_v, cs_q, \
802 + cs_k, cs_v, a_log, g, dt_bias, beta, onorm_g, onorm_weight, \
803 + ssm_state_indices, cu_seqlens, state, out, B, H, HV, lower_bound, \
804 + scale, onorm_eps, strides, stream); \
805 + } else { \
806 + launch_kda_decode_many_heads_raw<NUM_HEADS, kApplyOnorm, false, \
807 + kUseLowerBound, kApplyBetaSigmoid, \
808 + CS_CHANNEL_STRIDE, CS_TAP_STRIDE>( \
809 + x_q, x_k, x_v, w_q_t, w_k_t, w_v_t, bias_q, bias_k, bias_v, cs_q, \
810 + cs_k, cs_v, a_log, g, dt_bias, beta, onorm_g, onorm_weight, \
811 + ssm_state_indices, cu_seqlens, state, out, B, H, HV, lower_bound, \
812 + scale, onorm_eps, strides, stream); \
813 + } \
814 + } while (false)
815 +
816 + // SD packs (tap, channel) per slot while DS packs (channel, tap); both are
817 + // baked into the kernel as compile-time strides.
818 +#define LAUNCH_KDA_DECODE(NUM_HEADS) \
819 + do { \
820 + if (conv_state_is_ds) { \
821 + LAUNCH_KDA_DECODE_LAYOUT(NUM_HEADS, kConvStateWidth, 1); \
822 + } else { \
823 + LAUNCH_KDA_DECODE_LAYOUT(NUM_HEADS, 1, 3 * (NUM_HEADS) * kDimK); \
824 + } \
825 } while (false)
826
827 switch (H) {
@@ -816,6 +842,7 @@ void launch_kda_decode_many_heads_selected(
842 }
843
844 #undef LAUNCH_KDA_DECODE
845 +#undef LAUNCH_KDA_DECODE_LAYOUT
846 }
847
848 struct KdaDecodeLaunchParams {
@@ -845,6 +872,7 @@ struct KdaDecodeLaunchParams {
872 int H;
873 int HV;
874 bool update_conv_cache;
875 + bool conv_state_is_ds;
876 float lower_bound;
877 float scale;
878 float onorm_eps;
@@ -859,8 +887,8 @@ void launch_kda_decode_selected_backend(const KdaDecodeLaunchParams& p) {
887 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,
888 p.bias_v, p.cs_q, p.cs_k, p.cs_v, p.a_log, p.g, p.dt_bias, p.beta,
889 p.onorm_g, p.onorm_weight, p.ssm_state_indices, p.cu_seqlens, p.state,
862 - p.out, p.B, p.H, p.HV, p.update_conv_cache, p.lower_bound, p.scale,
863 - p.onorm_eps, p.strides, p.stream);
890 + p.out, p.B, p.H, p.HV, p.update_conv_cache, p.conv_state_is_ds,
891 + p.lower_bound, p.scale, p.onorm_eps, p.strides, p.stream);
892 }
893
894 template <bool kApplyOnorm, bool kUseLowerBound>
@@ -908,6 +936,8 @@ extern "C" void launch_kda_decode_many_heads_cuda(
936 cudaStream_t stream) {
937 const KdaDecodeStrides strides{raw_strides[0], raw_strides[1], raw_strides[2],
938 raw_strides[3], raw_strides[4]};
939 + const bool conv_state_is_ds =
940 + raw_strides[5] == kConvStateWidth && raw_strides[6] == 1;
941 const KdaDecodeLaunchParams params{x_q,
942 x_k,
943 x_v,
@@ -934,6 +964,7 @@ extern "C" void launch_kda_decode_many_heads_cuda(
964 H,
965 HV,
966 update_conv_cache,
967 + conv_state_is_ds,
968 lower_bound,
969 scale,
970 onorm_eps,
@@ -1027,10 +1058,15 @@ void fused_kda_decode(
1058 "out must have shape [1, B, H, 128]");
1059 STD_TORCH_CHECK(x.stride(1) == 1,
1060 "x must be contiguous in its channel dimension");
1030 - STD_TORCH_CHECK(conv_state.stride(0) >= 3 * dim * (kConvWidth - 1) &&
1031 - conv_state.stride(1) == 1 &&
1032 - conv_state.stride(2) == 3 * dim,
1033 - "conv_state must use the SD cache layout");
1061 + // Both conv-state cache layouts are supported: SD stores (state_len, dim)
1062 + // per block while DS stores (dim, state_len). The kernel only needs the
1063 + // per-plane strides.
1064 + STD_TORCH_CHECK(
1065 + conv_state.stride(0) >= 3 * dim * (kConvWidth - 1) &&
1066 + ((conv_state.stride(1) == 1 && conv_state.stride(2) == 3 * dim) ||
1067 + (conv_state.stride(1) == kConvWidth - 1 &&
1068 + conv_state.stride(2) == 1)),
1069 + "conv_state must use the SD or DS cache layout");
1070 STD_TORCH_CHECK(state.stride(0) >= num_heads * kHeadDim * kHeadDim &&
1071 state.stride(1) == kHeadDim * kHeadDim &&
1072 state.stride(2) == kHeadDim && state.stride(3) == 1,
@@ -1096,9 +1132,10 @@ void fused_kda_decode(
1132 int64_t const conv_segment_bytes =
1133 dim * conv_state.stride(1) * sizeof(__nv_bfloat16);
1134 int64_t const bias_segment_bytes = dim * sizeof(float);
1099 - std::array<int64_t, 5> const strides{
1135 + std::array<int64_t, 7> const strides{
1136 x.stride(0), raw_beta.stride(1), output_gate_row_stride,
1101 - conv_state.stride(0), state.stride(0),
1137 + conv_state.stride(0), state.stride(0), conv_state.stride(1),
1138 + conv_state.stride(2),
1139 };
1140 bool const use_lower_bound = lower_bound.has_value();
1141 float const lower_bound_value =
csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel_rocm.cu
+58 -28
@@ -147,7 +147,8 @@ __device__ __forceinline__ Sum3 block_reduce_sum3(float x, float y, float z,
147 scratch[3 * kGroups + 2]};
148 }
149
150 -template <bool kApplyOnorm, int kFixedHeads, bool kUpdateConvState,
150 +template <bool kApplyOnorm, int kConvStateChannelStride,
151 + int kConvStateTapStride, int kFixedHeads, bool kUpdateConvState,
152 bool kUseLowerBound, bool kApplyBetaSigmoid>
153 __global__ __launch_bounds__(kThreads) void kda_decode_fusion_kernel(
154 const bf16_t* __restrict__ x_q, const bf16_t* __restrict__ x_k,
@@ -186,7 +187,6 @@ __global__ __launch_bounds__(kThreads) void kda_decode_fusion_kernel(
187 }
188 return;
189 }
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;
@@ -242,8 +242,10 @@ __global__ __launch_bounds__(kThreads) void kda_decode_fusion_kernel(
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];
245 + const int cs_idx =
246 + hk * kConvStateChannelStride + w * kConvStateTapStride;
247 + const bf16_t q_state = cs_q_for_slot[cs_idx];
248 + const bf16_t k_state = cs_k_for_slot[cs_idx];
249 q_acc += __bfloat162float(q_state) *
250 conv_weight_load<kLocalDim>(w_q_t, hk, w);
251 k_acc += __bfloat162float(k_state) *
@@ -264,16 +266,21 @@ __global__ __launch_bounds__(kThreads) void kda_decode_fusion_kernel(
266 k_acc += __bfloat162float(k_new) *
267 conv_weight_load<kLocalDim>(w_k_t, hk, kKernelWidth - 1);
268
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;
269 + cs_q_for_slot[hk * kConvStateChannelStride] = q_shift0;
270 + cs_q_for_slot[hk * kConvStateChannelStride + kConvStateTapStride] =
271 + q_shift1;
272 + cs_q_for_slot[hk * kConvStateChannelStride + 2 * kConvStateTapStride] =
273 + q_new;
274 + cs_k_for_slot[hk * kConvStateChannelStride] = k_shift0;
275 + cs_k_for_slot[hk * kConvStateChannelStride + kConvStateTapStride] =
276 + k_shift1;
277 + cs_k_for_slot[hk * kConvStateChannelStride + 2 * kConvStateTapStride] =
278 + k_new;
279 } else {
280 #pragma unroll
281 for (int w = 0; w < kConvStateWidth; ++w) {
276 - const int cs_idx = hk + w * kPackedDim;
282 + const int cs_idx =
283 + hk * kConvStateChannelStride + w * kConvStateTapStride;
284 q_acc += bf16_load(cs_q_for_slot, cs_idx) *
285 conv_weight_load<kLocalDim>(w_q_t, hk, w);
286 k_acc += bf16_load(cs_k_for_slot, cs_idx) *
@@ -307,7 +314,9 @@ __global__ __launch_bounds__(kThreads) void kda_decode_fusion_kernel(
314 bf16_t v_shift1 = __float2bfloat16(0.0f);
315 #pragma unroll
316 for (int w = 0; w < kConvStateWidth; ++w) {
310 - const bf16_t v_state = cs_v_for_slot[hvv + w * kPackedDim];
317 + const int cs_idx =
318 + hvv * kConvStateChannelStride + w * kConvStateTapStride;
319 + const bf16_t v_state = cs_v_for_slot[cs_idx];
320 v_acc += __bfloat162float(v_state) *
321 conv_weight_load<kLocalDim>(w_v_t, hvv, w);
322 if (w == 1) {
@@ -320,13 +329,16 @@ __global__ __launch_bounds__(kThreads) void kda_decode_fusion_kernel(
329 const bf16_t v_new = x_v[xv_idx];
330 v_acc += __bfloat162float(v_new) *
331 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;
332 + cs_v_for_slot[hvv * kConvStateChannelStride] = v_shift0;
333 + cs_v_for_slot[hvv * kConvStateChannelStride + kConvStateTapStride] =
334 + v_shift1;
335 + cs_v_for_slot[hvv * kConvStateChannelStride + 2 * kConvStateTapStride] =
336 + v_new;
337 } else {
338 #pragma unroll
339 for (int w = 0; w < kConvStateWidth; ++w) {
329 - const int cs_idx = hvv + w * kPackedDim;
340 + const int cs_idx =
341 + hvv * kConvStateChannelStride + w * kConvStateTapStride;
342 v_acc += bf16_load(cs_v_for_slot, cs_idx) *
343 conv_weight_load<kLocalDim>(w_v_t, hvv, w);
344 }
@@ -461,7 +473,8 @@ __global__ __launch_bounds__(kThreads) void kda_decode_fusion_kernel(
473 }
474
475 template <int kHeads, bool kApplyOnorm, bool kUpdateConvState,
464 - bool kUseLowerBound, bool kApplyBetaSigmoid>
476 + bool kUseLowerBound, bool kApplyBetaSigmoid,
477 + int kConvStateChannelStride, int kConvStateTapStride>
478 void launch_kda_decode_raw(
479 const void* x_q, const void* x_k, const void* x_v, const void* w_q_t,
480 const void* w_k_t, const void* w_v_t, const void* bias_q,
@@ -471,7 +484,8 @@ void launch_kda_decode_raw(
484 const int* ssm_state_indices, float* state, void* out, int B,
485 float lower_bound, float scale, float onorm_eps, KdaDecodeStrides strides,
486 hipStream_t stream) {
474 - kda_decode_fusion_kernel<kApplyOnorm, kHeads, kUpdateConvState,
487 + kda_decode_fusion_kernel<kApplyOnorm, kConvStateChannelStride,
488 + kConvStateTapStride, kHeads, kUpdateConvState,
489 kUseLowerBound, kApplyBetaSigmoid>
490 <<<dim3(kHeads, B), dim3(kThreads), 0, stream>>>(
491 reinterpret_cast<const bf16_t*>(x_q),
@@ -517,6 +531,7 @@ struct KdaDecodeLaunchParams {
531 int B;
532 int H;
533 bool update_conv_cache;
534 + bool conv_state_is_ds;
535 float lower_bound;
536 float scale;
537 float onorm_eps;
@@ -527,19 +542,29 @@ struct KdaDecodeLaunchParams {
542 template <int kHeads, bool kApplyOnorm, bool kUseLowerBound,
543 bool kApplyBetaSigmoid>
544 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, \
545 +#define LAUNCH_KDA_DECODE_LAYOUT(UPDATE_CONV, CS_CHANNEL_STRIDE, \
546 + CS_TAP_STRIDE) \
547 + launch_kda_decode_raw<kHeads, kApplyOnorm, UPDATE_CONV, kUseLowerBound, \
548 + kApplyBetaSigmoid, CS_CHANNEL_STRIDE, CS_TAP_STRIDE>( \
549 + 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, \
550 + p.bias_v, p.cs_q, p.cs_k, p.cs_v, p.a_log, p.g, p.dt_bias, p.beta, \
551 + p.onorm_g, p.onorm_weight, p.ssm_state_indices, p.state, p.out, p.B, \
552 p.lower_bound, p.scale, p.onorm_eps, p.strides, p.stream)
553 + // SD packs (tap, channel) per slot while DS packs (channel, tap); both are
554 + // baked into the kernel as compile-time strides.
555 +#define LAUNCH_KDA_DECODE(UPDATE_CONV) \
556 + if (p.conv_state_is_ds) { \
557 + LAUNCH_KDA_DECODE_LAYOUT(UPDATE_CONV, kConvStateWidth, 1); \
558 + } else { \
559 + LAUNCH_KDA_DECODE_LAYOUT(UPDATE_CONV, 1, 3 * kHeads * kDimK); \
560 + }
561 if (p.update_conv_cache) {
562 LAUNCH_KDA_DECODE(true);
563 } else {
564 LAUNCH_KDA_DECODE(false);
565 }
566 #undef LAUNCH_KDA_DECODE
567 +#undef LAUNCH_KDA_DECODE_LAYOUT
568 }
569
570 template <bool kApplyOnorm, bool kUseLowerBound, bool kApplyBetaSigmoid>
@@ -686,10 +711,12 @@ void fused_kda_decode(
711 "out must have shape [1, B, H, 128]");
712 STD_TORCH_CHECK(x.stride(1) == 1,
713 "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");
714 + STD_TORCH_CHECK(
715 + conv_state.stride(0) >= 3 * dim * (kConvWidth - 1) &&
716 + ((conv_state.stride(1) == 1 && conv_state.stride(2) == 3 * dim) ||
717 + (conv_state.stride(1) == kConvWidth - 1 &&
718 + conv_state.stride(2) == 1)),
719 + "conv_state must use the SD or DS cache layout");
720 STD_TORCH_CHECK(state.stride(0) >= num_heads * kHeadDim * kHeadDim &&
721 state.stride(1) == kHeadDim * kHeadDim &&
722 state.stride(2) == kHeadDim && state.stride(3) == 1,
@@ -761,6 +788,8 @@ void fused_kda_decode(
788
789 torch::stable::accelerator::DeviceGuard const device_guard(
790 x.get_device_index());
791 + bool const conv_state_is_ds =
792 + conv_state.stride(1) == kConvStateWidth && conv_state.stride(2) == 1;
793 KdaDecodeLaunchParams const params{
794 x_ptr,
795 x_ptr + segment_bytes,
@@ -786,6 +815,7 @@ void fused_kda_decode(
815 batch_size,
816 static_cast<int>(num_heads),
817 true,
818 + conv_state_is_ds,
819 lower_bound_value,
820 0.08838834764831845f,
821 static_cast<float>(norm_eps),
tests/models/kimi_k3/test_kda.py
+37 -15
@@ -895,13 +895,18 @@ def test_kda_recoverssm_verify_and_group_commit(
895
896
897 @pytest.mark.parametrize(
898 - ("num_heads", "num_seqs", "lower_bound", "fuse_output_norm"),
898 + ("num_heads", "num_seqs", "lower_bound", "fuse_output_norm", "conv_layout"),
899 [
900 - (12, 1, -5.0, True),
901 - (12, 4, None, False),
902 - (24, 4, None, False),
903 - (48, 1, -5.0, True),
904 - (96, 1, -5.0, True),
900 + (12, 1, -5.0, True, "SD"),
901 + (12, 4, None, False, "SD"),
902 + (24, 4, None, False, "SD"),
903 + (48, 1, -5.0, True, "SD"),
904 + (96, 1, -5.0, True, "SD"),
905 + (12, 1, -5.0, True, "DS"),
906 + (12, 4, None, False, "DS"),
907 + (24, 4, None, False, "DS"),
908 + (48, 1, -5.0, True, "DS"),
909 + (96, 1, -5.0, True, "DS"),
910 ],
911 )
912 @torch.inference_mode()
@@ -910,6 +915,7 @@ def test_fused_kda_decode_correctness(
915 num_seqs: int,
916 lower_bound: float | None,
917 fuse_output_norm: bool,
918 + conv_layout: str,
919 ):
920 D, W = 128, 4
921 if not is_fused_kda_decode_supported(
@@ -921,7 +927,7 @@ def test_fused_kda_decode_correctness(
927 conv_state_dtype=torch.bfloat16,
928 ):
929 pytest.skip("Fused KDA decode is not supported on this platform")
924 - torch.manual_seed(967 + num_heads + num_seqs)
930 + torch.manual_seed(967 + num_heads + num_seqs + (conv_layout == "DS"))
931 dim = num_heads * D
932 slots = num_seqs + 2
933 packed_x_storage = torch.randn(
@@ -929,13 +935,25 @@ def test_fused_kda_decode_correctness(
935 )
936 packed_x = packed_x_storage[:, : 3 * dim]
937 weight = 0.1 * torch.randn(3 * dim, W, dtype=torch.float32, device=DEVICE)
932 - conv_seed = 0.1 * torch.randn(
933 - slots,
934 - W - 1,
935 - 3 * dim,
936 - dtype=torch.bfloat16,
937 - device=DEVICE,
938 - ).transpose(1, 2)
938 + if conv_layout == "DS":
939 + # DS cache layout: per slot the taps are innermost
940 + # (stride (W-1, 1)), matching VLLM_SSM_CONV_STATE_LAYOUT=DS.
941 + conv_seed = 0.1 * torch.randn(
942 + slots,
943 + 3 * dim,
944 + W - 1,
945 + dtype=torch.bfloat16,
946 + device=DEVICE,
947 + )
948 + else:
949 + # SD cache layout: per slot the channels are innermost.
950 + conv_seed = 0.1 * torch.randn(
951 + slots,
952 + W - 1,
953 + 3 * dim,
954 + dtype=torch.bfloat16,
955 + device=DEVICE,
956 + ).transpose(1, 2)
957 raw_g = torch.randn(
958 1,
959 num_seqs,
@@ -1017,7 +1035,11 @@ def test_fused_kda_decode_correctness(
1035 conv_actual = torch.as_strided(
1036 cache_storage.view(torch.bfloat16),
1037 size=(slots, 3 * dim, W - 1),
1020 - stride=(page_bytes // torch.bfloat16.itemsize, 1, 3 * dim),
1038 + stride=(
1039 + page_bytes // torch.bfloat16.itemsize,
1040 + (W - 1) if conv_layout == "DS" else 1,
1041 + 1 if conv_layout == "DS" else 3 * dim,
1042 + ),
1043 )
1044 state_actual = torch.as_strided(
1045 cache_storage.view(torch.float32),
vllm/models/kimi_k3/nvidia/kda.py
+2 -1
@@ -153,6 +153,8 @@ def is_fused_kda_decode_supported(
153 input_dtype: torch.dtype,
154 conv_state_dtype: torch.dtype,
155 ) -> bool:
156 + # The fused kernel handles both conv-state cache layouts (SD and DS); the
157 + # inner strides are selected from the tensor at launch time.
158 if (
159 num_heads not in (12, 24, 48, 96)
160 or head_dim != 128
@@ -160,7 +162,6 @@ def is_fused_kda_decode_supported(
162 or num_spec != 0
163 or input_dtype != torch.bfloat16
164 or conv_state_dtype != torch.bfloat16
163 - or is_conv_state_dim_first()
165 or not hasattr(torch.ops._C, "fused_kda_decode")
166 ):
167 return False