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