main
py 1,093 lines 45 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Clean Multi-head Latent Attention for Kimi-K3 (NVIDIA).
4
5 This is a self-contained MLA layer that owns the full attention path:
6
7 hidden_states
8 -> fused pre-attention ops (fused_qkv_a_proj / norms / q_b_proj;
9 with an output gate the rows use fused_qkv_a_g_proj instead)
10 -> explicit prefill / decode split
11 prefill: fused key-concat + cache-insert kernel -> run_prefill_new_tokens
12 (+ chunked-context merge, whose per-chunk gather -> kv_b_proj
13 -> fused K/V pack loop this layer owns); dispatched by cache
14 dtype (bf16 / plain fp8 / fp8_ds_mla)
15 decode : W_UK absorb (BMM1) -> fused q-concat + cache-insert kernel
16 -> impl.forward_mqa -> W_UV up-proj (MQA)
17 -> optional output gate
18 -> o_proj
19
20 Unlike ``MultiHeadLatentAttentionWrapper`` (which delegates orchestration to
21 ``MLAAttention.forward``), this class *is* the ``AttentionLayerBase``: it selects
22 the backend, builds the impl, registers itself in the forward context, owns the
23 KV cache, and absorbs ``kv_b_proj`` into ``W_UK_T`` / ``W_UV`` -- mirroring the
24 ``DeepseekV4Attention`` structure.
25
26 K3 specifics: optional rotary embedding (disabled for the target model's NoPE
27 layers, enabled for DSpark) and an optional sigmoid output gate (``g_proj``,
28 merged into ``fused_qkv_a_g_proj`` when the q-LoRA front-end is present).
29
30 Out of scope (extension points, not wired here): prefill context parallelism
31 (PCP), sparse/indexer MLA, and the ROCm/aiter fp8/fp4 BMM fast paths.
32 """
33
34 import math
35 from typing import TYPE_CHECKING, cast
36
37 import torch
38 import torch.nn.functional as F
39 from torch import nn
40
41 from vllm import _custom_ops as ops
42 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
43 from vllm.config import (
44 CacheConfig,
45 VllmConfig,
46 get_current_vllm_config,
47 )
48 from vllm.distributed import get_tensor_model_parallel_world_size
49 from vllm.forward_context import get_forward_context
50 from vllm.logger import init_logger
51 from vllm.model_executor.layers.attention.attention import (
52 _init_kv_cache_quant,
53 set_default_quant_scales,
54 should_load_quant_weights,
55 )
56 from vllm.model_executor.layers.attention.mla_attention import (
57 _get_kv_b_proj_input_dtype,
58 accumulate_mla_context_chunk,
59 init_mla_context_partial,
60 neutralize_empty_context_partials,
61 split_kv_b_proj,
62 )
63 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
64 from vllm.model_executor.layers.layernorm import RMSNorm
65 from vllm.model_executor.layers.linear import (
66 ColumnParallelLinear,
67 KimiK3MergedQKVGateLinear,
68 MergedColumnParallelLinear,
69 ReplicatedLinear,
70 RowParallelLinear,
71 UnquantizedLinearMethod,
72 )
73 from vllm.model_executor.layers.quantization import (
74 QuantizationConfig,
75 resolve_quant_method,
76 )
77 from vllm.model_executor.layers.rotary_embedding import RotaryEmbedding, get_rope
78 from vllm.model_executor.utils import replace_parameter
79 from vllm.models.common.ops import fused_q_kv_rmsnorm
80 from vllm.models.kimi_k3.nvidia.low_latency_gemm import try_low_latency_gemm
81 from vllm.models.kimi_k3.nvidia.ops.fused_mla_key_concat_kv_cache import (
82 fused_mla_decode_q_concat_kv_cache_insert,
83 fused_mla_key_concat_ds_mla_insert,
84 fused_mla_key_concat_kv_cache_insert,
85 fused_mla_kv_concat,
86 fused_mla_kv_concat_quant_fp8,
87 fused_mla_qkv_quant_kv_cache_fp8_insert,
88 )
89 from vllm.platforms import current_platform
90 from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
91 from vllm.utils.multi_stream_utils import maybe_execute_in_parallel
92 from vllm.utils.torch_utils import (
93 is_quantized_kv_cache,
94 kv_cache_dtype_str_to_dtype,
95 )
96 from vllm.v1.attention.backend import (
97 AttentionBackend,
98 AttentionType,
99 MLAAttentionImpl,
100 )
101 from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
102 from vllm.v1.attention.ops.dcp import MLADCPManager
103 from vllm.v1.attention.ops.merge_attn_states import merge_attn_states
104 from vllm.v1.attention.selector import get_attn_backend
105 from vllm.v1.kv_cache_interface import KVCacheSpec, MLAAttentionSpec, get_kv_quant_mode
106
107 if TYPE_CHECKING:
108 from vllm.model_executor.layers.attention.mla_attention import MLACommonMetadata
109
110 logger = init_logger(__name__)
111
112 # Below this conservative threshold, overlap the gate projection with attention
113 # on the auxiliary stream.
114 _GATE_MULTI_STREAM_TOKEN_THRESHOLD = 512
115
116
117 @torch.compile(backend=current_platform.simple_compile_backend)
118 def _gate_sigmoid_mul(attn_out: torch.Tensor, gate: torch.Tensor) -> torch.Tensor:
119 """Apply the sigmoid output gate to a precomputed gate projection."""
120 return attn_out * gate.sigmoid()
121
122
123 class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
124 """Kimi-K3 Multi-head Latent Attention with optional RoPE and output gate."""
125
126 def __init__(
127 self,
128 config: KimiLinearConfig,
129 hidden_size: int,
130 num_heads: int,
131 qk_nope_head_dim: int,
132 qk_rope_head_dim: int,
133 v_head_dim: int,
134 q_lora_rank: int | None,
135 kv_lora_rank: int,
136 use_output_gate: bool = False,
137 cache_config: CacheConfig | None = None,
138 quant_config: QuantizationConfig | None = None,
139 prefix: str = "",
140 aux_stream: torch.cuda.Stream | None = None,
141 use_rope: bool = False,
142 non_causal_multi_token_decode: bool = False,
143 run_gemm_rs_ar: bool = False,
144 ) -> None:
145 super().__init__()
146 self.hidden_size = hidden_size
147 self.qk_nope_head_dim = qk_nope_head_dim
148 self.qk_rope_head_dim = qk_rope_head_dim
149 self.qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
150 self.v_head_dim = v_head_dim
151 self.q_lora_rank = q_lora_rank
152 self.kv_lora_rank = kv_lora_rank
153 self.use_output_gate = use_output_gate
154 self.non_causal_multi_token_decode = non_causal_multi_token_decode
155 # Latent "head" seen by the attention kernel / KV cache.
156 self.head_size = kv_lora_rank + qk_rope_head_dim
157 self.scale = self.qk_head_dim**-0.5
158 self.rms_norm_eps = config.rms_norm_eps
159 self.layer_name = prefix
160
161 self.rotary_emb: RotaryEmbedding | None = None
162 if use_rope:
163 rope_parameters = dict(config.rope_parameters)
164 if rope_parameters["rope_type"] != "default":
165 rope_parameters["rope_type"] = (
166 "deepseek_llama_scaling"
167 if rope_parameters.get("attention_factor") == 1.0
168 else "deepseek_yarn"
169 )
170 self.rotary_emb = get_rope(
171 qk_rope_head_dim,
172 max_position=config.max_position_embeddings,
173 rope_parameters=rope_parameters,
174 is_neox_style=False,
175 dtype=torch.float32,
176 )
177 if rope_parameters["rope_type"] == "deepseek_yarn":
178 mscale_all_dim = rope_parameters.get("mscale_all_dim", False)
179 scaling_factor = rope_parameters["factor"]
180 mscale = (
181 1.0
182 if scaling_factor <= 1
183 else 0.1 * float(mscale_all_dim) * math.log(scaling_factor) + 1.0
184 )
185 self.scale *= mscale * mscale
186 # The fused epilogues read the cos/sin table directly in fp32 and run
187 # the RoPE math in fp32, so there is no per-forward dtype cast (and no
188 # precision loss). deepseek_yarn builds cos_sin_cache in fp32 already;
189 # dtype=torch.float32 above forces it for the default rope too (the
190 # DSpark draft, which has no yarn scaling).
191 assert self.rotary_emb.cos_sin_cache.dtype == torch.float32, (
192 "K3 fused MLA RoPE requires an fp32 cos/sin cache; got "
193 f"{self.rotary_emb.cos_sin_cache.dtype}."
194 )
195
196 tp_size = get_tensor_model_parallel_world_size()
197 assert num_heads % tp_size == 0
198 self.num_heads = num_heads
199 self.num_local_heads = num_heads // tp_size
200
201 # ---- Pre-attention projections (fusable front-end) ----
202 # Exactly one query layout is constructed below. Kimi-K3 uses q-LoRA;
203 # Kimi-Linear uses independent full-rank Q and KV projections.
204 self.fused_qkv_a_proj: MergedColumnParallelLinear | None = None
205 self.fused_qkv_a_g_proj: KimiK3MergedQKVGateLinear | None = None
206 self.q_proj: ColumnParallelLinear | None = None
207 self.kv_a_proj_with_mqa: ReplicatedLinear | None = None
208 self.g_proj: ColumnParallelLinear | None = None
209 self.aux_stream: torch.cuda.Stream | None = None
210 self._gate_events: tuple[torch.cuda.Event, torch.cuda.Event] | None = None
211
212 # Kimi-K3: low-rank Q and KV projections share one input projection.
213 if self.q_lora_rank is not None:
214 # The latent Q and KV shards are replicated across TP ranks. Their
215 # TP splitting happens in q_b_proj and kv_b_proj instead.
216 if use_output_gate:
217 # The output gate is shard 2 of the same input projection.
218 self.fused_qkv_a_g_proj = KimiK3MergedQKVGateLinear(
219 hidden_size=self.hidden_size,
220 q_lora_rank=self.q_lora_rank,
221 kv_lora_rank=self.kv_lora_rank,
222 qk_rope_head_dim=self.qk_rope_head_dim,
223 total_num_heads=num_heads,
224 v_head_dim=self.v_head_dim,
225 bias=False,
226 quant_config=quant_config,
227 prefix=f"{prefix}.fused_qkv_a_g_proj",
228 )
229 # Only supports unquantized weights for sliced side-stream path
230 # for now.
231 if aux_stream is not None and isinstance(
232 self.fused_qkv_a_g_proj.quant_method, UnquantizedLinearMethod
233 ):
234 self.aux_stream = aux_stream
235 self._gate_events = (torch.cuda.Event(), torch.cuda.Event())
236 else:
237 self.fused_qkv_a_proj = MergedColumnParallelLinear(
238 self.hidden_size,
239 [
240 self.q_lora_rank,
241 self.kv_lora_rank + self.qk_rope_head_dim,
242 ],
243 bias=False,
244 quant_config=quant_config,
245 prefix=f"{prefix}.fused_qkv_a_proj",
246 disable_tp=True,
247 )
248 self.q_a_layernorm = RMSNorm(self.q_lora_rank, eps=config.rms_norm_eps)
249 self.q_b_proj = ColumnParallelLinear(
250 self.q_lora_rank,
251 self.num_heads * self.qk_head_dim,
252 bias=False,
253 quant_config=quant_config,
254 prefix=f"{prefix}.q_b_proj",
255 )
256
257 # Kimi-Linear: Q and KV use independent full-rank input projections.
258 else:
259 self.q_proj = ColumnParallelLinear(
260 self.hidden_size,
261 self.num_heads * self.qk_head_dim,
262 bias=False,
263 quant_config=quant_config,
264 prefix=f"{prefix}.q_proj",
265 )
266 self.kv_a_proj_with_mqa = ReplicatedLinear(
267 self.hidden_size,
268 self.kv_lora_rank + self.qk_rope_head_dim,
269 bias=False,
270 quant_config=quant_config,
271 prefix=f"{prefix}.kv_a_proj_with_mqa",
272 )
273 if use_output_gate:
274 # Keep the gate standalone for the non-LoRA layout.
275 self.g_proj = ColumnParallelLinear(
276 self.hidden_size,
277 self.num_heads * self.v_head_dim,
278 bias=False,
279 quant_config=quant_config,
280 prefix=f"{prefix}.g_proj",
281 )
282
283 self.kv_a_layernorm = RMSNorm(self.kv_lora_rank, eps=config.rms_norm_eps)
284 self.kv_b_proj = ColumnParallelLinear(
285 self.kv_lora_rank,
286 self.num_heads * (self.qk_nope_head_dim + self.v_head_dim),
287 bias=False,
288 quant_config=quant_config,
289 prefix=f"{prefix}.kv_b_proj",
290 )
291
292 # ---- Post-attention projection ----
293 self.o_proj = RowParallelLinear(
294 self.num_heads * self.v_head_dim,
295 self.hidden_size,
296 bias=False,
297 quant_config=quant_config,
298 prefix=f"{prefix}.o_proj",
299 )
300 self.gemm_rs_ar = None
301 if run_gemm_rs_ar:
302 from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
303 get_gemm_rs_ar,
304 )
305
306 gemm_rs_ar = get_gemm_rs_ar()
307 if gemm_rs_ar.can_run(self.o_proj):
308 self.gemm_rs_ar = gemm_rs_ar
309 else:
310 gemm_rs_ar.warn_incompatible_projection()
311
312 # ---- Attention backend / impl / KV cache ----
313 self.quant_config = quant_config
314 if cache_config is not None:
315 self.kv_cache_dtype = cache_config.cache_dtype
316 else:
317 self.kv_cache_dtype = "auto"
318
319 dtype = torch.get_default_dtype()
320 self.attn_backend = get_attn_backend(
321 self.head_size,
322 dtype,
323 self.kv_cache_dtype,
324 use_mla=True,
325 use_sparse=False,
326 num_heads=self.num_local_heads,
327 )
328 _init_kv_cache_quant(self, quant_config, prefix)
329 # Unit (1.0) scale for the fused fp8 prefill path: q/k/v are cast
330 # unscaled to match forward_mha (the prefill flash path does not
331 # dequantize); only the cache uses _k_scale.
332 self.register_buffer(
333 "_one_scale", torch.ones(1, dtype=torch.float32), persistent=False
334 )
335
336 impl_cls = cast(type[MLAAttentionImpl], self.attn_backend.get_impl_cls())
337 self.impl = impl_cls( # type: ignore[assignment]
338 num_heads=self.num_local_heads,
339 head_size=self.head_size,
340 scale=self.scale,
341 num_kv_heads=1,
342 alibi_slopes=None,
343 sliding_window=None,
344 kv_cache_dtype=self.kv_cache_dtype,
345 logits_soft_cap=None,
346 attn_type=AttentionType.DECODER,
347 kv_sharing_target_layer_name=None,
348 q_lora_rank=self.q_lora_rank,
349 kv_lora_rank=self.kv_lora_rank,
350 qk_nope_head_dim=self.qk_nope_head_dim,
351 qk_rope_head_dim=self.qk_rope_head_dim,
352 qk_head_dim=self.qk_head_dim,
353 v_head_dim=self.v_head_dim,
354 kv_b_proj=self.kv_b_proj,
355 indexer=None,
356 )
357 if getattr(self.impl, "dcp_world_size", -1) < 1:
358 # FlashAttention requires the cp_world_size is positive and the cp_rank
359 # is non negative; manually set here if not set by caller (-1 is unset)
360 self.impl.dcp_world_size = 1
361 self.impl.dcp_rank = 0
362 self.q_pad_num_heads = getattr(self.impl, "q_pad_num_heads", None)
363
364 vllm_config = get_current_vllm_config()
365 parallel_config = vllm_config.parallel_config
366 assert parallel_config.prefill_context_parallel_size == 1, (
367 "Kimi-K3 MultiHeadLatentAttention does not support prefill context "
368 "parallelism."
369 )
370 self.dcp_world_size = parallel_config.decode_context_parallel_size
371 self.dcp_manager: MLADCPManager | None = None
372 if self.dcp_world_size > 1:
373 query_dtype = (
374 torch.float8_e4m3fn
375 if is_quantized_kv_cache(self.kv_cache_dtype)
376 and self.kv_cache_dtype != "fp8_ds_mla"
377 else dtype
378 )
379 self.dcp_manager = MLADCPManager(
380 vllm_config=vllm_config,
381 device=next(self.kv_b_proj.parameters()).device,
382 num_heads=self.num_local_heads,
383 query_head_dim=self.head_size,
384 output_head_dim=self.kv_lora_rank,
385 query_dtype=query_dtype,
386 output_dtype=dtype,
387 padded_num_heads=self.q_pad_num_heads,
388 is_lse_base_on_e=self.impl.lse_base_on_e,
389 use_pcp=False,
390 )
391 self.prefill_backend = get_mla_prefill_backend(vllm_config)(
392 num_heads=self.num_local_heads,
393 scale=self.scale,
394 kv_lora_rank=self.kv_lora_rank,
395 qk_nope_head_dim=self.qk_nope_head_dim,
396 qk_rope_head_dim=self.qk_rope_head_dim,
397 v_head_dim=self.v_head_dim,
398 vllm_config=vllm_config,
399 )
400
401 compilation_config = vllm_config.compilation_config
402 if prefix in compilation_config.static_forward_context:
403 raise ValueError(f"Duplicate layer name: {prefix}")
404 compilation_config.static_forward_context[prefix] = self
405 self.kv_cache = torch.tensor([])
406
407 # ------------------------------------------------------------------
408 # AttentionLayerBase interface
409 # ------------------------------------------------------------------
410 def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
411 # [B, H=1, N, C] -> [B, N, C]
412 self.kv_cache = kv_cache.squeeze(1)
413
414 def get_attn_backend(self) -> type[AttentionBackend]:
415 return self.attn_backend
416
417 def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
418 kv_cache_dtype = kv_cache_dtype_str_to_dtype(
419 self.kv_cache_dtype, vllm_config.model_config
420 )
421 # TODO: Remove this mypy workaround once the K3 PR is fully merged.
422 return MLAAttentionSpec( # type: ignore[call-arg]
423 block_size=vllm_config.cache_config.block_size,
424 num_kv_heads=1,
425 head_size=self.head_size,
426 dtype=kv_cache_dtype,
427 cache_dtype_str=self.kv_cache_dtype,
428 kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
429 # fp8_ds_mla: 656-byte custom layout; see flashmla_sparse.py.
430 state_content_bytes=656 if self.kv_cache_dtype == "fp8_ds_mla" else None,
431 non_causal_multi_token_decode=self.non_causal_multi_token_decode,
432 )
433
434 def process_weights_after_loading(self, act_dtype: torch.dtype) -> None:
435 """Absorb ``kv_b_proj`` into decode-time ``W_UK_T`` / ``W_UV`` bmm weights.
436
437 ``kv_b_proj`` produces ``[k_nope; v]`` per head from the ``kv_lora_rank``
438 latent. For the MQA decode path we pre-split it so that queries are
439 projected into latent space by ``W_UK_T`` and the attention output is
440 projected back to ``v`` by ``W_UV`` -- avoiding materializing full K/V.
441 """
442 W_UK, W_UV = split_kv_b_proj(
443 self.kv_b_proj,
444 act_dtype,
445 self.kv_lora_rank,
446 self.num_local_heads,
447 self.qk_nope_head_dim,
448 self.v_head_dim,
449 )
450 # (L, N, V) -> (N, L, V)
451 replace_parameter(self, "W_UV", W_UV.transpose(0, 1), prefer_copy=True)
452 # (L, N, P) -> (N, P, L)
453 replace_parameter(self, "W_UK_T", W_UK.permute(1, 2, 0), prefer_copy=True)
454
455 quant_method = (
456 resolve_quant_method(self.quant_config, self, prefix=self.layer_name)
457 if self.quant_config
458 else None
459 )
460 if not should_load_quant_weights(quant_method):
461 set_default_quant_scales(self, register_buffer=False)
462
463 # Precompute reciprocal scales once here (scales are final after load;
464 # K3 has no runtime calculate_kv_scales path) so the fp8 fused kernels
465 # in the decode/prefill hot path take a ready inverse instead of
466 # launching a per-step reciprocal kernel.
467 self.register_buffer(
468 "_q_scale_inv", self._q_scale.reciprocal().reshape(1), persistent=False
469 )
470 self.register_buffer(
471 "_k_scale_inv", self._k_scale.reciprocal().reshape(1), persistent=False
472 )
473
474 def _v_up_proj(self, x: torch.Tensor, out: torch.Tensor) -> None:
475 """Project latent attention output back to ``v`` via ``W_UV`` (bmm)."""
476 # (B, N, L) -> (N, B, L)
477 x = x.view(-1, self.num_local_heads, self.kv_lora_rank).transpose(0, 1)
478 out = out.view(-1, self.num_local_heads, self.v_head_dim)
479 # (N, B, L) x (N, L, V) -> (N, B, V) written transposed into (B, N, V)
480 torch.bmm(x, self.W_UV, out=out.transpose(0, 1))
481
482 def _attn_read_kv_cache(self) -> torch.Tensor:
483 """Latent cache as seen by the attention read kernels (decode / context).
484
485 A plain per-tensor fp8 cache is stored as ``uint8``; view it as fp8 so
486 the backend reads it as E4M3 rather than fp4/E2M1 -- the latter doubles
487 the perceived head dim (``head_size * 2``) and fails the kernel's
488 ``head_dim_k == head_dim_q`` check. Mirrors ``MLAAttention.forward``;
489 the fp8_ds_mla layout keeps its native uint8 view.
490 """
491 cache = self.kv_cache
492 if (
493 is_quantized_kv_cache(self.kv_cache_dtype)
494 and self.kv_cache_dtype != "fp8_ds_mla"
495 ):
496 return cache.view(current_platform.fp8_dtype())
497 return cache
498
499 # ------------------------------------------------------------------
500 # Forward
501 # ------------------------------------------------------------------
502 def _unquantized_gemm(
503 self, hidden_states: torch.Tensor, weight: torch.Tensor
504 ) -> torch.Tensor:
505 out = try_low_latency_gemm(hidden_states, weight)
506 return out if out is not None else F.linear(hidden_states, weight)
507
508 def _apply_q_lora_attention(
509 self,
510 positions: torch.Tensor,
511 hidden_states: torch.Tensor,
512 qkv_lora: torch.Tensor,
513 ) -> torch.Tensor:
514 """Expand q-LoRA output and run attention."""
515 q_lora_rank = self.q_lora_rank
516 assert q_lora_rank is not None
517 q_c, kv_c, k_pe = qkv_lora.split(
518 [q_lora_rank, self.kv_lora_rank, self.qk_rope_head_dim], dim=-1
519 )
520 q_c, kv_c_normed = fused_q_kv_rmsnorm(
521 q_c,
522 kv_c,
523 self.q_a_layernorm.weight.data,
524 self.kv_a_layernorm.weight.data,
525 self.rms_norm_eps,
526 )
527 q = self.q_b_proj(q_c)[0].view(-1, self.num_local_heads, self.qk_head_dim)
528
529 attn_out = torch.empty(
530 (hidden_states.shape[0], self.num_local_heads * self.v_head_dim),
531 dtype=hidden_states.dtype,
532 device=hidden_states.device,
533 )
534 self._attention(positions, q, kv_c_normed, k_pe.unsqueeze(1), attn_out)
535 return attn_out
536
537 def _forward_q_lora(
538 self,
539 positions: torch.Tensor,
540 hidden_states: torch.Tensor,
541 ) -> tuple[torch.Tensor, torch.Tensor | None]:
542 """Run the q-LoRA path, including gate overlap when eligible."""
543 q_lora_rank = self.q_lora_rank
544 assert q_lora_rank is not None
545 qkv_a_rows = q_lora_rank + self.kv_lora_rank + self.qk_rope_head_dim
546
547 # run g_proj in aux_stream
548 if (
549 self._gate_events is not None
550 and hidden_states.shape[0] < _GATE_MULTI_STREAM_TOKEN_THRESHOLD
551 ):
552 fused_qkv_a_g_proj = self.fused_qkv_a_g_proj
553 assert fused_qkv_a_g_proj is not None
554 qkv_a_weight = fused_qkv_a_g_proj.weight[:qkv_a_rows]
555 gate_weight = fused_qkv_a_g_proj.weight[qkv_a_rows:]
556
557 start_event, end_event = self._gate_events
558 return maybe_execute_in_parallel(
559 lambda: self._apply_q_lora_attention(
560 positions,
561 hidden_states,
562 self._unquantized_gemm(hidden_states, qkv_a_weight),
563 ),
564 lambda: self._unquantized_gemm(hidden_states, gate_weight),
565 start_event,
566 end_event,
567 self.aux_stream,
568 )
569
570 # run g_proj together with qkv_a
571 if self.use_output_gate:
572 qkv_a_proj = self.fused_qkv_a_g_proj
573 assert qkv_a_proj is not None
574 qkv_lora, gate = qkv_a_proj(hidden_states)[0].split(
575 [qkv_a_rows, self.num_local_heads * self.v_head_dim], dim=-1
576 )
577 else:
578 qkv_a_proj = self.fused_qkv_a_proj
579 assert qkv_a_proj is not None
580 qkv_lora = qkv_a_proj(hidden_states)[0]
581 gate = None
582
583 attn_out = self._apply_q_lora_attention(positions, hidden_states, qkv_lora)
584 return attn_out, gate
585
586 def _forward_full_rank_q(
587 self,
588 positions: torch.Tensor,
589 hidden_states: torch.Tensor,
590 ) -> tuple[torch.Tensor, torch.Tensor | None]:
591 """Run the full-rank Q path with an optional sequential gate."""
592 q_proj = self.q_proj
593 kv_a_proj_with_mqa = self.kv_a_proj_with_mqa
594 assert q_proj is not None
595 assert kv_a_proj_with_mqa is not None
596
597 q = q_proj(hidden_states)[0].view(-1, self.num_local_heads, self.qk_head_dim)
598 kv_lora = kv_a_proj_with_mqa(hidden_states)[0]
599 kv_c, k_pe = kv_lora.split([self.kv_lora_rank, self.qk_rope_head_dim], dim=-1)
600 kv_c_normed = self.kv_a_layernorm(kv_c)
601 attn_out = torch.empty(
602 (hidden_states.shape[0], self.num_local_heads * self.v_head_dim),
603 dtype=hidden_states.dtype,
604 device=hidden_states.device,
605 )
606 self._attention(positions, q, kv_c_normed, k_pe.unsqueeze(1), attn_out)
607
608 gate = self.g_proj(hidden_states)[0] if self.g_proj is not None else None
609 return attn_out, gate
610
611 def forward(
612 self,
613 positions: torch.Tensor,
614 hidden_states: torch.Tensor,
615 ) -> torch.Tensor:
616 if self.q_lora_rank is None:
617 attn_out, gate = self._forward_full_rank_q(positions, hidden_states)
618 else:
619 attn_out, gate = self._forward_q_lora(positions, hidden_states)
620
621 if gate is not None:
622 attn_out = _gate_sigmoid_mul(attn_out, gate)
623
624 if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(attn_out):
625 return self.gemm_rs_ar.apply(attn_out, self.o_proj)
626
627 return self.o_proj(attn_out)[0]
628
629 @eager_break_during_capture
630 def _attention(
631 self,
632 positions: torch.Tensor,
633 q: torch.Tensor,
634 kv_c_normed: torch.Tensor,
635 k_pe: torch.Tensor,
636 attn_out: torch.Tensor,
637 ) -> None:
638 forward_context = get_forward_context()
639 attn_metadata_by_layer = forward_context.attn_metadata
640 if attn_metadata_by_layer is None:
641 attn_out.zero_()
642 return
643 assert isinstance(attn_metadata_by_layer, dict)
644 attn_metadata = cast(
645 "MLACommonMetadata", attn_metadata_by_layer[self.layer_name]
646 )
647
648 num_actual_toks = attn_metadata.num_actual_tokens
649 slot_mapping_by_layer = forward_context.slot_mapping
650 assert isinstance(slot_mapping_by_layer, dict)
651 slot_mapping = slot_mapping_by_layer[self.layer_name]
652
653 q = q[:num_actual_toks]
654 kv_c_normed = kv_c_normed[:num_actual_toks]
655 k_pe = k_pe[:num_actual_toks]
656 positions = positions[:num_actual_toks]
657 attn_out = attn_out[:num_actual_toks]
658
659 cos_sin_cache = None
660 rope_positions = None
661 if self.rotary_emb is not None:
662 # Pass the fp32 cos/sin table straight to the fused epilogue (it reads
663 # fp32 and does the RoPE math in fp32) -- no per-forward dtype cast.
664 cos_sin_cache = self.rotary_emb.cos_sin_cache
665 rope_positions = positions
666
667 # Decode tokens are laid out first, prefill tokens after. The fused
668 # prefill covers every supported config (bf16 / plain-fp8 /
669 # fp8_ds_mla), so there is no dense-MHA (forward_mha) fallback.
670 num_mqa_tokens = attn_metadata.num_decode_tokens
671 num_mha_tokens = q.size(0) - num_mqa_tokens
672
673 # Both the prefill and decode fused epilogues write their own cache
674 # slice, so there is no separate do_kv_cache_update.
675
676 # ---- Prefill: fused key-concat + cache-insert + attention ----
677 if num_mha_tokens > 0:
678 self._forward_prefill_fused(
679 q[num_mqa_tokens:],
680 kv_c_normed[num_mqa_tokens:],
681 k_pe[num_mqa_tokens:],
682 rope_positions[num_mqa_tokens:] if rope_positions is not None else None,
683 cos_sin_cache,
684 slot_mapping[num_mqa_tokens:num_actual_toks],
685 attn_metadata,
686 attn_out[num_mqa_tokens:],
687 )
688
689 # ---- Decode: latent multi-query attention ----
690 if num_mqa_tokens > 0:
691 mqa_q_nope, mqa_q_pe = q[:num_mqa_tokens].split(
692 [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1
693 )
694 # BMM1: absorb q_nope into latent space. (N,B,P) x (N,P,L) -> (B,N,L)
695 ql_nope = torch.bmm(mqa_q_nope.transpose(0, 1), self.W_UK_T).transpose(0, 1)
696 # Fused: concat mqa_q = [ql_nope | q_pe] and insert the decode-token
697 # latent into the paged cache (one launch, right before forward_mqa).
698 mqa_q = self._decode_concat_cache(
699 ql_nope,
700 mqa_q_pe,
701 kv_c_normed[:num_mqa_tokens],
702 k_pe[:num_mqa_tokens],
703 rope_positions[:num_mqa_tokens] if rope_positions is not None else None,
704 cos_sin_cache,
705 slot_mapping[:num_mqa_tokens],
706 )
707 if self.dcp_world_size > 1:
708 assert self.dcp_manager is not None
709 assert self.dcp_manager.query_gather is not None
710 mqa_q = self.dcp_manager.query_gather(mqa_q)
711 latent_out, lse = self.impl.forward_mqa( # type: ignore[attr-defined]
712 mqa_q, self._attn_read_kv_cache(), attn_metadata, self
713 )
714 if self.dcp_world_size > 1:
715 assert lse is not None
716 assert self.dcp_manager is not None
717 assert attn_metadata.decode is not None
718 latent_out = self.dcp_manager.combine(
719 latent_out,
720 lse,
721 seq_lens=attn_metadata.decode.seq_lens,
722 query_start_loc=attn_metadata.query_start_loc[
723 : attn_metadata.num_decodes + 1
724 ],
725 )
726 self._v_up_proj(latent_out, out=attn_out[:num_mqa_tokens])
727
728 def _decode_concat_cache(
729 self,
730 ql_nope: torch.Tensor,
731 q_pe: torch.Tensor,
732 kv_c_normed: torch.Tensor,
733 k_pe: torch.Tensor,
734 positions: torch.Tensor | None,
735 cos_sin_cache: torch.Tensor | None,
736 slot_mapping: torch.Tensor,
737 ) -> torch.Tensor:
738 """Fused decode query-concat + latent cache insert, dispatched by cache
739 dtype (same policy as prefill: fp8 cache -> fp8 query)."""
740 if self.kv_cache_dtype == "fp8_ds_mla":
741 cache = self.kv_cache
742 if cache.dtype != torch.uint8:
743 cache = cache.view(torch.uint8)
744 return fused_mla_decode_q_concat_kv_cache_insert(
745 ql_nope,
746 q_pe,
747 kv_c_normed,
748 k_pe,
749 cache,
750 slot_mapping,
751 ds_mla=True,
752 positions=positions,
753 cos_sin_cache=cos_sin_cache,
754 )
755 if is_quantized_kv_cache(self.kv_cache_dtype):
756 cache = self.kv_cache
757 if cache.dtype != torch.float8_e4m3fn:
758 cache = cache.view(torch.float8_e4m3fn)
759 mqa_q = fused_mla_decode_q_concat_kv_cache_insert(
760 ql_nope,
761 q_pe,
762 kv_c_normed,
763 k_pe,
764 cache,
765 slot_mapping,
766 q_scale_inv=self._q_scale_inv,
767 cache_scale_inv=self._k_scale_inv,
768 positions=positions,
769 cos_sin_cache=cos_sin_cache,
770 )
771 if not self.impl.supports_quant_query_input: # type: ignore[attr-defined]
772 # Backend dequantizes fp8 KV on load and takes a bf16 query
773 # (e.g. TRITON_MLA, the DSpark draft); undo the query
774 # quantization.
775 mqa_q = (mqa_q.to(torch.float32) * self._q_scale).to(ql_nope.dtype)
776 return mqa_q
777 return fused_mla_decode_q_concat_kv_cache_insert(
778 ql_nope,
779 q_pe,
780 kv_c_normed,
781 k_pe,
782 self.kv_cache,
783 slot_mapping,
784 positions=positions,
785 cos_sin_cache=cos_sin_cache,
786 )
787
788 def _compute_prefill_context(
789 self,
790 q: torch.Tensor,
791 attn_metadata: "MLACommonMetadata",
792 ) -> tuple[torch.Tensor, torch.Tensor]:
793 """Chunked-context prefill, K3-fused. Replaces the impl's version.
794
795 Per chunk the impl gathers the paged latent, up-projects it, then casts
796 and concatenates K (and casts V) in two or three more launches. Here that
797 tail is one fused kernel per chunk -- ``fused_mla_kv_concat`` for a bf16
798 query, ``fused_mla_kv_concat_quant_fp8`` when the query is fp8 -- reading
799 the strided ``kv_b_proj`` output in place and writing a contiguous key, so
800 only the gather and ``kv_b_proj`` remain.
801
802 The impl's query cast is gone as well: ``q`` already carries
803 ``prefill.q_data_type`` because the new-token epilogue quantized it. The
804 gathered latent still gets the impl's cast to whatever ``kv_b_proj``
805 consumes -- free (a no-op ``.to``) for a checkpoint whose ``kv_b_proj``
806 takes the fp8 latent directly, and required for a bf16 one, which is
807 what a stock K3 checkpoint carries. Its output is bf16 either way.
808
809 The gathered ``k_pe`` is likewise used as-is (fp8 for a plain fp8 cache)
810 and needs no RoPE: it was rotated on the way in.
811
812 Chunk partials are written straight into the accumulating context partial
813 when the prefill backend honors ``out``, so only the (64x smaller) lse is
814 copied per chunk.
815
816 Decode context parallelism keeps using
817 ``impl._context_parallel_compute_prefill_context``; its extra allgather
818 and reorg are not fused here.
819 """
820 prefill = attn_metadata.prefill
821 assert prefill is not None
822 prefill_backend = prefill.prefill_backend
823 assert prefill_backend is not None
824 chunked_context = prefill.chunked_context
825 assert chunked_context is not None
826 assert q.dtype == prefill.q_data_type, (
827 "Kimi-K3 chunked context expects the new-token epilogue to have "
828 f"produced a {prefill.q_data_type} query; got {q.dtype}."
829 )
830
831 fp8_prefill = q.dtype == current_platform.fp8_dtype()
832 workspace = chunked_context.workspace
833 kv_cache = self._attn_read_kv_cache()
834 kv_b_proj_input_dtype = _get_kv_b_proj_input_dtype(self.kv_b_proj, fp8_prefill)
835
836 def run_chunk(
837 chunk, out: torch.Tensor | None = None
838 ) -> tuple[torch.Tensor, torch.Tensor]:
839 self._gather_context_latent(chunk, kv_cache, prefill, fp8_prefill)
840 gathered = workspace[: chunk.num_context_tokens]
841 kv_c_normed = gathered[..., : self.kv_lora_rank]
842 if kv_b_proj_input_dtype is not None:
843 kv_c_normed = kv_c_normed.to(kv_b_proj_input_dtype)
844 kv_nope = self.kv_b_proj(kv_c_normed)[0].view(
845 -1, self.num_local_heads, self.qk_nope_head_dim + self.v_head_dim
846 )
847 k_nope, v = kv_nope.split([self.qk_nope_head_dim, self.v_head_dim], dim=-1)
848 k_pe = gathered[..., self.kv_lora_rank :]
849 if fp8_prefill:
850 k, v = fused_mla_kv_concat_quant_fp8(k_nope, k_pe, v)
851 else:
852 k = fused_mla_kv_concat(k_nope, k_pe)
853 attn_output, attn_lse = prefill_backend.run_prefill_context_chunk(
854 chunk=chunk, q=q[chunk.token_slice], k=k, v=v, out=out
855 )
856 assert out is None or attn_output.data_ptr() == out.data_ptr(), (
857 f"{prefill_backend.get_name()} reports supports_out() but did not "
858 "write the context chunk into the `out` it was given."
859 )
860 return attn_output, attn_lse
861
862 chunks = chunked_context.chunks
863 if len(chunks) == 1 and not chunked_context.empty_token_slices:
864 # One chunk covering every prefill token: its partial *is* the context
865 # partial, so it needs neither an accumulator nor a copy.
866 return run_chunk(chunks[0])
867
868 # A backend honoring `out` writes each chunk's partial straight into the
869 # accumulator, so the per-chunk output copy disappears -- and because that
870 # contract fixes the trailing shape, the accumulator can be sized before
871 # any chunk runs. Otherwise the shape is only knowable from a real partial,
872 # so the first chunk runs ahead of the loop and is copied in.
873 writes_out = prefill_backend.supports_out()
874 if writes_out:
875 assert prefill.output_dtype is not None
876 output = torch.empty(
877 (q.shape[0], self.num_local_heads, self.v_head_dim),
878 dtype=prefill.output_dtype,
879 device=q.device,
880 )
881 output_lse = torch.empty(
882 (self.num_local_heads, q.shape[0]),
883 dtype=torch.float32,
884 device=q.device,
885 )
886 neutralize_empty_context_partials(chunked_context, output, output_lse)
887 else:
888 attn_output, attn_lse = run_chunk(chunks[0])
889 output, output_lse = init_mla_context_partial(
890 chunked_context, attn_output, attn_lse, num_tokens=q.shape[0]
891 )
892 accumulate_mla_context_chunk(
893 chunks[0], attn_output, attn_lse, output, output_lse
894 )
895 chunks = chunks[1:]
896
897 for chunk in chunks:
898 # A continuation chunk's leading tokens have to be merged with the
899 # partial already sitting there, so it cannot write in place.
900 out = (
901 output[chunk.token_slice]
902 if writes_out and not chunk.is_continuation
903 else None
904 )
905 attn_output, attn_lse = run_chunk(chunk, out=out)
906 accumulate_mla_context_chunk(
907 chunk,
908 attn_output,
909 attn_lse,
910 output,
911 output_lse,
912 output_written=out is not None,
913 )
914 return output, output_lse
915
916 def _gather_context_latent(
917 self,
918 chunk,
919 kv_cache: torch.Tensor,
920 prefill,
921 fp8_prefill: bool,
922 ) -> None:
923 """Gather one chunk's paged context latent into the workspace.
924
925 Dispatched exactly as in ``impl._compute_prefill_context``: an fp8 query
926 reads the plain fp8 cache in its stored layout, anything else lands in the
927 workspace as the model dtype.
928 """
929 workspace = prefill.chunked_context.workspace
930 toks = chunk.num_context_tokens
931 block_table = prefill.block_table[chunk.request_slice]
932 if self.kv_cache_dtype == "fp8_ds_mla":
933 ops.cp_gather_and_upconvert_fp8_kv_cache(
934 src_cache=kv_cache,
935 dst=workspace[:toks],
936 block_table=block_table,
937 workspace_starts=chunk.cu_seq_lens,
938 batch_size=chunk.num_requests,
939 seq_starts=chunk.starts,
940 )
941 elif not fp8_prefill:
942 ops.gather_and_maybe_dequant_cache(
943 src_cache=kv_cache,
944 dst=workspace,
945 block_table=block_table,
946 cu_seq_lens=chunk.cu_seq_lens,
947 token_to_seq=chunk.token_to_seq,
948 num_tokens=toks,
949 kv_cache_dtype=self.kv_cache_dtype,
950 scale=self._k_scale,
951 seq_starts=chunk.starts,
952 )
953 else:
954 ops.cp_gather_cache(
955 src_cache=kv_cache,
956 dst=workspace[:toks],
957 block_table=block_table,
958 cu_seq_lens=chunk.cu_seq_lens,
959 batch_size=chunk.num_requests,
960 seq_starts=chunk.starts,
961 )
962
963 def _forward_prefill_fused(
964 self,
965 q: torch.Tensor,
966 kv_c_normed: torch.Tensor,
967 k_pe: torch.Tensor,
968 positions: torch.Tensor | None,
969 cos_sin_cache: torch.Tensor | None,
970 slot_mapping: torch.Tensor,
971 attn_metadata,
972 out: torch.Tensor,
973 ) -> None:
974 """Prefill using the fused key-concat + cache-insert kernel.
975
976 Replaces ``_concat_k_nope_k_pe`` and the prefill cache write with one
977 fused kernel launch, dispatched by cache dtype. Chunked context runs
978 through this layer's ``_compute_prefill_context``, except under DCP where
979 it is delegated to the impl.
980
981 Supported configs (K3 fp8 policy):
982 - bf16 cache -> bf16 prefill query
983 - plain fp8 cache -> fp8 prefill query (unscaled q/k/v; cache _k_scale)
984 - fp8_ds_mla cache -> bf16 prefill query (656B per-tile self-scaled)
985 """
986 prefill = attn_metadata.prefill
987 has_context = prefill.chunked_context is not None
988 fp8_prefill = prefill.q_data_type == current_platform.fp8_dtype()
989
990 kv_nope = self.kv_b_proj(kv_c_normed)[0].view(
991 -1, self.num_local_heads, self.qk_nope_head_dim + self.v_head_dim
992 )
993 k_nope, v = kv_nope.split([self.qk_nope_head_dim, self.v_head_dim], dim=-1)
994
995 if self.kv_cache_dtype == "fp8_ds_mla":
996 # fp8_ds_mla cache (656B, per-tile self-scaled); bf16 attention.
997 assert not fp8_prefill, (
998 "Kimi-K3 fp8_ds_mla uses a bf16 prefill query; fp8 prefill "
999 "query is not supported with fp8_ds_mla."
1000 )
1001 kv_cache = self.kv_cache
1002 if kv_cache.dtype != torch.uint8:
1003 kv_cache = kv_cache.view(torch.uint8)
1004 k = fused_mla_key_concat_ds_mla_insert(
1005 q,
1006 k_nope,
1007 k_pe,
1008 kv_c_normed,
1009 kv_cache,
1010 slot_mapping,
1011 positions,
1012 cos_sin_cache,
1013 )
1014 elif is_quantized_kv_cache(self.kv_cache_dtype):
1015 assert fp8_prefill, (
1016 "Kimi-K3 fp8 KV cache requires an fp8 prefill query; enable "
1017 "--attention-config '{\"use_prefill_query_quantization\": true}'."
1018 )
1019 # Plain per-tensor fp8: quant q/k/v (unscaled, matching forward_mha's
1020 # unscaled `.to(fp8)`) and insert the fp8 latent (scaled by _k_scale).
1021 kv_cache = self.kv_cache
1022 if kv_cache.dtype != torch.float8_e4m3fn:
1023 kv_cache = kv_cache.view(torch.float8_e4m3fn)
1024 q, k, v = fused_mla_qkv_quant_kv_cache_fp8_insert(
1025 q,
1026 k_nope,
1027 k_pe,
1028 kv_c_normed,
1029 v,
1030 kv_cache,
1031 slot_mapping,
1032 self._one_scale,
1033 self._one_scale,
1034 self._one_scale,
1035 self._k_scale_inv,
1036 positions,
1037 cos_sin_cache,
1038 )
1039 else:
1040 # Concat full K = [k_nope | k_pe] and insert [kv_c_normed | k_pe]
1041 # into the paged cache for these prefill tokens, in one launch.
1042 k = fused_mla_key_concat_kv_cache_insert(
1043 q,
1044 k_nope,
1045 k_pe,
1046 kv_c_normed,
1047 self.kv_cache,
1048 slot_mapping,
1049 positions,
1050 cos_sin_cache,
1051 )
1052
1053 # When there is no chunked context, backends that honor `out` write the
1054 # attention result straight into it, avoiding a slice+flatten+copy.
1055 writes_out = not has_context and prefill.prefill_backend.supports_out()
1056 output_prefill = prefill.prefill_backend.run_prefill_new_tokens(
1057 q=q,
1058 k=k,
1059 v=v,
1060 return_softmax_lse=has_context,
1061 out=(
1062 out.view(-1, self.num_local_heads, self.v_head_dim)
1063 if writes_out
1064 else None
1065 ),
1066 )
1067
1068 if has_context:
1069 if self.dcp_world_size > 1:
1070 context_output, context_lse = (
1071 self.impl._context_parallel_compute_prefill_context( # type: ignore[attr-defined]
1072 q,
1073 self._attn_read_kv_cache(),
1074 attn_metadata,
1075 k_scale=self._k_scale,
1076 dcp_world_size=self.dcp_world_size,
1077 )
1078 )
1079 else:
1080 context_output, context_lse = self._compute_prefill_context(
1081 q, attn_metadata
1082 )
1083 suffix_output, suffix_lse = output_prefill
1084 out = out.view(-1, self.num_local_heads, self.v_head_dim)
1085 merge_attn_states(
1086 output=out,
1087 prefix_output=context_output[..., : self.v_head_dim],
1088 prefix_lse=context_lse,
1089 suffix_output=suffix_output[..., : self.v_head_dim],
1090 suffix_lse=suffix_lse,
1091 )
1092 elif not writes_out:
1093 out.copy_(output_prefill[..., : self.v_head_dim].flatten(start_dim=-2))