1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+from collections.abc import Callable
5
+
6
import torch
7
from einops import rearrange
8
from torch import nn
9
+from torch.nn.parameter import Parameter
10
8
-from vllm.config import VllmConfig, get_current_vllm_config
9
-from vllm.distributed import (
10
- divide,
11
-)
12
-from vllm.forward_context import ForwardContext, get_forward_context
13
-from vllm.logger import init_logger
11
+from vllm.compilation.breakable_cudagraph import eager_break_during_capture
12
+from vllm.config import VllmConfig
13
+from vllm.distributed import divide, get_tensor_model_parallel_rank
14
+from vllm.forward_context import get_forward_context
15
from vllm.model_executor.custom_op import PluggableLayer
16
from vllm.model_executor.layers.mamba.gdn.base import GatedDeltaNetAttention
16
-from vllm.model_executor.model_loader.weight_utils import sharded_weight_loader
17
-from vllm.model_executor.utils import set_weight_attrs
18
-from vllm.third_party.flash_linear_attention.ops.kda import (
19
- FusedRMSNormGated,
20
- chunk_kda_with_fused_gate,
21
- fused_kda_gate,
22
- fused_recurrent_kda,
17
+from vllm.model_executor.model_loader.weight_utils import (
18
+ default_weight_loader,
19
+ sharded_weight_loader,
20
)
21
+from vllm.model_executor.parameter import BasevLLMParameter
22
+from vllm.model_executor.utils import set_weight_attrs
23
+from vllm.platforms import current_platform
24
+from vllm.third_party.flash_linear_attention.ops.kda import FusedRMSNormGated
25
from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
25
-from vllm.utils.torch_utils import direct_register_custom_op
26
from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadata
27
28
from ...linear import (
29
ColumnParallelLinear,
30
+ MergedColumnParallelLinear,
31
ReplicatedLinear,
32
RowParallelLinear,
33
)
37
is_conv_state_dim_first,
38
)
39
from ..ops.causal_conv1d import causal_conv1d_fn, causal_conv1d_update
40
+from ..ops.gather_initial_states import gather_initial_states
41
40
-logger = init_logger(__name__)
41
-
42
-
43
-def kda_attention(
44
- q_proj_states: torch.Tensor,
45
- k_proj_states: torch.Tensor,
46
- v_proj_states: torch.Tensor,
47
- g1: torch.Tensor,
48
- beta: torch.Tensor,
49
- core_attn_out: torch.Tensor,
50
- layer_name: str,
51
-) -> None:
52
- forward_context: ForwardContext = get_forward_context()
53
- self = forward_context.no_compile_layers[layer_name]
54
- self._forward(
55
- q_proj_states=q_proj_states,
56
- k_proj_states=k_proj_states,
57
- v_proj_states=v_proj_states,
58
- g1=g1,
59
- beta=beta,
60
- core_attn_out=core_attn_out,
61
- )
62
-
63
-
64
-def kda_attention_fake(
65
- q_proj_states: torch.Tensor,
66
- k_proj_states: torch.Tensor,
67
- v_proj_states: torch.Tensor,
68
- g1: torch.Tensor,
69
- beta: torch.Tensor,
70
- core_attn_out: torch.Tensor,
71
- layer_name: str,
72
-) -> None:
73
- return
74
-
75
-
76
-direct_register_custom_op(
77
- op_name="kda_attention",
78
- op_func=kda_attention,
79
- mutates_args=["core_attn_out"],
80
- fake_impl=kda_attention_fake,
81
-)
42
+# Empirical lower bound for the KDA gate to avoid numerical underflow.
43
+_KDA_GATE_LOGBOUND_MIN = -5.0
44
+
45
+
46
+def a_log_weight_loader(
47
+ shard_axis: int,
48
+) -> Callable[[torch.Tensor, torch.Tensor], None]:
49
+ """Load KDA A_log stored as either old 4D or current 1D weights."""
50
+
51
+ def loader(param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
52
+ tp_rank = get_tensor_model_parallel_rank()
53
+ shard_size = param.data.shape[shard_axis]
54
+ start_idx = tp_rank * shard_size
55
+
56
+ if loaded_weight.dim() == 4:
57
+ assert loaded_weight.shape[:2] == (1, 1), (
58
+ f"Expected old A_log shape (1, 1, H, 1), got {loaded_weight.shape}"
59
+ )
60
+ assert loaded_weight.shape[-1] == 1, (
61
+ f"Expected old A_log last dim to be 1, got {loaded_weight.shape}"
62
+ )
63
+ loaded_weight = loaded_weight.view(loaded_weight.shape[2])
64
+
65
+ loaded_weight = loaded_weight.narrow(shard_axis, start_idx, shard_size)
66
+ return default_weight_loader(param, loaded_weight)
67
+
68
+ return loader
69
+
70
+
71
+def _make_fused_conv1d_weight_loader(
72
+ dims: list[int],
73
+ tp_size: int,
74
+ tp_rank: int,
75
+) -> Callable[..., None]:
76
+ sharded_dims = [dim // tp_size for dim in dims]
77
+
78
+ def weight_loader(
79
+ param: torch.Tensor,
80
+ loaded_weight: torch.Tensor,
81
+ loaded_shard_id: int,
82
+ ) -> None:
83
+ if loaded_weight.dim() == 2:
84
+ loaded_weight = loaded_weight.unsqueeze(1)
85
+ shard_size = sharded_dims[loaded_shard_id]
86
+ source_start = tp_rank * shard_size
87
+ target_start = sum(sharded_dims[:loaded_shard_id])
88
+ loaded_shard = loaded_weight[source_start : source_start + shard_size]
89
+ param.data[target_start : target_start + shard_size].copy_(loaded_shard)
90
+
91
+ return weight_loader
92
+
93
+
94
+class _KimiGDNMergedColumnParallelLinear(MergedColumnParallelLinear):
95
+ """Merged projection with one output replicated across TP ranks.
96
+
97
+ The replicated shard is represented as ``size * tp_size`` so the merged
98
+ parameter reserves ``size`` local rows on every rank. Loading that shard
99
+ from rank zero then gives every rank the complete checkpoint weight.
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ input_size: int,
105
+ output_sizes: list[int],
106
+ replicated_shard_id: int,
107
+ tp_size: int,
108
+ **kwargs,
109
+ ) -> None:
110
+ self.replicated_shard_id = replicated_shard_id
111
+ output_sizes = output_sizes.copy()
112
+ output_sizes[replicated_shard_id] *= tp_size
113
+ super().__init__(input_size, output_sizes, **kwargs)
114
+
115
+ def weight_loader(
116
+ self,
117
+ param: Parameter,
118
+ loaded_weight: torch.Tensor,
119
+ loaded_shard_id: tuple[int, ...] | int | None = None,
120
+ ) -> None:
121
+ tp_rank = self.tp_rank
122
+ param_tp_rank = getattr(param, "tp_rank", None)
123
+ if loaded_shard_id == self.replicated_shard_id:
124
+ self.tp_rank = 0
125
+ if param_tp_rank is not None:
126
+ param.tp_rank = 0
127
+ try:
128
+ super().weight_loader(param, loaded_weight, loaded_shard_id)
129
+ finally:
130
+ self.tp_rank = tp_rank
131
+ if param_tp_rank is not None:
132
+ param.tp_rank = param_tp_rank
133
+
134
+ def weight_loader_v2(
135
+ self,
136
+ param: BasevLLMParameter,
137
+ loaded_weight: torch.Tensor,
138
+ loaded_shard_id: tuple[int, ...] | int | None = None,
139
+ ) -> None:
140
+ tp_rank = self.tp_rank
141
+ param_tp_rank = getattr(param, "tp_rank", None)
142
+ if loaded_shard_id == self.replicated_shard_id:
143
+ self.tp_rank = 0
144
+ if param_tp_rank is not None:
145
+ param.tp_rank = 0
146
+ try:
147
+ super().weight_loader_v2(param, loaded_weight, loaded_shard_id)
148
+ finally:
149
+ self.tp_rank = tp_rank
150
+ if param_tp_rank is not None:
151
+ param.tp_rank = param_tp_rank
152
153
154
@PluggableLayer.register("kimi_gated_delta_net_attention")
166
self,
167
) -> tuple[tuple[int, ...], tuple[int, ...]]:
168
return MambaStateShapeCalculator.kda_state_shape(
99
- self.tp_size, self.num_heads, self.head_dim, conv_kernel_size=self.conv_size
169
+ self.tp_size,
170
+ self.num_heads,
171
+ self.head_dim,
172
+ conv_kernel_size=self.conv_size,
173
+ num_spec=self.num_spec,
174
)
175
176
def __init__(
188
assert self.num_heads % self.tp_size == 0
189
self.local_num_heads = divide(self.num_heads, self.tp_size)
190
117
- projection_size = self.head_dim * self.num_heads
191
+ self.projection_size = self.head_dim * self.num_heads
192
+ self.local_projection_size = divide(self.projection_size, self.tp_size)
193
self.conv_size = kda_config["short_conv_kernel_size"]
119
-
120
- self.q_proj = ColumnParallelLinear(
121
- self.hidden_size,
122
- projection_size,
123
- bias=False,
124
- quant_config=self.quant_config,
125
- prefix=f"{prefix}.q_proj",
126
- )
127
- self.k_proj = ColumnParallelLinear(
128
- self.hidden_size,
129
- projection_size,
130
- bias=False,
131
- quant_config=self.quant_config,
132
- prefix=f"{prefix}.k_proj",
133
- )
134
- self.v_proj = ColumnParallelLinear(
135
- self.hidden_size,
136
- projection_size,
137
- bias=False,
138
- quant_config=self.quant_config,
139
- prefix=f"{prefix}.v_proj",
140
- )
141
-
142
- self.f_a_proj = ReplicatedLinear(
194
+ self.use_full_rank_gate = kda_config.get("use_full_rank_gate", False)
195
+
196
+ if self.use_full_rank_gate:
197
+ # Keep f_a before the narrow beta shard, then pad each TP-local row
198
+ # to select the aligned BF16 GEMM path. The padding also avoids an
199
+ # Inductor correctness issue seen with the row-strided G view.
200
+ qkvg_output_sizes = [self.projection_size] * 4
201
+ in_proj_output_sizes = qkvg_output_sizes + [
202
+ self.head_dim,
203
+ self.num_heads,
204
+ ]
205
+ local_output_size = (
206
+ 4 * self.local_projection_size + self.head_dim + self.local_num_heads
207
+ )
208
+ self.in_proj_padding = -local_output_size % 16
209
+ if self.in_proj_padding:
210
+ in_proj_output_sizes.append(self.in_proj_padding * self.tp_size)
211
+ else:
212
+ in_proj_output_sizes = [self.projection_size] * 3 + [
213
+ self.num_heads,
214
+ self.head_dim,
215
+ ]
216
+ self.in_proj_padding = 0
217
+ self.in_proj_qkvgfab = _KimiGDNMergedColumnParallelLinear(
218
self.hidden_size,
144
- self.head_dim,
219
+ in_proj_output_sizes,
220
+ replicated_shard_id=4,
221
+ tp_size=self.tp_size,
222
bias=False,
223
quant_config=self.quant_config,
147
- prefix=f"{prefix}.f_a_proj",
224
+ prefix=f"{prefix}.in_proj_qkvgfab",
225
)
226
+ if self.in_proj_padding:
227
+ self.in_proj_qkvgfab.weight.data[-self.in_proj_padding :].zero_()
228
229
self.f_b_proj = ColumnParallelLinear(
230
self.head_dim,
152
- projection_size,
231
+ self.projection_size,
232
bias=False,
233
quant_config=self.quant_config,
234
prefix=f"{prefix}.f_b_proj",
235
)
236
self.dt_bias = nn.Parameter(
158
- torch.empty(divide(projection_size, self.tp_size), dtype=torch.float32)
237
+ torch.empty(self.local_projection_size, dtype=torch.float32)
238
)
239
240
set_weight_attrs(self.dt_bias, {"weight_loader": sharded_weight_loader(0)})
241
163
- self.b_proj = ColumnParallelLinear(
164
- self.hidden_size,
165
- self.num_heads,
166
- bias=False,
167
- quant_config=self.quant_config,
168
- prefix=f"{prefix}.b_proj",
169
- )
170
-
171
- self.q_conv1d = ColumnParallelLinear(
242
+ # One packed parameter and cache let decode run a single conv update.
243
+ # Prefill slices them back into Q/K/V to obtain dense outputs cheaply.
244
+ self.conv1d = ColumnParallelLinear(
245
input_size=self.conv_size,
173
- output_size=projection_size,
246
+ output_size=3 * self.projection_size,
247
bias=False,
248
params_dtype=torch.float32,
176
- prefix=f"{prefix}.q_conv1d",
249
+ prefix=f"{prefix}.conv1d",
250
)
178
- self.k_conv1d = ColumnParallelLinear(
179
- input_size=self.conv_size,
180
- output_size=projection_size,
181
- bias=False,
182
- params_dtype=torch.float32,
183
- prefix=f"{prefix}.k_conv1d",
251
+ self.conv1d.weight.data = self.conv1d.weight.data.unsqueeze(1)
252
+ delattr(self.conv1d.weight, "weight_loader")
253
+ set_weight_attrs(
254
+ self.conv1d.weight,
255
+ {
256
+ "weight_loader": _make_fused_conv1d_weight_loader(
257
+ [self.projection_size] * 3,
258
+ self.tp_size,
259
+ self.tp_rank,
260
+ )
261
+ },
262
)
185
- self.v_conv1d = ColumnParallelLinear(
186
- input_size=self.conv_size,
187
- output_size=projection_size,
188
- bias=False,
189
- params_dtype=torch.float32,
190
- prefix=f"{prefix}.v_conv1d",
191
- )
192
- # unsqueeze to fit conv1d weights shape into the linear weights shape.
193
- # Can't do this in `weight_loader` since it already exists in
194
- # `ColumnParallelLinear` and `set_weight_attrs`
195
- # doesn't allow to override it
196
- self.q_conv1d.weight.data = self.q_conv1d.weight.data.unsqueeze(1)
197
- self.k_conv1d.weight.data = self.k_conv1d.weight.data.unsqueeze(1)
198
- self.v_conv1d.weight.data = self.v_conv1d.weight.data.unsqueeze(1)
263
264
self.A_log = nn.Parameter(
201
- torch.empty(1, 1, self.local_num_heads, 1, dtype=torch.float32)
265
+ torch.empty(self.local_num_heads, dtype=torch.float32)
266
)
203
- set_weight_attrs(self.A_log, {"weight_loader": sharded_weight_loader(2)})
204
-
205
- self.g_a_proj = ReplicatedLinear(
206
- self.hidden_size,
207
- self.head_dim,
208
- bias=False,
209
- quant_config=self.quant_config,
210
- prefix=f"{prefix}.g_a_proj",
267
+ set_weight_attrs(self.A_log, {"weight_loader": a_log_weight_loader(0)})
268
+
269
+ self.gate_lower_bound: float | None = kda_config.get("gate_lower_bound", None)
270
+ if self.gate_lower_bound is not None:
271
+ assert _KDA_GATE_LOGBOUND_MIN <= self.gate_lower_bound < 0, (
272
+ "KDA gate lower bound must be in "
273
+ f"[{_KDA_GATE_LOGBOUND_MIN}, 0). "
274
+ f"Got {self.gate_lower_bound}."
275
+ )
276
+ self.use_safe_gate = self.gate_lower_bound is not None
277
+ additional_config = vllm_config.additional_config
278
+ backend = (
279
+ additional_config.get("kda_prefill_backend", "auto")
280
+ if isinstance(additional_config, dict)
281
+ else "auto"
282
)
212
- self.g_b_proj = ColumnParallelLinear(
213
- self.head_dim,
214
- projection_size,
215
- bias=False,
216
- quant_config=self.quant_config,
217
- prefix=f"{prefix}.g_b_proj",
283
+ backend = "triton" if backend == "auto" else backend
284
+ assert backend == "triton", (
285
+ "The shared Kimi GDN layer only supports the Triton KDA "
286
+ f"prefill backend, got {backend!r}."
287
)
288
+ if not self.use_full_rank_gate:
289
+ self.g_a_proj = ReplicatedLinear(
290
+ self.hidden_size,
291
+ self.head_dim,
292
+ bias=False,
293
+ quant_config=self.quant_config,
294
+ prefix=f"{prefix}.g_a_proj",
295
+ )
296
+ self.g_b_proj = ColumnParallelLinear(
297
+ self.head_dim,
298
+ self.projection_size,
299
+ bias=False,
300
+ quant_config=self.quant_config,
301
+ prefix=f"{prefix}.g_b_proj",
302
+ )
303
self.o_norm = FusedRMSNormGated(self.head_dim, activation="sigmoid")
304
self.o_proj = RowParallelLinear(
221
- projection_size,
305
+ self.projection_size,
306
self.hidden_size,
307
bias=False,
308
quant_config=self.quant_config,
309
prefix=f"{prefix}.o_proj",
310
)
311
228
- compilation_config = get_current_vllm_config().compilation_config
312
+ compilation_config = vllm_config.compilation_config
313
if prefix in compilation_config.static_forward_context:
314
raise ValueError(f"Duplicate layer name: {prefix}")
315
compilation_config.static_forward_context[prefix] = self
316
317
+ def rearrange_mixed_qkv(
318
+ self, mixed_qkv: torch.Tensor
319
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
320
+ seq_len = mixed_qkv.shape[0]
321
+ qkv = mixed_qkv.view(seq_len, 3, self.local_num_heads, self.head_dim)
322
+ # Materialize all three row-strided inputs with one token-major to
323
+ # QKV-major permutation. Each unbound tensor is then contiguous.
324
+ qkv = qkv.permute(1, 0, 2, 3).contiguous().unsqueeze(1)
325
+ return qkv.unbind(0)
326
+
327
def forward(
328
self,
329
hidden_states: torch.Tensor,
331
output: torch.Tensor,
332
) -> None:
333
num_tokens = hidden_states.size(0)
240
- q = self.q_proj(hidden_states)[0]
241
- k = self.k_proj(hidden_states)[0]
242
- v = self.v_proj(hidden_states)[0]
334
+ projected_qkvgfab = self.in_proj_qkvgfab(hidden_states)[0]
335
+ if self.use_full_rank_gate:
336
+ split_sizes = [
337
+ 3 * self.local_projection_size,
338
+ self.local_projection_size,
339
+ self.head_dim,
340
+ self.local_num_heads,
341
+ ]
342
+ if self.in_proj_padding:
343
+ split_sizes.append(self.in_proj_padding)
344
+ projected = projected_qkvgfab.split(split_sizes, dim=-1)
345
+ mixed_qkv, g_proj_states, f_a, beta = projected[:4]
346
+ else:
347
+ mixed_qkv, beta, f_a = projected_qkvgfab.split(
348
+ [
349
+ 3 * self.local_projection_size,
350
+ self.local_num_heads,
351
+ self.head_dim,
352
+ ],
353
+ dim=-1,
354
+ )
355
+ g_proj_states = self.g_b_proj(self.g_a_proj(hidden_states)[0])[0]
356
244
- beta = self.b_proj(hidden_states)[0].float().sigmoid()
245
- g1 = self.f_b_proj(self.f_a_proj(hidden_states)[0])[0]
357
+ g1 = self.f_b_proj(f_a)[0]
358
beta = beta.unsqueeze(0)
359
g1 = rearrange(g1, "n (h d) -> 1 n h d", d=self.head_dim)
360
249
- g_proj_states = self.g_b_proj(self.g_a_proj(hidden_states)[0])[0]
361
g2 = rearrange(g_proj_states, "... (h d) -> ... h d", d=self.head_dim)
362
252
- core_attn_out = torch.zeros(
363
+ core_attn_out = torch.empty(
364
(1, num_tokens, self.local_num_heads, self.head_dim),
365
dtype=hidden_states.dtype,
366
device=hidden_states.device,
367
)
257
- torch.ops.vllm.kda_attention(
258
- q,
259
- k,
260
- v,
261
- g1,
262
- beta,
263
- core_attn_out,
264
- self.prefix,
368
+
369
+ self._forward(
370
+ mixed_qkv=mixed_qkv,
371
+ g1=g1,
372
+ g2=g2,
373
+ beta=beta,
374
+ core_attn_out=core_attn_out,
375
)
266
- core_attn_out = self.o_norm(core_attn_out, g2)
376
core_attn_out = rearrange(core_attn_out, "1 n h d -> n (h d)")
377
output[:] = self.o_proj(core_attn_out)[0]
378
379
+ @eager_break_during_capture
380
def _forward(
381
self,
272
- q_proj_states: torch.Tensor,
273
- k_proj_states: torch.Tensor,
274
- v_proj_states: torch.Tensor,
382
+ mixed_qkv: torch.Tensor,
383
g1: torch.Tensor,
384
+ g2: torch.Tensor,
385
beta: torch.Tensor,
386
core_attn_out: torch.Tensor,
387
) -> None:
389
attn_metadata_raw = forward_context.attn_metadata
390
391
if attn_metadata_raw is None:
283
- # # V1 profile run
392
return
393
394
+ # Vendor-specific KDA kernels: AMD/ROCm and NVIDIA keep their own copies
395
+ # under kimi_k3/{amd,nvidia}/ops so each can diverge independently.
396
+ if current_platform.is_rocm():
397
+ from vllm.models.kimi_k3.amd.ops.third_party.kda import (
398
+ chunk_kda_with_fused_gate,
399
+ fused_recurrent_kda,
400
+ fused_recurrent_kda_packed_decode,
401
+ )
402
+ else:
403
+ from vllm.models.kimi_k3.nvidia.ops.third_party.kda import (
404
+ chunk_kda_with_fused_gate,
405
+ fused_recurrent_kda,
406
+ fused_recurrent_kda_packed_decode,
407
+ )
408
+
409
assert isinstance(attn_metadata_raw, dict)
410
attn_metadata_narrowed = attn_metadata_raw[self.prefix]
411
assert isinstance(attn_metadata_narrowed, GDNAttentionMetadata)
289
- has_initial_state = attn_metadata_narrowed.has_initial_state
290
- non_spec_query_start_loc = attn_metadata_narrowed.non_spec_query_start_loc
291
- non_spec_state_indices_tensor = (
292
- attn_metadata_narrowed.non_spec_state_indices_tensor
293
- ) # noqa: E501
294
- num_actual_tokens = attn_metadata_narrowed.num_actual_tokens
295
- constant_caches = self.kv_cache
296
-
297
- q_proj_states = q_proj_states[:num_actual_tokens]
298
- k_proj_states = k_proj_states[:num_actual_tokens]
299
- v_proj_states = v_proj_states[:num_actual_tokens]
412
+ m = attn_metadata_narrowed
413
+ has_initial_state = m.has_initial_state
414
+ non_spec_query_start_loc = m.non_spec_query_start_loc
415
+ non_spec_state_indices_tensor = m.non_spec_state_indices_tensor
416
+ spec_sequence_masks = m.spec_sequence_masks
417
+ spec_token_indx = m.spec_token_indx
418
+ non_spec_token_indx = m.non_spec_token_indx
419
+ spec_state_indices_tensor = m.spec_state_indices_tensor
420
+ spec_query_start_loc = m.spec_query_start_loc
421
+ num_accepted_tokens = m.num_accepted_tokens
422
+ num_actual_tokens = m.num_actual_tokens
423
+ mixed_qkv = mixed_qkv[:num_actual_tokens]
424
g1 = g1[:, :num_actual_tokens]
425
beta = beta[:, :num_actual_tokens]
426
303
- (conv_state, recurrent_state) = constant_caches
427
+ constant_caches = self.kv_cache
428
+
429
+ conv_state, recurrent_state = constant_caches
430
# conv_state must be (..., dim, width-1) for the conv kernels.
431
# DS layout stores it that way directly; SD layout needs a transpose.
432
if not is_conv_state_dim_first():
433
conv_state = conv_state.transpose(-1, -2)
434
309
- conv_state_q, conv_state_k, conv_state_v = conv_state.chunk(3, dim=-2)
310
-
311
- q_conv_weights = self.q_conv1d.weight.view(
312
- self.q_conv1d.weight.size(0), self.q_conv1d.weight.size(2)
435
+ conv_weights = self.conv1d.weight.view(
436
+ self.conv1d.weight.size(0), self.conv1d.weight.size(2)
437
)
314
- k_conv_weights = self.k_conv1d.weight.view(
315
- self.k_conv1d.weight.size(0), self.k_conv1d.weight.size(2)
438
+ q_conv_weight, k_conv_weight, v_conv_weight = conv_weights.split(
439
+ self.local_projection_size, dim=0
440
)
317
- v_conv_weights = self.v_conv1d.weight.view(
318
- self.v_conv1d.weight.size(0), self.v_conv1d.weight.size(2)
441
+ q_conv_state, k_conv_state, v_conv_state = conv_state.split(
442
+ self.local_projection_size, dim=-2
443
)
320
- if attn_metadata_narrowed.num_prefills > 0:
321
- q_proj_states = q_proj_states.transpose(0, 1)
322
- k_proj_states = k_proj_states.transpose(0, 1)
323
- v_proj_states = v_proj_states.transpose(0, 1)
324
- q = causal_conv1d_fn(
325
- q_proj_states,
326
- q_conv_weights,
327
- self.q_conv1d.bias,
328
- activation="silu",
329
- conv_states=conv_state_q,
330
- has_initial_state=has_initial_state,
331
- cache_indices=non_spec_state_indices_tensor,
332
- query_start_loc=non_spec_query_start_loc,
333
- metadata=attn_metadata_narrowed,
334
- ).transpose(0, 1)
335
- k = causal_conv1d_fn(
336
- k_proj_states,
337
- k_conv_weights,
338
- self.k_conv1d.bias,
339
- activation="silu",
340
- conv_states=conv_state_k,
341
- has_initial_state=has_initial_state,
342
- cache_indices=non_spec_state_indices_tensor,
343
- query_start_loc=non_spec_query_start_loc,
344
- metadata=attn_metadata_narrowed,
345
- ).transpose(0, 1)
346
- v = causal_conv1d_fn(
347
- v_proj_states,
348
- v_conv_weights,
349
- self.v_conv1d.bias,
350
- activation="silu",
351
- conv_states=conv_state_v,
352
- has_initial_state=has_initial_state,
353
- cache_indices=non_spec_state_indices_tensor,
354
- query_start_loc=non_spec_query_start_loc,
355
- metadata=attn_metadata_narrowed,
356
- ).transpose(0, 1)
444
+
445
+ # Split tokens into the multi-query spec-decode part and the remaining
446
+ # (prefill / plain decode) part.
447
+ if spec_sequence_masks is not None:
448
+ if m.num_prefills == 0 and m.num_decodes == 0:
449
+ mixed_qkv_spec = mixed_qkv
450
+ g1_spec, beta_spec = g1, beta
451
+ mixed_qkv_ns = g1_ns = beta_ns = None
452
+ else:
453
+ mixed_qkv_spec = mixed_qkv.index_select(0, spec_token_indx)
454
+ g1_spec = g1.index_select(1, spec_token_indx)
455
+ beta_spec = beta.index_select(1, spec_token_indx)
456
+ mixed_qkv_ns = mixed_qkv.index_select(0, non_spec_token_indx)
457
+ g1_ns = g1.index_select(1, non_spec_token_indx)
458
+ beta_ns = beta.index_select(1, non_spec_token_indx)
459
else:
358
- assert non_spec_state_indices_tensor is not None
359
- decode_conv_indices = non_spec_state_indices_tensor[
360
- : attn_metadata_narrowed.num_actual_tokens
361
- ]
362
- q = causal_conv1d_update(
363
- q_proj_states,
364
- conv_state_q,
365
- q_conv_weights,
366
- self.q_conv1d.bias,
367
- activation="silu",
368
- conv_state_indices=decode_conv_indices,
369
- validate_data=True,
460
+ mixed_qkv_spec = g1_spec = beta_spec = None
461
+ mixed_qkv_ns, g1_ns, beta_ns = mixed_qkv, g1, beta
462
+
463
+ # ---------- spec-decode multi-query path ----------
464
+ core_attn_out_spec = None
465
+ if spec_sequence_masks is not None:
466
+ assert spec_state_indices_tensor is not None
467
+ assert spec_query_start_loc is not None
468
+ spec_conv_indices = spec_state_indices_tensor[:, 0][: m.num_spec_decodes]
469
+ spec_max_query_len = spec_state_indices_tensor.size(-1)
470
+
471
+ # Sibling beta and, for full-rank gates, output-gate views remain
472
+ # live, so write the convolution output separately.
473
+ spec_conv_out = torch.empty(
474
+ mixed_qkv_spec.shape,
475
+ dtype=mixed_qkv_spec.dtype,
476
+ device=mixed_qkv_spec.device,
477
)
371
- k = causal_conv1d_update(
372
- k_proj_states,
373
- conv_state_k,
374
- k_conv_weights,
375
- self.k_conv1d.bias,
478
+ mixed_qkv_spec = causal_conv1d_update(
479
+ mixed_qkv_spec,
480
+ conv_state,
481
+ conv_weights,
482
+ self.conv1d.bias,
483
activation="silu",
377
- conv_state_indices=decode_conv_indices,
378
- validate_data=True,
484
+ conv_state_indices=spec_conv_indices,
485
+ num_accepted_tokens=num_accepted_tokens,
486
+ query_start_loc=spec_query_start_loc,
487
+ max_query_len=spec_max_query_len,
488
+ validate_data=False,
489
+ out=spec_conv_out,
490
)
380
- v = causal_conv1d_update(
381
- v_proj_states,
382
- conv_state_v,
383
- v_conv_weights,
384
- self.v_conv1d.bias,
385
- activation="silu",
386
- conv_state_indices=decode_conv_indices,
387
- validate_data=True,
491
+ q_spec, k_spec, v_spec = (
492
+ rearrange(x, "n (h d) -> 1 n h d", d=self.head_dim)
493
+ for x in mixed_qkv_spec.split(self.local_projection_size, dim=-1)
494
)
389
-
390
- q, k, v = map(
391
- lambda x: rearrange(x, "n (h d) -> 1 n h d", d=self.head_dim), (q, k, v)
392
- )
393
-
394
- if attn_metadata_narrowed.num_prefills > 0:
395
- assert non_spec_state_indices_tensor is not None
396
- assert has_initial_state is not None
397
- zero_idx = non_spec_state_indices_tensor[~has_initial_state]
398
- recurrent_state[zero_idx] = 0
399
- initial_state = recurrent_state[non_spec_state_indices_tensor].contiguous()
400
- (
401
- core_attn_out_non_spec,
402
- last_recurrent_state,
403
- ) = chunk_kda_with_fused_gate(
404
- q=q,
405
- k=k,
406
- v=v,
407
- raw_g=g1,
408
- beta=beta,
409
- A_log=self.A_log,
410
- g_bias=self.dt_bias,
411
- initial_state=initial_state,
412
- output_final_state=True,
413
- use_qk_l2norm_in_kernel=True,
414
- cu_seqlens=non_spec_query_start_loc,
495
+ spec_cu_seqlens = spec_query_start_loc[: m.num_spec_decodes + 1]
496
+ # Spec-only batches write directly into core_attn_out.
497
+ spec_out = (
498
+ core_attn_out[:, : q_spec.shape[1]]
499
+ if m.num_prefills == 0 and m.num_decodes == 0
500
+ else None
501
)
416
- # Init cache
417
- recurrent_state[non_spec_state_indices_tensor] = last_recurrent_state
418
- else:
419
- assert non_spec_query_start_loc is not None
420
- g1 = fused_kda_gate(
421
- rearrange(g1, "1 n h d -> n (h d)"),
422
- self.A_log,
423
- self.head_dim,
424
- g_bias=self.dt_bias,
425
- ).unsqueeze(0)
426
- (
427
- core_attn_out_non_spec,
428
- last_recurrent_state,
429
- ) = fused_recurrent_kda(
430
- q=q,
431
- k=k,
432
- v=v,
433
- g=g1,
434
- beta=beta,
502
+ core_attn_out_spec, _ = fused_recurrent_kda(
503
+ q=q_spec,
504
+ k=k_spec,
505
+ v=v_spec,
506
+ raw_g=g1_spec,
507
+ raw_beta=beta_spec,
508
+ A_log=self.A_log,
509
+ dt_bias=self.dt_bias,
510
+ lower_bound=self.gate_lower_bound,
511
initial_state=recurrent_state,
436
- use_qk_l2norm_in_kernel=True,
437
- cu_seqlens=non_spec_query_start_loc[
438
- : attn_metadata_narrowed.num_decodes + 1
439
- ],
440
- ssm_state_indices=non_spec_state_indices_tensor,
512
+ cu_seqlens=spec_cu_seqlens,
513
+ ssm_state_indices=spec_state_indices_tensor,
514
+ num_accepted_tokens=num_accepted_tokens,
515
+ out=spec_out,
516
)
442
- core_attn_out[0, :num_actual_tokens] = core_attn_out_non_spec[
443
- 0, :num_actual_tokens
444
- ]
517
+
518
+ # ---------- non-spec path (prefill or plain decode) ----------
519
+ core_attn_out_non_spec = None
520
+ if mixed_qkv_ns is not None:
521
+ assert g1_ns is not None and beta_ns is not None
522
+ if m.num_prefills > 0:
523
+ q_ns, k_ns, v_ns = mixed_qkv_ns.split(
524
+ self.local_projection_size, dim=-1
525
+ )
526
+
527
+ # Packed prefill conv would require copying V solely to make
528
+ # it dense for KDA. Separate calls accept the strided inputs
529
+ # and produce dense Q/K/V without that extra traffic.
530
+ # TODO: Use packed conv once every KDA prefill backend accepts
531
+ # row-strided Q/K/V directly.
532
+ def _prefill_conv(
533
+ x: torch.Tensor,
534
+ state: torch.Tensor,
535
+ weight: torch.Tensor,
536
+ ) -> torch.Tensor:
537
+ return causal_conv1d_fn(
538
+ x.transpose(0, 1),
539
+ weight,
540
+ None,
541
+ activation="silu",
542
+ conv_states=state,
543
+ has_initial_state=has_initial_state,
544
+ cache_indices=non_spec_state_indices_tensor,
545
+ query_start_loc=non_spec_query_start_loc,
546
+ metadata=m,
547
+ ).transpose(0, 1)
548
+
549
+ q_ns = _prefill_conv(q_ns, q_conv_state, q_conv_weight)
550
+ k_ns = _prefill_conv(k_ns, k_conv_state, k_conv_weight)
551
+ v_ns = _prefill_conv(v_ns, v_conv_state, v_conv_weight)
552
+ q_ns, k_ns, v_ns = (
553
+ rearrange(x, "n (h d) -> 1 n h d", d=self.head_dim)
554
+ for x in (q_ns, k_ns, v_ns)
555
+ )
556
+
557
+ assert non_spec_state_indices_tensor is not None
558
+ assert has_initial_state is not None
559
+ initial_state = gather_initial_states(
560
+ recurrent_state,
561
+ non_spec_state_indices_tensor,
562
+ has_initial_state,
563
+ )
564
+ (
565
+ core_attn_out_non_spec,
566
+ last_recurrent_state,
567
+ ) = chunk_kda_with_fused_gate(
568
+ q=q_ns,
569
+ k=k_ns,
570
+ v=v_ns,
571
+ raw_g=g1_ns,
572
+ raw_beta=beta_ns,
573
+ A_log=self.A_log,
574
+ g_bias=self.dt_bias,
575
+ lower_bound=self.gate_lower_bound,
576
+ initial_state=initial_state,
577
+ output_final_state=True,
578
+ use_qk_l2norm_in_kernel=True,
579
+ cu_seqlens=non_spec_query_start_loc,
580
+ )
581
+ # Init cache
582
+ recurrent_state[non_spec_state_indices_tensor] = last_recurrent_state
583
+
584
+ else:
585
+ # pure-decode non-spec batch
586
+ assert non_spec_state_indices_tensor is not None
587
+ decode_conv_indices = non_spec_state_indices_tensor[
588
+ : mixed_qkv_ns.size(0)
589
+ ]
590
+ # Sibling beta and, for full-rank gates, output-gate views
591
+ # remain live, so write the conv output separately.
592
+ packed_conv_out = torch.empty(
593
+ mixed_qkv_ns.shape,
594
+ dtype=mixed_qkv_ns.dtype,
595
+ device=mixed_qkv_ns.device,
596
+ )
597
+ mixed_qkv_ns = causal_conv1d_update(
598
+ mixed_qkv_ns,
599
+ conv_state,
600
+ conv_weights,
601
+ self.conv1d.bias,
602
+ activation="silu",
603
+ conv_state_indices=decode_conv_indices,
604
+ validate_data=True,
605
+ out=packed_conv_out,
606
+ )
607
+ core_attn_out_non_spec, _ = fused_recurrent_kda_packed_decode(
608
+ mixed_qkv=mixed_qkv_ns,
609
+ raw_g=g1_ns,
610
+ raw_beta=beta_ns,
611
+ A_log=self.A_log,
612
+ dt_bias=self.dt_bias,
613
+ lower_bound=self.gate_lower_bound,
614
+ initial_state=recurrent_state,
615
+ state_indices=decode_conv_indices,
616
+ )
617
+
618
+ # ---------- merge spec and non-spec outputs ----------
619
+ if core_attn_out_spec is not None and core_attn_out_non_spec is not None:
620
+ # Mixed batches require indexed placement in the original order.
621
+ merged = torch.empty(
622
+ (1, num_actual_tokens, *core_attn_out_spec.shape[2:]),
623
+ dtype=core_attn_out_spec.dtype,
624
+ device=core_attn_out_spec.device,
625
+ )
626
+ merged.index_copy_(1, spec_token_indx, core_attn_out_spec)
627
+ merged.index_copy_(1, non_spec_token_indx, core_attn_out_non_spec)
628
+ core_attn_out[0, :num_actual_tokens] = merged[0, :num_actual_tokens]
629
+ elif core_attn_out_non_spec is not None:
630
+ core_attn_out[0, :num_actual_tokens] = core_attn_out_non_spec[
631
+ 0, :num_actual_tokens
632
+ ]
633
+ else:
634
+ assert core_attn_out_spec is not None
635
+ core_attn_out.copy_(self.o_norm(core_attn_out, g2))