main
py 462 lines 18.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Inference-only Kimi-K3 Multi-Token-Prediction (MTP) draft model."""
4
5 import copy
6 from collections.abc import Iterable
7
8 import torch
9 import torch.nn as nn
10
11 import vllm.envs as envs
12 from vllm.config import VllmConfig
13 from vllm.forward_context import get_forward_context, is_forward_context_available
14 from vllm.logger import init_logger
15 from vllm.model_executor.layers.fused_moe import (
16 fused_moe_make_expert_params_mapping,
17 )
18 from vllm.model_executor.layers.layernorm import RMSNorm
19 from vllm.model_executor.layers.linear import ReplicatedLinear
20 from vllm.model_executor.layers.logits_processor import LogitsProcessor
21 from vllm.model_executor.layers.quantization import QuantizationConfig
22 from vllm.model_executor.layers.vocab_parallel_embedding import (
23 ParallelLMHead,
24 VocabParallelEmbedding,
25 )
26 from vllm.model_executor.model_loader.weight_utils import (
27 default_weight_loader,
28 maybe_remap_kv_scale_name,
29 )
30 from vllm.model_executor.models.utils import get_pp_missing_layer_names, maybe_prefix
31 from vllm.models.common.ops.sequence_parallel import (
32 sp_all_gather,
33 sp_padding_mask,
34 sp_shard,
35 )
36 from vllm.sequence import IntermediateTensors
37 from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
38
39 from ..common.mtp import fused_mtp_input
40 from .low_latency_gemm import enable_kimi_k3_low_latency_gemm
41 from .model import (
42 KimiDecoderLayer,
43 KimiMoE,
44 get_spec_layer_idx_from_weight_name,
45 make_kimi_k3_mega_moe_expert_params_mapping,
46 )
47
48 logger = init_logger(__name__)
49
50
51 class SharedHead(nn.Module):
52 def __init__(
53 self,
54 config: KimiLinearConfig,
55 prefix: str,
56 quant_config: QuantizationConfig | None = None,
57 ) -> None:
58 super().__init__()
59 self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
60 self.head = ParallelLMHead(
61 config.vocab_size,
62 config.hidden_size,
63 quant_config=quant_config,
64 prefix=maybe_prefix(prefix, "head"),
65 )
66
67 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
68 return self.norm(hidden_states)
69
70
71 class KimiK3MultiTokenPredictorLayer(nn.Module):
72 def __init__(
73 self,
74 config: KimiLinearConfig,
75 vllm_config: VllmConfig,
76 prefix: str,
77 ) -> None:
78 super().__init__()
79 self.config = config
80 quant_config = vllm_config.quant_config
81
82 self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
83 self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
84 self.eh_proj = ReplicatedLinear(
85 config.hidden_size * 2,
86 config.hidden_size,
87 bias=False,
88 quant_config=None,
89 prefix=maybe_prefix(prefix, "eh_proj"),
90 return_bias=False,
91 )
92
93 self.shared_head = SharedHead(
94 config=config, prefix=prefix, quant_config=quant_config
95 )
96 # The MTP block starts without the base model's AttnRes state.
97 block_config = copy.copy(config)
98 block_config.attn_res_block_size = None
99 # NOTE: the prefix must end in the numeric spec-layer index so that
100 # KimiDecoderLayer can parse ``layer_idx`` and pick MLA (full attn).
101 # Own aux stream for the MLA g_proj output-gate overlap (DeepseekV4
102 # convention: the MTP block creates its own stream).
103 aux_stream = torch.cuda.Stream()
104 self.mtp_block = KimiDecoderLayer(
105 block_config, vllm_config, prefix=prefix, aux_stream=aux_stream
106 )
107
108 def forward(
109 self,
110 input_ids: torch.Tensor,
111 positions: torch.Tensor,
112 previous_hidden_states: torch.Tensor,
113 inputs_embeds: torch.Tensor | None = None,
114 spec_step_index: int = 0,
115 ) -> tuple[torch.Tensor, torch.Tensor]:
116 assert inputs_embeds is not None
117 hidden_states = self.eh_proj(
118 fused_mtp_input(
119 positions,
120 inputs_embeds,
121 previous_hidden_states,
122 self.enorm.weight,
123 self.hnorm.weight,
124 self.enorm.variance_epsilon,
125 )
126 )
127 if self.mtp_block.use_sequence_parallel:
128 if envs.VLLM_MOE_SKIP_PADDING and is_forward_context_available():
129 forward_context = get_forward_context()
130 forward_context.is_padding = sp_padding_mask(
131 forward_context.is_padding, hidden_states
132 )
133 hidden_states = sp_shard(hidden_states)
134
135 hidden_states, _, residual = self.mtp_block(
136 positions=positions,
137 hidden_states=hidden_states,
138 residual=None,
139 )
140 if self.mtp_block.use_sequence_parallel:
141 assert residual is not None
142 hidden_states = hidden_states + residual
143 hidden_states = sp_all_gather(hidden_states)[: positions.shape[0]]
144 logits_hidden_states = self.shared_head.norm(hidden_states)
145 return logits_hidden_states, hidden_states
146
147 # Produce the normalized logits input and the pre-norm recurrent state
148 # in one fused add-RMSNorm launch.
149 logits_hidden_states, hidden_states = self.shared_head.norm(
150 hidden_states, residual
151 )
152 return logits_hidden_states, hidden_states
153
154
155 class KimiK3MultiTokenPredictor(nn.Module):
156 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
157 super().__init__()
158 config: KimiLinearConfig = vllm_config.model_config.hf_text_config
159 self.config = config
160 self.mtp_start_layer_idx = config.num_hidden_layers
161 self.num_mtp_layers = config.num_nextn_predict_layers
162
163 self.layers = torch.nn.ModuleDict(
164 {
165 str(idx): KimiK3MultiTokenPredictorLayer(
166 config, vllm_config, f"{prefix}.layers.{idx}"
167 )
168 for idx in range(
169 self.mtp_start_layer_idx,
170 self.mtp_start_layer_idx + self.num_mtp_layers,
171 )
172 }
173 )
174 self.embed_tokens = VocabParallelEmbedding(
175 config.vocab_size,
176 config.hidden_size,
177 prefix=maybe_prefix(prefix, "embed_tokens"),
178 )
179 self.logits_processor = LogitsProcessor(config.vocab_size)
180
181 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
182 return self.embed_tokens(input_ids)
183
184 def forward(
185 self,
186 input_ids: torch.Tensor,
187 positions: torch.Tensor,
188 previous_hidden_states: torch.Tensor,
189 inputs_embeds: torch.Tensor | None = None,
190 spec_step_idx: int = 0,
191 ) -> tuple[torch.Tensor, torch.Tensor]:
192 if inputs_embeds is None:
193 inputs_embeds = self.embed_tokens(input_ids)
194 current_step_idx = spec_step_idx % self.num_mtp_layers
195 return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
196 input_ids,
197 positions,
198 previous_hidden_states,
199 inputs_embeds,
200 current_step_idx,
201 )
202
203 def compute_logits(
204 self,
205 hidden_states: torch.Tensor,
206 spec_step_idx: int = 0,
207 ) -> torch.Tensor:
208 current_step_idx = spec_step_idx % self.num_mtp_layers
209 mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
210 logits = self.logits_processor(mtp_layer.shared_head.head, hidden_states)
211 return logits
212
213
214 class KimiK3MTP(nn.Module):
215 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
216 super().__init__()
217 self.config = vllm_config.model_config.hf_text_config
218 self.quant_config = vllm_config.quant_config
219 self.model = KimiK3MultiTokenPredictor(
220 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
221 )
222 enable_kimi_k3_low_latency_gemm(self, vllm_config.model_config.dtype)
223
224 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
225 return self.model.embed_input_ids(input_ids)
226
227 def forward(
228 self,
229 input_ids: torch.Tensor | None,
230 positions: torch.Tensor,
231 hidden_states: torch.Tensor,
232 intermediate_tensors: IntermediateTensors | None = None,
233 inputs_embeds: torch.Tensor | None = None,
234 spec_step_idx: int = 0,
235 ) -> tuple[torch.Tensor, torch.Tensor]:
236 return self.model(
237 input_ids,
238 positions,
239 hidden_states,
240 inputs_embeds,
241 spec_step_idx,
242 )
243
244 def compute_logits(
245 self,
246 hidden_states: torch.Tensor,
247 spec_step_idx: int = 0,
248 ) -> torch.Tensor | None:
249 return self.model.compute_logits(hidden_states, spec_step_idx)
250
251 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
252 # Mirror KimiLinearForCausalLM.load_weights naming: leading-dot shard
253 # names, q_lora-conditional fused QKV, and w1/w2/w3 expert weights.
254 kda_config = self.config.linear_attn_config
255 use_full_rank_gate = bool(
256 kda_config and kda_config.get("use_full_rank_gate", False)
257 )
258 beta_shard_id = 5 if use_full_rank_gate else 3
259 stacked_params_mapping = [
260 # (param_name, shard_name, shard_id)
261 (".in_proj_qkvgfab", ".q_proj", 0),
262 (".in_proj_qkvgfab", ".k_proj", 1),
263 (".in_proj_qkvgfab", ".v_proj", 2),
264 (".in_proj_qkvgfab", ".b_proj", beta_shard_id),
265 (".in_proj_qkvgfab", ".f_a_proj", 4),
266 (".conv1d", ".q_conv1d", 0),
267 (".conv1d", ".k_conv1d", 1),
268 (".conv1d", ".v_conv1d", 2),
269 (".gate_up_proj", ".gate_proj", 0),
270 (".gate_up_proj", ".up_proj", 1),
271 ]
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 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
289 for module in self.modules()
290 if isinstance(module, KimiMoE)
291 )
292 if self.config.is_moe and use_mega_moe:
293 expert_params_mapping = make_kimi_k3_mega_moe_expert_params_mapping(
294 self.config.num_experts
295 )
296 elif self.config.is_moe:
297 expert_params_mapping = fused_moe_make_expert_params_mapping(
298 self,
299 ckpt_gate_proj_name="w1",
300 ckpt_down_proj_name="w2",
301 ckpt_up_proj_name="w3",
302 num_experts=self.config.num_experts,
303 )
304 else:
305 expert_params_mapping = []
306
307 pp_missing_layer_names = get_pp_missing_layer_names(self)
308 params_dict = dict(self.named_parameters())
309 # Under the MXFP4 quant interface the routed experts register unpacked
310 # params (``w13_weight``), while the compressed-tensors checkpoint names
311 # them ``.weight_packed``. Rebind so the expert mapping resolves; scales
312 # already share the ``.weight_scale`` suffix.
313 experts_unpacked = not use_mega_moe and not any(
314 n.endswith("w13_weight_packed") for n in params_dict
315 )
316 loaded_params: set[str] = set()
317 for name, loaded_weight in weights:
318 if "rotary_emb.inv_freq" in name:
319 continue
320 # The multimodal checkpoint prefixes text weights with
321 # ``language_model.``; strip it so names match this draft model's
322 # parameter paths (``model.layers.{i}.``). Non-text weights
323 # (vision_tower, mm_projector, ...) never match a spec layer below.
324 if name.startswith("language_model."):
325 name = name[len("language_model.") :]
326 if experts_unpacked and name.endswith(".weight_packed"):
327 name = name.replace(".weight_packed", ".weight")
328 spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
329 if spec_layer is None:
330 continue
331 name = self._rewrite_spec_layer_name(spec_layer, name)
332
333 for param_name, weight_name, shard_id in stacked_params_mapping:
334 if weight_name not in name:
335 continue
336 # Routed experts (``.experts.{i}.w1/w2/w3``) are handled by the
337 # expert mapping below; skip them here. Shared experts
338 # (``.shared_experts.``) use gate/up_proj and fall through.
339 if ".experts." in name:
340 continue
341 name_mapped = name.replace(weight_name, param_name)
342 # Only take this mapping if the fused destination actually
343 # exists (e.g. QKV fusion is only present when q_lora is used).
344 if name_mapped not in params_dict:
345 continue
346 if name_mapped in pp_missing_layer_names:
347 continue
348 name = name_mapped
349 param = params_dict[name]
350 weight_loader = param.weight_loader
351 weight_loader(param, loaded_weight, shard_id)
352 break
353 else:
354 for (
355 expert_param_name,
356 expert_weight_name,
357 expert_id,
358 expert_shard_id,
359 ) in expert_params_mapping:
360 if expert_weight_name not in name:
361 continue
362 name_mapped = name.replace(expert_weight_name, expert_param_name)
363 if name_mapped in pp_missing_layer_names:
364 continue
365 param = params_dict[name_mapped]
366 weight_loader = param.weight_loader
367 weight_loader(
368 param,
369 loaded_weight,
370 name_mapped,
371 shard_id=expert_shard_id,
372 expert_id=expert_id,
373 )
374 name = name_mapped
375 break
376 else:
377 if name.endswith(".bias") and name not in params_dict:
378 continue
379 remapped_name = maybe_remap_kv_scale_name(name, params_dict)
380 if remapped_name is None:
381 continue
382 name = remapped_name
383
384 # The embedding is shared across MTP layers; only the first
385 # spec layer carries the hoisted (non-".layers") copy.
386 if spec_layer != self.model.mtp_start_layer_idx and (
387 ".layers" not in name
388 ):
389 continue
390 if name in pp_missing_layer_names:
391 continue
392 # The base model uses an attn-residual scheme whose per-layer
393 # weights (self_attention_res_*, mlp_res_*) are not used by
394 # the draft block; such names have no matching parameter and
395 # are safely skipped.
396 if name not in params_dict:
397 continue
398
399 param = params_dict[name]
400 weight_loader = getattr(
401 param, "weight_loader", default_weight_loader
402 )
403 weight_loader(param, loaded_weight)
404 loaded_params.add(name)
405
406 # Validate that weights were loaded for each expected MTP layer.
407 loaded_layers: set[int] = set()
408 for param_name in loaded_params:
409 spec_layer = get_spec_layer_idx_from_weight_name(self.config, param_name)
410 if spec_layer is not None:
411 loaded_layers.add(spec_layer)
412 for layer_idx in range(
413 self.model.mtp_start_layer_idx,
414 self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
415 ):
416 if layer_idx not in loaded_layers:
417 raise ValueError(
418 f"MTP speculative decoding layer {layer_idx} weights "
419 f"missing from checkpoint. The checkpoint may not include "
420 f"the MTP layer weights. Use a checkpoint that includes "
421 f"MTP layer weights, or disable speculative decoding."
422 )
423
424 if use_mega_moe:
425 for module in self.modules():
426 if isinstance(module, KimiMoE) and module.use_mega_moe:
427 module.experts.finalize_weights()
428
429 return loaded_params
430
431 def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
432 """Rewrite a checkpoint weight name to this module's parameter path.
433
434 Top-level MTP submodules (enorm/hnorm/eh_proj/shared_head) stay under
435 ``model.layers.{spec_layer}.*``; the shared ``embed_tokens`` is hoisted
436 to ``model.*``; everything else is a transformer-block weight and gets
437 ``.mtp_block`` inserted.
438 """
439 spec_layer_weight_names = [
440 "embed_tokens",
441 "enorm",
442 "hnorm",
443 "eh_proj",
444 "shared_head",
445 ]
446 shared_weight_names = ["embed_tokens"]
447 spec_layer_weight = False
448 shared_weight = False
449 for weight_name in spec_layer_weight_names:
450 if weight_name in name:
451 spec_layer_weight = True
452 if weight_name in shared_weight_names:
453 shared_weight = True
454 break
455 if not spec_layer_weight:
456 name = name.replace(
457 f"model.layers.{spec_layer}.",
458 f"model.layers.{spec_layer}.mtp_block.",
459 )
460 elif shared_weight:
461 name = name.replace(f"model.layers.{spec_layer}.", "model.")
462 return name