[Kimi-K3] Merge MLA gate into QKV-A projection (#54015)
Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>
Thien Tran committed
Aug 28, 2026 at 08:47 UTC
6ec92bcbc8ef9af25cb4834ba3d922115792fbeb
4 files changed
+280
-103
vllm/model_executor/layers/linear.py
+90
-1
@@ -2,7 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from abc import abstractmethod
5
-from collections.abc import Iterable
5
+from collections.abc import Callable, Iterable
6
from typing import Any
7
8
import torch
@@ -1512,6 +1512,95 @@ class MinimaxM3QKVParallelLinearWithIndexer(QKVParallelLinear):
1512
param_data.copy_(loaded_weight)
1513
1514
1515
+class KimiK3MergedQKVGateLinear(MergedColumnParallelLinear):
1516
+ """Kimi-K3 QKV-A projection fused with its output-gate projection.
1517
+
1518
+ NOTE: Kimi-K3-specific. This is tailored to its replicated Q/KV latent
1519
+ projections and tensor-parallel output gate; it is not a general-purpose
1520
+ linear layer. It lives here alongside ``MergedColumnParallelLinear``, whose
1521
+ sharding and weight-loading machinery it reuses.
1522
+
1523
+ The per-rank shard layout is ``[q_a | kv_a | gate]``. The latent shards
1524
+ stay replicated across tensor-parallel ranks while the gate shard is
1525
+ tensor-parallel, matching the standalone projections they replace.
1526
+ """
1527
+
1528
+ def __init__(
1529
+ self,
1530
+ hidden_size: int,
1531
+ q_lora_rank: int,
1532
+ kv_lora_rank: int,
1533
+ qk_rope_head_dim: int,
1534
+ total_num_heads: int,
1535
+ v_head_dim: int,
1536
+ bias: bool = False,
1537
+ quant_config: QuantizationConfig | None = None,
1538
+ prefix: str = "",
1539
+ ) -> None:
1540
+ tp_size = get_tensor_model_parallel_world_size()
1541
+
1542
+ # MergedColumnParallelLinear divides every logical shard by TP size.
1543
+ # Inflate the replicated latent shards to retain their checkpoint widths.
1544
+ output_sizes = [
1545
+ q_lora_rank * tp_size,
1546
+ (kv_lora_rank + qk_rope_head_dim) * tp_size,
1547
+ total_num_heads * v_head_dim,
1548
+ ]
1549
+ super().__init__(
1550
+ input_size=hidden_size,
1551
+ output_sizes=output_sizes,
1552
+ bias=bias,
1553
+ quant_config=quant_config,
1554
+ prefix=prefix,
1555
+ )
1556
+
1557
+ def _load_with_replicated_latents(
1558
+ self,
1559
+ loader: Callable[..., None],
1560
+ param: Parameter,
1561
+ loaded_weight: torch.Tensor,
1562
+ loaded_shard_id: tuple[int, ...] | int | None,
1563
+ ) -> None:
1564
+ is_replicated = isinstance(loaded_shard_id, int) and loaded_shard_id < 2
1565
+ if not is_replicated:
1566
+ loader(param, loaded_weight, loaded_shard_id)
1567
+ return
1568
+
1569
+ # The base loader narrows every checkpoint shard using its TP rank.
1570
+ # Replicated latent shards instead load the rank-zero slice on every rank.
1571
+ tp_rank = self.tp_rank
1572
+ param_tp_rank = getattr(param, "tp_rank", None)
1573
+ self.tp_rank = 0
1574
+ if param_tp_rank is not None:
1575
+ param.tp_rank = 0
1576
+ try:
1577
+ loader(param, loaded_weight, loaded_shard_id)
1578
+ finally:
1579
+ self.tp_rank = tp_rank
1580
+ if param_tp_rank is not None:
1581
+ param.tp_rank = param_tp_rank
1582
+
1583
+ def weight_loader(
1584
+ self,
1585
+ param: Parameter,
1586
+ loaded_weight: torch.Tensor,
1587
+ loaded_shard_id: tuple[int, ...] | int | None = None,
1588
+ ) -> None:
1589
+ self._load_with_replicated_latents(
1590
+ super().weight_loader, param, loaded_weight, loaded_shard_id
1591
+ )
1592
+
1593
+ def weight_loader_v2(
1594
+ self,
1595
+ param: BasevLLMParameter,
1596
+ loaded_weight: torch.Tensor,
1597
+ loaded_shard_id: tuple[int, ...] | int | None = None,
1598
+ ) -> None:
1599
+ self._load_with_replicated_latents(
1600
+ super().weight_loader_v2, param, loaded_weight, loaded_shard_id
1601
+ )
1602
+
1603
+
1604
# --8<-- [start:row_parallel_linear]
1605
@PluggableLayer.register("row_parallel_linear")
1606
class RowParallelLinear(LinearBase):
vllm/models/kimi_k3/nvidia/mla.py
+167
-94
@@ -5,7 +5,8 @@
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)
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
@@ -23,7 +24,8 @@ 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
26
-layers, enabled for DSpark) and an optional sigmoid output gate (``g_proj``).
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.
@@ -33,6 +35,7 @@ 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
@@ -60,9 +63,11 @@ from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
63
from vllm.model_executor.layers.layernorm import RMSNorm
64
from vllm.model_executor.layers.linear import (
65
ColumnParallelLinear,
66
+ KimiK3MergedQKVGateLinear,
67
MergedColumnParallelLinear,
68
ReplicatedLinear,
69
RowParallelLinear,
70
+ UnquantizedLinearMethod,
71
)
72
from vllm.model_executor.layers.quantization import QuantizationConfig
73
from vllm.model_executor.layers.quantization.utils.quant_utils import (
@@ -71,6 +76,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
76
from vllm.model_executor.layers.rotary_embedding import RotaryEmbedding, get_rope
77
from vllm.model_executor.utils import replace_parameter
78
from vllm.models.common.ops import fused_q_kv_rmsnorm
79
+from vllm.models.kimi_k3.nvidia.low_latency_gemm import try_low_latency_gemm
80
from vllm.models.kimi_k3.nvidia.ops.fused_mla_key_concat_kv_cache import (
81
fused_mla_decode_q_concat_kv_cache_insert,
82
fused_mla_key_concat_ds_mla_insert,
@@ -102,15 +108,14 @@ if TYPE_CHECKING:
108
109
logger = init_logger(__name__)
110
105
-# Below this many tokens, overlap the g_proj GEMM on the aux stream with the
106
-# attention front-end (the GEMM is small and launch-bound, so the overlap
107
-# hides it); at or above it, run the gate on the main stream.
111
+# Below this conservative threshold, overlap the gate projection with attention
112
+# on the auxiliary stream.
113
_GATE_MULTI_STREAM_TOKEN_THRESHOLD = 512
114
115
116
@torch.compile(backend=current_platform.simple_compile_backend)
117
def _gate_sigmoid_mul(attn_out: torch.Tensor, gate: torch.Tensor) -> torch.Tensor:
113
- """Apply the sigmoid output gate to a precomputed ``g_proj`` projection."""
118
+ """Apply the sigmoid output gate to a precomputed gate projection."""
119
return attn_out * gate.sigmoid()
120
121
@@ -144,6 +149,7 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
149
self.v_head_dim = v_head_dim
150
self.q_lora_rank = q_lora_rank
151
self.kv_lora_rank = kv_lora_rank
152
+ self.use_output_gate = use_output_gate
153
self.non_causal_multi_token_decode = non_causal_multi_token_decode
154
# Latent "head" seen by the attention kernel / KV cache.
155
self.head_size = kv_lora_rank + qk_rope_head_dim
@@ -192,22 +198,52 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
198
self.num_local_heads = num_heads // tp_size
199
200
# ---- Pre-attention projections (fusable front-end) ----
195
- # Two query variants: a low-rank q-LoRA path (Kimi-K3) fused with the
196
- # kv-down proj, or an uncompressed q path (Kimi-Linear, ``q_lora_rank``
197
- # None) with a standalone ``q_proj`` and separate ``kv_a_proj_with_mqa``.
201
+ # Exactly one query layout is constructed below. Kimi-K3 uses q-LoRA;
202
+ # Kimi-Linear uses independent full-rank Q and KV projections.
203
+ self.fused_qkv_a_proj: MergedColumnParallelLinear | None = None
204
+ self.fused_qkv_a_g_proj: KimiK3MergedQKVGateLinear | None = None
205
+ self.q_proj: ColumnParallelLinear | None = None
206
+ self.kv_a_proj_with_mqa: ReplicatedLinear | None = None
207
+ self.g_proj: ColumnParallelLinear | None = None
208
+ self.aux_stream: torch.cuda.Stream | None = None
209
+ self._gate_events: tuple[torch.cuda.Event, torch.cuda.Event] | None = None
210
+
211
+ # Kimi-K3: low-rank Q and KV projections share one input projection.
212
if self.q_lora_rank is not None:
199
- # Fused q-down + kv-down projection. Replicated (disable_tp) because
200
- # the low-rank latents are shared across TP ranks; TP splitting
201
- # happens at q_b_proj / kv_b_proj. Checkpoint weights ``q_a_proj``
202
- # and ``kv_a_proj_with_mqa`` map onto shards 0 and 1 respectively.
203
- self.fused_qkv_a_proj = MergedColumnParallelLinear(
204
- self.hidden_size,
205
- [self.q_lora_rank, self.kv_lora_rank + self.qk_rope_head_dim],
206
- bias=False,
207
- quant_config=quant_config,
208
- prefix=f"{prefix}.fused_qkv_a_proj",
209
- disable_tp=True,
210
- )
213
+ # The latent Q and KV shards are replicated across TP ranks. Their
214
+ # TP splitting happens in q_b_proj and kv_b_proj instead.
215
+ if use_output_gate:
216
+ # The output gate is shard 2 of the same input projection.
217
+ self.fused_qkv_a_g_proj = KimiK3MergedQKVGateLinear(
218
+ hidden_size=self.hidden_size,
219
+ q_lora_rank=self.q_lora_rank,
220
+ kv_lora_rank=self.kv_lora_rank,
221
+ qk_rope_head_dim=self.qk_rope_head_dim,
222
+ total_num_heads=num_heads,
223
+ v_head_dim=self.v_head_dim,
224
+ bias=False,
225
+ quant_config=quant_config,
226
+ prefix=f"{prefix}.fused_qkv_a_g_proj",
227
+ )
228
+ # Only supports unquantized weights for sliced side-stream path
229
+ # for now.
230
+ if aux_stream is not None and isinstance(
231
+ self.fused_qkv_a_g_proj.quant_method, UnquantizedLinearMethod
232
+ ):
233
+ self.aux_stream = aux_stream
234
+ self._gate_events = (torch.cuda.Event(), torch.cuda.Event())
235
+ else:
236
+ self.fused_qkv_a_proj = MergedColumnParallelLinear(
237
+ self.hidden_size,
238
+ [
239
+ self.q_lora_rank,
240
+ self.kv_lora_rank + self.qk_rope_head_dim,
241
+ ],
242
+ bias=False,
243
+ quant_config=quant_config,
244
+ prefix=f"{prefix}.fused_qkv_a_proj",
245
+ disable_tp=True,
246
+ )
247
self.q_a_layernorm = RMSNorm(self.q_lora_rank, eps=config.rms_norm_eps)
248
self.q_b_proj = ColumnParallelLinear(
249
self.q_lora_rank,
@@ -216,9 +252,9 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
252
quant_config=quant_config,
253
prefix=f"{prefix}.q_b_proj",
254
)
255
+
256
+ # Kimi-Linear: Q and KV use independent full-rank input projections.
257
else:
220
- # Uncompressed query: full-rank q_proj (TP-split over heads) plus a
221
- # replicated kv-down projection (shared latent across TP ranks).
258
self.q_proj = ColumnParallelLinear(
259
self.hidden_size,
260
self.num_heads * self.qk_head_dim,
@@ -233,6 +269,16 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
269
quant_config=quant_config,
270
prefix=f"{prefix}.kv_a_proj_with_mqa",
271
)
272
+ if use_output_gate:
273
+ # Keep the gate standalone for the non-LoRA layout.
274
+ self.g_proj = ColumnParallelLinear(
275
+ self.hidden_size,
276
+ self.num_heads * self.v_head_dim,
277
+ bias=False,
278
+ quant_config=quant_config,
279
+ prefix=f"{prefix}.g_proj",
280
+ )
281
+
282
self.kv_a_layernorm = RMSNorm(self.kv_lora_rank, eps=config.rms_norm_eps)
283
self.kv_b_proj = ColumnParallelLinear(
284
self.kv_lora_rank,
@@ -242,28 +288,7 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
288
prefix=f"{prefix}.kv_b_proj",
289
)
290
245
- # ---- Post-attention projections ----
246
- self.use_output_gate = use_output_gate
247
- self.g_proj = (
248
- ColumnParallelLinear(
249
- self.hidden_size,
250
- self.num_heads * self.v_head_dim,
251
- bias=False,
252
- quant_config=quant_config,
253
- prefix=f"{prefix}.g_proj",
254
- )
255
- if use_output_gate
256
- else None
257
- )
258
- # Aux stream (created at the model level, DeepseekV4 convention) for
259
- # overlapping the g_proj GEMM with the attention front-end. None on
260
- # ROCm/non-cuda -> maybe_execute_in_parallel falls back to sequential.
261
- self.aux_stream = aux_stream
262
- self._gate_events = (
263
- [torch.cuda.Event(), torch.cuda.Event()]
264
- if self.g_proj is not None and current_platform.is_cuda_alike()
265
- else None
266
- )
291
+ # ---- Post-attention projection ----
292
self.o_proj = RowParallelLinear(
293
self.num_heads * self.v_head_dim,
294
self.hidden_size,
@@ -483,76 +508,124 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
508
# ------------------------------------------------------------------
509
# Forward
510
# ------------------------------------------------------------------
486
- def _forward_attn(
511
+ def _unquantized_gemm(
512
+ self, hidden_states: torch.Tensor, weight: torch.Tensor
513
+ ) -> torch.Tensor:
514
+ out = try_low_latency_gemm(hidden_states, weight)
515
+ return out if out is not None else F.linear(hidden_states, weight)
516
+
517
+ def _apply_q_lora_attention(
518
self,
519
positions: torch.Tensor,
520
hidden_states: torch.Tensor,
521
+ qkv_lora: torch.Tensor,
522
) -> torch.Tensor:
491
- """Attention front-end: fused qkv-a proj -> norms -> q_b -> attention.
492
-
493
- Returns the pre-gate attention output ``[num_tokens,
494
- num_local_heads * v_head_dim]``. On a profile/dummy run
495
- it returns a zeroed buffer.
496
- """
497
- if self.q_lora_rank is not None:
498
- qkv_lora = self.fused_qkv_a_proj(hidden_states)[0]
499
- q_c, kv_c, k_pe = qkv_lora.split(
500
- [self.q_lora_rank, self.kv_lora_rank, self.qk_rope_head_dim], dim=-1
501
- )
502
- q_c, kv_c_normed = fused_q_kv_rmsnorm(
503
- q_c,
504
- kv_c,
505
- self.q_a_layernorm.weight.data,
506
- self.kv_a_layernorm.weight.data,
507
- self.rms_norm_eps,
508
- )
509
- q = self.q_b_proj(q_c)[0].view(-1, self.num_local_heads, self.qk_head_dim)
510
- else:
511
- # Uncompressed query: project directly (no q-LoRA, no q norm) and
512
- # normalize only the kv latent.
513
- q = self.q_proj(hidden_states)[0].view(
514
- -1, self.num_local_heads, self.qk_head_dim
515
- )
516
- kv_lora = self.kv_a_proj_with_mqa(hidden_states)[0]
517
- kv_c, k_pe = kv_lora.split(
518
- [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1
519
- )
520
- kv_c_normed = self.kv_a_layernorm(kv_c)
521
- k_pe = k_pe.unsqueeze(1)
523
+ """Expand q-LoRA output and run attention."""
524
+ q_lora_rank = self.q_lora_rank
525
+ assert q_lora_rank is not None
526
+ q_c, kv_c, k_pe = qkv_lora.split(
527
+ [q_lora_rank, self.kv_lora_rank, self.qk_rope_head_dim], dim=-1
528
+ )
529
+ q_c, kv_c_normed = fused_q_kv_rmsnorm(
530
+ q_c,
531
+ kv_c,
532
+ self.q_a_layernorm.weight.data,
533
+ self.kv_a_layernorm.weight.data,
534
+ self.rms_norm_eps,
535
+ )
536
+ q = self.q_b_proj(q_c)[0].view(-1, self.num_local_heads, self.qk_head_dim)
537
538
attn_out = torch.empty(
539
(hidden_states.shape[0], self.num_local_heads * self.v_head_dim),
540
dtype=hidden_states.dtype,
541
device=hidden_states.device,
542
)
528
- self._attention(positions, q, kv_c_normed, k_pe, attn_out)
543
+ self._attention(positions, q, kv_c_normed, k_pe.unsqueeze(1), attn_out)
544
return attn_out
545
531
- def forward(
546
+ def _forward_q_lora(
547
self,
548
positions: torch.Tensor,
549
hidden_states: torch.Tensor,
535
- ) -> torch.Tensor:
536
- # Both branches produce (attn_out, gate); they differ only in whether
537
- # the g_proj GEMM is overlapped on the aux stream.
538
- g_proj = self.g_proj
539
- events = self._gate_events
550
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
551
+ """Run the q-LoRA path, including gate overlap when eligible."""
552
+ q_lora_rank = self.q_lora_rank
553
+ assert q_lora_rank is not None
554
+ qkv_a_rows = q_lora_rank + self.kv_lora_rank + self.qk_rope_head_dim
555
+
556
+ # run g_proj in aux_stream
557
if (
541
- g_proj is not None
542
- and events is not None
543
- and self.aux_stream is not None
558
+ self._gate_events is not None
559
and hidden_states.shape[0] < _GATE_MULTI_STREAM_TOKEN_THRESHOLD
560
):
546
- attn_out, gate = maybe_execute_in_parallel(
547
- lambda: self._forward_attn(positions, hidden_states),
548
- lambda: g_proj(hidden_states)[0],
549
- events[0],
550
- events[1],
561
+ fused_qkv_a_g_proj = self.fused_qkv_a_g_proj
562
+ assert fused_qkv_a_g_proj is not None
563
+ qkv_a_weight = fused_qkv_a_g_proj.weight[:qkv_a_rows]
564
+ gate_weight = fused_qkv_a_g_proj.weight[qkv_a_rows:]
565
+
566
+ start_event, end_event = self._gate_events
567
+ return maybe_execute_in_parallel(
568
+ lambda: self._apply_q_lora_attention(
569
+ positions,
570
+ hidden_states,
571
+ self._unquantized_gemm(hidden_states, qkv_a_weight),
572
+ ),
573
+ lambda: self._unquantized_gemm(hidden_states, gate_weight),
574
+ start_event,
575
+ end_event,
576
self.aux_stream,
577
)
578
+
579
+ # run g_proj together with qkv_a
580
+ if self.use_output_gate:
581
+ qkv_a_proj = self.fused_qkv_a_g_proj
582
+ assert qkv_a_proj is not None
583
+ qkv_lora, gate = qkv_a_proj(hidden_states)[0].split(
584
+ [qkv_a_rows, self.num_local_heads * self.v_head_dim], dim=-1
585
+ )
586
+ else:
587
+ qkv_a_proj = self.fused_qkv_a_proj
588
+ assert qkv_a_proj is not None
589
+ qkv_lora = qkv_a_proj(hidden_states)[0]
590
+ gate = None
591
+
592
+ attn_out = self._apply_q_lora_attention(positions, hidden_states, qkv_lora)
593
+ return attn_out, gate
594
+
595
+ def _forward_full_rank_q(
596
+ self,
597
+ positions: torch.Tensor,
598
+ hidden_states: torch.Tensor,
599
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
600
+ """Run the full-rank Q path with an optional sequential gate."""
601
+ q_proj = self.q_proj
602
+ kv_a_proj_with_mqa = self.kv_a_proj_with_mqa
603
+ assert q_proj is not None
604
+ assert kv_a_proj_with_mqa is not None
605
+
606
+ q = q_proj(hidden_states)[0].view(-1, self.num_local_heads, self.qk_head_dim)
607
+ kv_lora = kv_a_proj_with_mqa(hidden_states)[0]
608
+ kv_c, k_pe = kv_lora.split([self.kv_lora_rank, self.qk_rope_head_dim], dim=-1)
609
+ kv_c_normed = self.kv_a_layernorm(kv_c)
610
+ attn_out = torch.empty(
611
+ (hidden_states.shape[0], self.num_local_heads * self.v_head_dim),
612
+ dtype=hidden_states.dtype,
613
+ device=hidden_states.device,
614
+ )
615
+ self._attention(positions, q, kv_c_normed, k_pe.unsqueeze(1), attn_out)
616
+
617
+ gate = self.g_proj(hidden_states)[0] if self.g_proj is not None else None
618
+ return attn_out, gate
619
+
620
+ def forward(
621
+ self,
622
+ positions: torch.Tensor,
623
+ hidden_states: torch.Tensor,
624
+ ) -> torch.Tensor:
625
+ if self.q_lora_rank is None:
626
+ attn_out, gate = self._forward_full_rank_q(positions, hidden_states)
627
else:
554
- attn_out = self._forward_attn(positions, hidden_states)
555
- gate = g_proj(hidden_states)[0] if g_proj is not None else None
628
+ attn_out, gate = self._forward_q_lora(positions, hidden_states)
629
630
if gate is not None:
631
attn_out = _gate_sigmoid_mul(attn_out, gate)
vllm/models/kimi_k3/nvidia/model.py
+12
-4
@@ -1120,6 +1120,7 @@ class KimiLinearModel(nn.Module, EagleModelMixin, SupportsQuant):
1120
"in_proj_qkvgfab": ["q_proj", "k_proj", "v_proj", "b_proj", "f_a_proj"],
1121
"conv1d": ["q_conv1d", "k_conv1d", "v_conv1d"],
1122
"fused_qkv_a_proj": ["q_a_proj", "kv_a_proj_with_mqa"],
1123
+ "fused_qkv_a_g_proj": ["q_a_proj", "kv_a_proj_with_mqa", "g_proj"],
1124
}
1125
1126
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
@@ -1460,10 +1461,17 @@ class KimiLinearModel(nn.Module, EagleModelMixin, SupportsQuant):
1461
if use_full_rank_gate:
1462
stacked_params_mapping.append((".in_proj_qkvgfab", ".g_proj", 3))
1463
if getattr(self.config, "q_lora_rank", None) is not None:
1463
- stacked_params_mapping += [
1464
- (".fused_qkv_a_proj", ".q_a_proj", 0),
1465
- (".fused_qkv_a_proj", ".kv_a_proj_with_mqa", 1),
1466
- ]
1464
+ if self.config.mla_use_output_gate:
1465
+ stacked_params_mapping += [
1466
+ (".fused_qkv_a_g_proj", ".q_a_proj", 0),
1467
+ (".fused_qkv_a_g_proj", ".kv_a_proj_with_mqa", 1),
1468
+ (".fused_qkv_a_g_proj", ".g_proj", 2),
1469
+ ]
1470
+ else:
1471
+ stacked_params_mapping += [
1472
+ (".fused_qkv_a_proj", ".q_a_proj", 0),
1473
+ (".fused_qkv_a_proj", ".kv_a_proj_with_mqa", 1),
1474
+ ]
1475
use_mega_moe = any(
1476
module.use_mega_moe
1477
for module in self.modules()
vllm/models/kimi_k3/nvidia/mtp.py
+11
-4
@@ -272,10 +272,17 @@ class KimiK3MTP(nn.Module):
272
if use_full_rank_gate:
273
stacked_params_mapping.append((".in_proj_qkvgfab", ".g_proj", 3))
274
if getattr(self.config, "q_lora_rank", None) is not None:
275
- stacked_params_mapping += [
276
- (".fused_qkv_a_proj", ".q_a_proj", 0),
277
- (".fused_qkv_a_proj", ".kv_a_proj_with_mqa", 1),
278
- ]
275
+ if self.config.mla_use_output_gate:
276
+ stacked_params_mapping += [
277
+ (".fused_qkv_a_g_proj", ".q_a_proj", 0),
278
+ (".fused_qkv_a_g_proj", ".kv_a_proj_with_mqa", 1),
279
+ (".fused_qkv_a_g_proj", ".g_proj", 2),
280
+ ]
281
+ else:
282
+ stacked_params_mapping += [
283
+ (".fused_qkv_a_proj", ".q_a_proj", 0),
284
+ (".fused_qkv_a_proj", ".kv_a_proj_with_mqa", 1),
285
+ ]
286
287
use_mega_moe = any(
288
module.use_mega_moe