[ROCm][Bugfix] Kimi-K3 Fix KDA NaN on mixed batches and racy autotune config (#50649)
Signed-off-by: kliuae <kuanfu.liu@embeddedllm.com>
kliuae committed
Aug 5, 2026 at 16:07 UTC
f5cd862dbdafb9d9870945a4f61454f90d87e2ae
3 files changed
+556
-7
vllm/models/kimi_k3/amd/kda.py
new
+530
@@ -0,0 +1,530 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import torch
5
+from einops import rearrange
6
+from torch import nn
7
+
8
+from vllm.compilation.breakable_cudagraph import eager_break_during_capture
9
+from vllm.config import VllmConfig
10
+from vllm.distributed import divide
11
+from vllm.forward_context import get_forward_context
12
+from vllm.model_executor.layers.linear import (
13
+ ColumnParallelLinear,
14
+ RowParallelLinear,
15
+)
16
+from vllm.model_executor.layers.mamba.gdn.base import GatedDeltaNetAttention
17
+
18
+# Generic KDA helpers, shared with Kimi-Linear. They are neither ROCm- nor
19
+# K3-specific, so they are imported rather than duplicated. (nvidia/kda.py keeps
20
+# its own copies; duplicating here instead would be a one-line change.)
21
+from vllm.model_executor.layers.mamba.gdn.kimi_gdn_linear_attn import (
22
+ _KDA_GATE_LOGBOUND_MIN,
23
+ _KimiGDNMergedColumnParallelLinear,
24
+ _make_fused_conv1d_weight_loader,
25
+ a_log_weight_loader,
26
+)
27
+from vllm.model_executor.layers.mamba.mamba_utils import (
28
+ MambaStateDtypeCalculator,
29
+ MambaStateShapeCalculator,
30
+ is_conv_state_dim_first,
31
+)
32
+from vllm.model_executor.layers.mamba.ops.causal_conv1d import (
33
+ causal_conv1d_fn,
34
+ causal_conv1d_update,
35
+)
36
+from vllm.model_executor.layers.mamba.ops.gather_initial_states import (
37
+ gather_initial_states,
38
+)
39
+from vllm.model_executor.model_loader.weight_utils import sharded_weight_loader
40
+from vllm.model_executor.utils import set_weight_attrs
41
+from vllm.models.kimi_k3.amd.ops.third_party.kda import (
42
+ chunk_kda_with_fused_gate,
43
+ fused_recurrent_kda,
44
+ fused_recurrent_kda_packed_decode,
45
+)
46
+from vllm.third_party.flash_linear_attention.ops.kda import FusedRMSNormGated
47
+from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
48
+from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadata
49
+
50
+
51
+class KimiK3DeltaAttention(GatedDeltaNetAttention):
52
+ def get_state_dtype(self) -> tuple[torch.dtype, torch.dtype]:
53
+ if self.model_config is None or self.cache_config is None:
54
+ raise ValueError("model_config and cache_config must be set")
55
+ return MambaStateDtypeCalculator.kda_state_dtype(
56
+ self.model_config.dtype, self.cache_config.mamba_cache_dtype
57
+ )
58
+
59
+ def get_state_shape(self) -> tuple[tuple[int, ...], tuple[int, ...]]:
60
+ return MambaStateShapeCalculator.kda_state_shape(
61
+ self.tp_size,
62
+ self.num_heads,
63
+ self.head_dim,
64
+ conv_kernel_size=self.conv_size,
65
+ num_spec=self.num_spec,
66
+ )
67
+
68
+ def __init__(
69
+ self,
70
+ config: KimiLinearConfig,
71
+ vllm_config: VllmConfig,
72
+ prefix: str = "",
73
+ ) -> None:
74
+ super().__init__(config, vllm_config, prefix)
75
+
76
+ kda_config = config.linear_attn_config # type: ignore[attr-defined]
77
+ assert kda_config is not None, "linear_attn_config must be set"
78
+ assert kda_config.get("use_full_rank_gate", False), (
79
+ "KimiK3DeltaAttention requires use_full_rank_gate; the low-rank "
80
+ "gate path belongs to the shared Kimi-Linear layer."
81
+ )
82
+
83
+ self.head_dim = kda_config["head_dim"]
84
+ self.num_heads = kda_config["num_heads"]
85
+ assert self.num_heads % self.tp_size == 0
86
+ self.local_num_heads = divide(self.num_heads, self.tp_size)
87
+
88
+ self.projection_size = self.head_dim * self.num_heads
89
+ self.local_projection_size = divide(self.projection_size, self.tp_size)
90
+ self.conv_size = kda_config["short_conv_kernel_size"]
91
+ self.use_full_rank_gate = True
92
+
93
+ # Keep f_a before the narrow beta shard, then pad each TP-local row to
94
+ # select the aligned BF16 GEMM path. The padding also avoids an Inductor
95
+ # correctness issue seen with the row-strided G view.
96
+ qkvg_output_sizes = [self.projection_size] * 4
97
+ in_proj_output_sizes = qkvg_output_sizes + [self.head_dim, self.num_heads]
98
+ local_output_size = (
99
+ 4 * self.local_projection_size + self.head_dim + self.local_num_heads
100
+ )
101
+ self.in_proj_padding = -local_output_size % 16
102
+ if self.in_proj_padding:
103
+ in_proj_output_sizes.append(self.in_proj_padding * self.tp_size)
104
+
105
+ self.in_proj_qkvgfab = _KimiGDNMergedColumnParallelLinear(
106
+ self.hidden_size,
107
+ in_proj_output_sizes,
108
+ replicated_shard_id=4,
109
+ tp_size=self.tp_size,
110
+ bias=False,
111
+ quant_config=self.quant_config,
112
+ prefix=f"{prefix}.in_proj_qkvgfab",
113
+ )
114
+ if self.in_proj_padding:
115
+ self.in_proj_qkvgfab.weight.data[-self.in_proj_padding :].zero_()
116
+
117
+ self.f_b_proj = ColumnParallelLinear(
118
+ self.head_dim,
119
+ self.projection_size,
120
+ bias=False,
121
+ quant_config=self.quant_config,
122
+ prefix=f"{prefix}.f_b_proj",
123
+ )
124
+ self.dt_bias = nn.Parameter(
125
+ torch.empty(self.local_projection_size, dtype=torch.float32)
126
+ )
127
+ set_weight_attrs(self.dt_bias, {"weight_loader": sharded_weight_loader(0)})
128
+
129
+ # One packed parameter and cache let decode run a single conv update.
130
+ # Prefill slices them back into Q/K/V to obtain dense outputs cheaply.
131
+ self.conv1d = ColumnParallelLinear(
132
+ input_size=self.conv_size,
133
+ output_size=3 * self.projection_size,
134
+ bias=False,
135
+ params_dtype=torch.float32,
136
+ prefix=f"{prefix}.conv1d",
137
+ )
138
+ self.conv1d.weight.data = self.conv1d.weight.data.unsqueeze(1)
139
+ 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
+ )
150
+
151
+ self.A_log = nn.Parameter(
152
+ torch.empty(self.local_num_heads, dtype=torch.float32)
153
+ )
154
+ set_weight_attrs(self.A_log, {"weight_loader": a_log_weight_loader(0)})
155
+
156
+ self.gate_lower_bound: float | None = kda_config.get("gate_lower_bound", None)
157
+ if self.gate_lower_bound is not None:
158
+ assert _KDA_GATE_LOGBOUND_MIN <= self.gate_lower_bound < 0, (
159
+ "KDA gate lower bound must be in "
160
+ f"[{_KDA_GATE_LOGBOUND_MIN}, 0). "
161
+ f"Got {self.gate_lower_bound}."
162
+ )
163
+ self.use_safe_gate = self.gate_lower_bound is not None
164
+
165
+ additional_config = vllm_config.additional_config
166
+ backend = (
167
+ additional_config.get("kda_prefill_backend", "auto")
168
+ if isinstance(additional_config, dict)
169
+ else "auto"
170
+ )
171
+ backend = "triton" if backend == "auto" else backend
172
+ assert backend == "triton", (
173
+ "The ROCm Kimi-K3 KDA layer only supports the Triton KDA prefill "
174
+ f"backend, got {backend!r}."
175
+ )
176
+
177
+ self.o_norm = FusedRMSNormGated(self.head_dim, activation="sigmoid")
178
+ self.o_proj = RowParallelLinear(
179
+ self.projection_size,
180
+ self.hidden_size,
181
+ bias=False,
182
+ quant_config=self.quant_config,
183
+ prefix=f"{prefix}.o_proj",
184
+ )
185
+
186
+ compilation_config = vllm_config.compilation_config
187
+ if prefix in compilation_config.static_forward_context:
188
+ raise ValueError(f"Duplicate layer name: {prefix}")
189
+ compilation_config.static_forward_context[prefix] = self
190
+
191
+ def rearrange_mixed_qkv(
192
+ self, mixed_qkv: torch.Tensor
193
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
194
+ seq_len = mixed_qkv.shape[0]
195
+ qkv = mixed_qkv.view(seq_len, 3, self.local_num_heads, self.head_dim)
196
+ # Materialize all three row-strided inputs with one token-major to
197
+ # QKV-major permutation. Each unbound tensor is then contiguous.
198
+ qkv = qkv.permute(1, 0, 2, 3).contiguous().unsqueeze(1)
199
+ return qkv.unbind(0)
200
+
201
+ def forward(
202
+ self,
203
+ hidden_states: torch.Tensor,
204
+ positions: torch.Tensor,
205
+ output: torch.Tensor,
206
+ ) -> None:
207
+ num_tokens = hidden_states.size(0)
208
+ projected_qkvgfab = self.in_proj_qkvgfab(hidden_states)[0]
209
+
210
+ split_sizes = [
211
+ 3 * self.local_projection_size,
212
+ self.local_projection_size,
213
+ self.head_dim,
214
+ self.local_num_heads,
215
+ ]
216
+ if self.in_proj_padding:
217
+ split_sizes.append(self.in_proj_padding)
218
+ projected = projected_qkvgfab.split(split_sizes, dim=-1)
219
+ mixed_qkv, g_proj_states, f_a, beta = projected[:4]
220
+
221
+ g1 = self.f_b_proj(f_a)[0]
222
+ beta = beta.unsqueeze(0)
223
+ g1 = rearrange(g1, "n (h d) -> 1 n h d", d=self.head_dim)
224
+ g2 = rearrange(g_proj_states, "... (h d) -> ... h d", d=self.head_dim)
225
+
226
+ core_attn_out = torch.empty(
227
+ (1, num_tokens, self.local_num_heads, self.head_dim),
228
+ dtype=hidden_states.dtype,
229
+ device=hidden_states.device,
230
+ )
231
+
232
+ self._forward(
233
+ mixed_qkv=mixed_qkv,
234
+ g1=g1,
235
+ g2=g2,
236
+ beta=beta,
237
+ core_attn_out=core_attn_out,
238
+ )
239
+ core_attn_out = rearrange(core_attn_out, "1 n h d -> n (h d)")
240
+ output[:] = self.o_proj(core_attn_out)[0]
241
+
242
+ @eager_break_during_capture
243
+ def _forward(
244
+ self,
245
+ mixed_qkv: torch.Tensor,
246
+ g1: torch.Tensor,
247
+ g2: torch.Tensor,
248
+ beta: torch.Tensor,
249
+ core_attn_out: torch.Tensor,
250
+ ) -> None:
251
+ forward_context = get_forward_context()
252
+ attn_metadata_raw = forward_context.attn_metadata
253
+
254
+ if attn_metadata_raw is None:
255
+ return
256
+
257
+ assert isinstance(attn_metadata_raw, dict)
258
+ attn_metadata_narrowed = attn_metadata_raw[self.prefix]
259
+ assert isinstance(attn_metadata_narrowed, GDNAttentionMetadata)
260
+ m = attn_metadata_narrowed
261
+ has_initial_state = m.has_initial_state
262
+ non_spec_query_start_loc = m.non_spec_query_start_loc
263
+ non_spec_state_indices_tensor = m.non_spec_state_indices_tensor
264
+ spec_sequence_masks = m.spec_sequence_masks
265
+ spec_token_indx = m.spec_token_indx
266
+ non_spec_token_indx = m.non_spec_token_indx
267
+ spec_state_indices_tensor = m.spec_state_indices_tensor
268
+ spec_query_start_loc = m.spec_query_start_loc
269
+ num_accepted_tokens = m.num_accepted_tokens
270
+ num_actual_tokens = m.num_actual_tokens
271
+ mixed_qkv = mixed_qkv[:num_actual_tokens]
272
+ g1 = g1[:, :num_actual_tokens]
273
+ beta = beta[:, :num_actual_tokens]
274
+
275
+ constant_caches = self.kv_cache
276
+
277
+ conv_state, recurrent_state = constant_caches
278
+ # conv_state must be (..., dim, width-1) for the conv kernels.
279
+ # DS layout stores it that way directly; SD layout needs a transpose.
280
+ if not is_conv_state_dim_first():
281
+ conv_state = conv_state.transpose(-1, -2)
282
+
283
+ conv_weights = self.conv1d.weight.view(
284
+ self.conv1d.weight.size(0), self.conv1d.weight.size(2)
285
+ )
286
+ q_conv_weight, k_conv_weight, v_conv_weight = conv_weights.split(
287
+ self.local_projection_size, dim=0
288
+ )
289
+ q_conv_state, k_conv_state, v_conv_state = conv_state.split(
290
+ self.local_projection_size, dim=-2
291
+ )
292
+
293
+ # Split tokens into the multi-query spec-decode part and the remaining
294
+ # (prefill / plain decode) part.
295
+ if spec_sequence_masks is not None:
296
+ if m.num_prefills == 0 and m.num_decodes == 0:
297
+ mixed_qkv_spec = mixed_qkv
298
+ g1_spec, beta_spec = g1, beta
299
+ mixed_qkv_ns = g1_ns = beta_ns = None
300
+ else:
301
+ mixed_qkv_spec = mixed_qkv.index_select(0, spec_token_indx)
302
+ g1_spec = g1.index_select(1, spec_token_indx)
303
+ beta_spec = beta.index_select(1, spec_token_indx)
304
+ mixed_qkv_ns = mixed_qkv.index_select(0, non_spec_token_indx)
305
+ g1_ns = g1.index_select(1, non_spec_token_indx)
306
+ beta_ns = beta.index_select(1, non_spec_token_indx)
307
+ else:
308
+ mixed_qkv_spec = g1_spec = beta_spec = None
309
+ mixed_qkv_ns, g1_ns, beta_ns = mixed_qkv, g1, beta
310
+
311
+ # ---------- spec-decode multi-query path ----------
312
+ core_attn_out_spec = None
313
+ if spec_sequence_masks is not None:
314
+ assert spec_state_indices_tensor is not None
315
+ assert spec_query_start_loc is not None
316
+ spec_conv_indices = spec_state_indices_tensor[:, 0][: m.num_spec_decodes]
317
+ spec_max_query_len = spec_state_indices_tensor.size(-1)
318
+
319
+ # Sibling beta and, for full-rank gates, output-gate views remain
320
+ # live, so write the convolution output separately.
321
+ spec_conv_out = torch.empty(
322
+ mixed_qkv_spec.shape,
323
+ dtype=mixed_qkv_spec.dtype,
324
+ device=mixed_qkv_spec.device,
325
+ )
326
+ mixed_qkv_spec = causal_conv1d_update(
327
+ mixed_qkv_spec,
328
+ conv_state,
329
+ conv_weights,
330
+ self.conv1d.bias,
331
+ activation="silu",
332
+ conv_state_indices=spec_conv_indices,
333
+ num_accepted_tokens=num_accepted_tokens,
334
+ query_start_loc=spec_query_start_loc,
335
+ max_query_len=spec_max_query_len,
336
+ validate_data=False,
337
+ out=spec_conv_out,
338
+ )
339
+ q_spec, k_spec, v_spec = (
340
+ rearrange(x, "n (h d) -> 1 n h d", d=self.head_dim)
341
+ for x in mixed_qkv_spec.split(self.local_projection_size, dim=-1)
342
+ )
343
+ spec_cu_seqlens = spec_query_start_loc[: m.num_spec_decodes + 1]
344
+ # Spec-only batches write directly into core_attn_out.
345
+ spec_out = (
346
+ core_attn_out[:, : q_spec.shape[1]]
347
+ if m.num_prefills == 0 and m.num_decodes == 0
348
+ else None
349
+ )
350
+ core_attn_out_spec, _ = fused_recurrent_kda(
351
+ q=q_spec,
352
+ k=k_spec,
353
+ v=v_spec,
354
+ raw_g=g1_spec,
355
+ raw_beta=beta_spec,
356
+ A_log=self.A_log,
357
+ dt_bias=self.dt_bias,
358
+ lower_bound=self.gate_lower_bound,
359
+ initial_state=recurrent_state,
360
+ cu_seqlens=spec_cu_seqlens,
361
+ ssm_state_indices=spec_state_indices_tensor,
362
+ num_accepted_tokens=num_accepted_tokens,
363
+ out=spec_out,
364
+ )
365
+
366
+ # ---------- non-spec path (prefill or plain decode) ----------
367
+ core_attn_out_non_spec = None
368
+ if mixed_qkv_ns is not None:
369
+ assert g1_ns is not None and beta_ns is not None
370
+ if m.num_prefills > 0:
371
+ q_ns, k_ns, v_ns = mixed_qkv_ns.split(
372
+ self.local_projection_size, dim=-1
373
+ )
374
+
375
+ # Packed prefill conv would require copying V solely to make
376
+ # it dense for KDA. Separate calls accept the strided inputs
377
+ # and produce dense Q/K/V without that extra traffic.
378
+ # TODO: Use packed conv once every KDA prefill backend accepts
379
+ # row-strided Q/K/V directly.
380
+ def _prefill_conv(
381
+ x: torch.Tensor,
382
+ state: torch.Tensor,
383
+ weight: torch.Tensor,
384
+ ) -> torch.Tensor:
385
+ return causal_conv1d_fn(
386
+ x.transpose(0, 1),
387
+ weight,
388
+ None,
389
+ activation="silu",
390
+ conv_states=state,
391
+ has_initial_state=has_initial_state,
392
+ cache_indices=non_spec_state_indices_tensor,
393
+ query_start_loc=non_spec_query_start_loc,
394
+ metadata=m,
395
+ ).transpose(0, 1)
396
+
397
+ q_ns = _prefill_conv(q_ns, q_conv_state, q_conv_weight)
398
+ k_ns = _prefill_conv(k_ns, k_conv_state, k_conv_weight)
399
+ v_ns = _prefill_conv(v_ns, v_conv_state, v_conv_weight)
400
+ q_ns, k_ns, v_ns = (
401
+ rearrange(x, "n (h d) -> 1 n h d", d=self.head_dim)
402
+ for x in (q_ns, k_ns, v_ns)
403
+ )
404
+
405
+ assert non_spec_state_indices_tensor is not None
406
+ assert has_initial_state is not None
407
+
408
+ # A mixed non-spec batch is decode-first: the decodes are
409
+ # length-1 sequences, and the chunk kernel returns NaN for those.
410
+ # Send them to the recurrent kernel and give the chunk kernel the
411
+ # prefill tail only.
412
+ core_attn_out_decode = None
413
+ split_non_spec = spec_sequence_masks is None and m.num_decodes > 0
414
+ if split_non_spec:
415
+ assert non_spec_query_start_loc is not None
416
+ nd_tok = m.num_decode_tokens
417
+ core_attn_out_decode, _ = fused_recurrent_kda(
418
+ q=q_ns[:, :nd_tok],
419
+ k=k_ns[:, :nd_tok],
420
+ v=v_ns[:, :nd_tok],
421
+ raw_g=g1_ns[:, :nd_tok],
422
+ raw_beta=beta_ns[:, :nd_tok],
423
+ A_log=self.A_log,
424
+ dt_bias=self.dt_bias,
425
+ lower_bound=self.gate_lower_bound,
426
+ initial_state=recurrent_state,
427
+ cu_seqlens=non_spec_query_start_loc[: m.num_decodes + 1],
428
+ ssm_state_indices=non_spec_state_indices_tensor[
429
+ : m.num_decodes
430
+ ],
431
+ )
432
+ q_ns = q_ns[:, nd_tok:]
433
+ k_ns = k_ns[:, nd_tok:]
434
+ v_ns = v_ns[:, nd_tok:]
435
+ g1_ns = g1_ns[:, nd_tok:]
436
+ beta_ns = beta_ns[:, nd_tok:]
437
+ prefill_query_start_loc = m.prefill_query_start_loc
438
+ prefill_state_indices = m.prefill_state_indices
439
+ prefill_has_initial_state = m.prefill_has_initial_state
440
+ assert prefill_query_start_loc is not None
441
+ assert prefill_state_indices is not None
442
+ assert prefill_has_initial_state is not None
443
+ else:
444
+ prefill_query_start_loc = non_spec_query_start_loc
445
+ prefill_state_indices = non_spec_state_indices_tensor
446
+ prefill_has_initial_state = has_initial_state
447
+
448
+ initial_state = gather_initial_states(
449
+ recurrent_state,
450
+ prefill_state_indices,
451
+ prefill_has_initial_state,
452
+ )
453
+ (
454
+ core_attn_out_non_spec,
455
+ last_recurrent_state,
456
+ ) = chunk_kda_with_fused_gate(
457
+ q=q_ns,
458
+ k=k_ns,
459
+ v=v_ns,
460
+ raw_g=g1_ns,
461
+ raw_beta=beta_ns,
462
+ A_log=self.A_log,
463
+ g_bias=self.dt_bias,
464
+ lower_bound=self.gate_lower_bound,
465
+ initial_state=initial_state,
466
+ output_final_state=True,
467
+ use_qk_l2norm_in_kernel=True,
468
+ cu_seqlens=prefill_query_start_loc,
469
+ )
470
+ # Init cache
471
+ recurrent_state[prefill_state_indices] = last_recurrent_state
472
+
473
+ if split_non_spec:
474
+ # Restore decode-first token order for the merge below.
475
+ core_attn_out_non_spec = torch.cat(
476
+ [core_attn_out_decode, core_attn_out_non_spec], dim=1
477
+ )
478
+
479
+ else:
480
+ # pure-decode non-spec batch
481
+ assert non_spec_state_indices_tensor is not None
482
+ decode_conv_indices = non_spec_state_indices_tensor[
483
+ : mixed_qkv_ns.size(0)
484
+ ]
485
+ # Sibling beta and, for full-rank gates, output-gate views
486
+ # remain live, so write the conv output separately.
487
+ packed_conv_out = torch.empty(
488
+ mixed_qkv_ns.shape,
489
+ dtype=mixed_qkv_ns.dtype,
490
+ device=mixed_qkv_ns.device,
491
+ )
492
+ mixed_qkv_ns = causal_conv1d_update(
493
+ mixed_qkv_ns,
494
+ conv_state,
495
+ conv_weights,
496
+ self.conv1d.bias,
497
+ activation="silu",
498
+ conv_state_indices=decode_conv_indices,
499
+ validate_data=True,
500
+ out=packed_conv_out,
501
+ )
502
+ core_attn_out_non_spec, _ = fused_recurrent_kda_packed_decode(
503
+ mixed_qkv=mixed_qkv_ns,
504
+ raw_g=g1_ns,
505
+ raw_beta=beta_ns,
506
+ A_log=self.A_log,
507
+ dt_bias=self.dt_bias,
508
+ lower_bound=self.gate_lower_bound,
509
+ initial_state=recurrent_state,
510
+ state_indices=decode_conv_indices,
511
+ )
512
+
513
+ # ---------- merge spec and non-spec outputs ----------
514
+ if core_attn_out_spec is not None and core_attn_out_non_spec is not None:
515
+ # Mixed batches require indexed placement in the original order.
516
+ merged = torch.empty(
517
+ (1, num_actual_tokens, *core_attn_out_spec.shape[2:]),
518
+ dtype=core_attn_out_spec.dtype,
519
+ device=core_attn_out_spec.device,
520
+ )
521
+ merged.index_copy_(1, spec_token_indx, core_attn_out_spec)
522
+ merged.index_copy_(1, non_spec_token_indx, core_attn_out_non_spec)
523
+ core_attn_out[0, :num_actual_tokens] = merged[0, :num_actual_tokens]
524
+ elif core_attn_out_non_spec is not None:
525
+ core_attn_out[0, :num_actual_tokens] = core_attn_out_non_spec[
526
+ 0, :num_actual_tokens
527
+ ]
528
+ else:
529
+ assert core_attn_out_spec is not None
530
+ core_attn_out.copy_(self.o_norm(core_attn_out, g2))
vllm/models/kimi_k3/amd/linear.py
+18
-6
@@ -28,7 +28,7 @@ from vllm.model_executor.layers.linear import (
28
)
29
from vllm.model_executor.layers.logits_processor import LogitsProcessor
30
from vllm.model_executor.layers.mamba.gdn.kimi_gdn_linear_attn import (
31
- KimiGatedDeltaNetAttention,
31
+ KimiGatedDeltaNetAttention as KimiLinearGatedDeltaNetAttention,
32
)
33
from vllm.model_executor.layers.mamba.mamba_utils import (
34
MambaStateCopyFunc,
@@ -61,6 +61,7 @@ from vllm.model_executor.models.utils import (
61
make_layers,
62
maybe_prefix,
63
)
64
+from vllm.models.kimi_k3.amd.kda import KimiK3DeltaAttention
65
from vllm.models.kimi_k3.amd.ops.attn_res import attn_res
66
from vllm.sequence import IntermediateTensors
67
from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
@@ -477,11 +478,22 @@ class KimiDecoderLayer(nn.Module):
478
quant_config = vllm_config.quant_config
479
480
if config.is_kda_layer(layer_idx):
480
- self.self_attn = KimiGatedDeltaNetAttention(
481
- config,
482
- vllm_config,
483
- prefix=f"{prefix}.self_attn",
484
- )
481
+ # Kimi-K3 sets use_full_rank_gate and uses the ROCm-specific K3 KDA
482
+ # layer; Kimi-Linear keeps the shared low-rank-gate implementation.
483
+ kda_config = config.linear_attn_config
484
+ assert kda_config is not None
485
+ if kda_config.get("use_full_rank_gate", False):
486
+ self.self_attn = KimiK3DeltaAttention(
487
+ config,
488
+ vllm_config,
489
+ prefix=f"{prefix}.self_attn",
490
+ )
491
+ else:
492
+ self.self_attn = KimiLinearGatedDeltaNetAttention(
493
+ config,
494
+ vllm_config,
495
+ prefix=f"{prefix}.self_attn",
496
+ )
497
else:
498
qk_nope_head_dim = config.qk_nope_head_dim
499
qk_rope_head_dim = config.qk_rope_head_dim
vllm/models/kimi_k3/amd/ops/third_party/kda/chunk.py
+8
-1
@@ -28,6 +28,13 @@ BT_LIST_AUTOTUNE = [32, 64, 128]
28
NUM_WARPS_AUTOTUNE = [2, 4, 8, 16] if is_amd else [4, 8, 16, 32]
29
30
31
+# num_stages=4 is excluded. The `u` loop has a trip count of 2 (V / BV), and a
32
+# 4-stage pipeline over it emits a third, consumer-less async copy into LDS that
33
+# the `w` loop then reuses; at num_warps=4 that races on gfx950 and `u` comes
34
+# back with non-deterministic O(1e38) garbage once a batch reaches 4096 tokens.
35
+_RECOMPUTE_W_U_NUM_STAGES = [2, 3]
36
+
37
+
38
@triton.heuristics(
39
{
40
"STORE_QG": lambda args: args["qg"] is not None,
@@ -39,7 +46,7 @@ NUM_WARPS_AUTOTUNE = [2, 4, 8, 16] if is_amd else [4, 8, 16, 32]
46
configs=[
47
triton.Config({}, num_warps=num_warps, num_stages=num_stages)
48
for num_warps in [2, 4, 8]
42
- for num_stages in [2, 3, 4]
49
+ for num_stages in _RECOMPUTE_W_U_NUM_STAGES
50
],
51
key=["H", "K", "V", "BT", "BK", "BV", "IS_VARLEN"],
52
)