+12








7c6729b769
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com> Signed-off-by: Isotr0py <Isotr0py@outlook.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com> Signed-off-by: Yongye Zhu <zyy1102000@gmail.com> Co-authored-by: Isotr0py <Isotr0py@outlook.com> Co-authored-by: Thien Tran <gau.nernst@yahoo.com.sg> Co-authored-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: Yongye Zhu <zyy1102000@gmail.com> Co-authored-by: Ziming Huang <zelda.huanghuang@gmail.com> Co-authored-by: Roger Wang <hey@rogerw.io> Co-authored-by: Isotr0py <mozf@inferact.ai> Co-authored-by: aoshen02 <aoshen02@users.noreply.github.com> Co-authored-by: Woosuk Kwon <woosuk@inferact.ai> Co-authored-by: Jee Jee Li <jeejeelee@inferact.ai> Co-authored-by: aoshen02 <aoshen@inferact.ai> Co-authored-by: Summer Yang <girasoleyang@gmail.com> Co-authored-by: Kevin H. Luu <khluu000@gmail.com> Co-authored-by: Bowen Wang <abmfy@icloud.com> Co-authored-by: gnovack <novackgm@gmail.com> Co-authored-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: xiaozhoupy <peiyuanzhou1994@gmail.com> Co-authored-by: Roy Wang <yasong.wang@inferact.ai> Co-authored-by: Jeff (Junze) Ma <93145857+majunze2001@users.noreply.github.com> Co-authored-by: Codex <codex@openai.com>
404 lines
16 KiB
Python
404 lines
16 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Inference-only Kimi-K3 Multi-Token-Prediction (MTP) draft model."""
|
|
|
|
import copy
|
|
from collections.abc import Iterable
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
from vllm.config import VllmConfig
|
|
from vllm.logger import init_logger
|
|
from vllm.model_executor.layers.fused_moe import (
|
|
fused_moe_make_expert_params_mapping,
|
|
)
|
|
from vllm.model_executor.layers.layernorm import RMSNorm
|
|
from vllm.model_executor.layers.logits_processor import LogitsProcessor
|
|
from vllm.model_executor.layers.quantization import QuantizationConfig
|
|
from vllm.model_executor.layers.vocab_parallel_embedding import (
|
|
ParallelLMHead,
|
|
VocabParallelEmbedding,
|
|
)
|
|
from vllm.model_executor.model_loader.weight_utils import (
|
|
default_weight_loader,
|
|
maybe_remap_kv_scale_name,
|
|
)
|
|
from vllm.model_executor.models.utils import get_pp_missing_layer_names, maybe_prefix
|
|
from vllm.sequence import IntermediateTensors
|
|
from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
|
|
|
|
from ..common.mtp import fused_mtp_input
|
|
from .linear import KimiDecoderLayer, get_spec_layer_idx_from_weight_name
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
class SharedHead(nn.Module):
|
|
def __init__(
|
|
self,
|
|
config: KimiLinearConfig,
|
|
prefix: str,
|
|
quant_config: QuantizationConfig | None = None,
|
|
) -> None:
|
|
super().__init__()
|
|
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
|
self.head = ParallelLMHead(
|
|
config.vocab_size,
|
|
config.hidden_size,
|
|
quant_config=quant_config,
|
|
prefix=maybe_prefix(prefix, "head"),
|
|
)
|
|
|
|
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
|
return self.norm(hidden_states)
|
|
|
|
|
|
class KimiK3MultiTokenPredictorLayer(nn.Module):
|
|
def __init__(
|
|
self,
|
|
config: KimiLinearConfig,
|
|
vllm_config: VllmConfig,
|
|
prefix: str,
|
|
) -> None:
|
|
super().__init__()
|
|
self.config = config
|
|
quant_config = vllm_config.quant_config
|
|
|
|
self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
|
self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
|
self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
|
|
|
|
self.shared_head = SharedHead(
|
|
config=config, prefix=prefix, quant_config=quant_config
|
|
)
|
|
# The MTP block is a standard KimiDecoderLayer, but it must NOT use the
|
|
# attn-residual (block-residual) scheme even when the base model does:
|
|
# the draft starts from the target's hidden state, without the main
|
|
# model's accumulated per-block residual tensor. We disable it by
|
|
# shallow-copying the config with ``attn_res_block_size=None``.
|
|
block_config = copy.copy(config)
|
|
block_config.attn_res_block_size = None
|
|
# NOTE: the prefix must end in the numeric spec-layer index so that
|
|
# KimiDecoderLayer can parse ``layer_idx`` and pick MLA (full attn).
|
|
self.mtp_block = KimiDecoderLayer(block_config, vllm_config, prefix=prefix)
|
|
|
|
def forward(
|
|
self,
|
|
input_ids: torch.Tensor,
|
|
positions: torch.Tensor,
|
|
previous_hidden_states: torch.Tensor,
|
|
inputs_embeds: torch.Tensor | None = None,
|
|
spec_step_index: int = 0,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
assert inputs_embeds is not None
|
|
hidden_states = self.eh_proj(
|
|
fused_mtp_input(
|
|
positions,
|
|
inputs_embeds,
|
|
previous_hidden_states,
|
|
self.enorm.weight,
|
|
self.hnorm.weight,
|
|
self.enorm.variance_epsilon,
|
|
)
|
|
)
|
|
|
|
hidden_states, residual = self.mtp_block(
|
|
positions=positions,
|
|
hidden_states=hidden_states,
|
|
residual=None,
|
|
)
|
|
# Produce the normalized logits input and the pre-norm recurrent state
|
|
# in one fused add-RMSNorm launch.
|
|
logits_hidden_states, hidden_states = self.shared_head.norm(
|
|
hidden_states, residual
|
|
)
|
|
return logits_hidden_states, hidden_states
|
|
|
|
|
|
class KimiK3MultiTokenPredictor(nn.Module):
|
|
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
|
super().__init__()
|
|
config: KimiLinearConfig = vllm_config.model_config.hf_text_config
|
|
self.config = config
|
|
self.mtp_start_layer_idx = config.num_hidden_layers
|
|
self.num_mtp_layers = config.num_nextn_predict_layers
|
|
|
|
self.layers = torch.nn.ModuleDict(
|
|
{
|
|
str(idx): KimiK3MultiTokenPredictorLayer(
|
|
config, vllm_config, f"{prefix}.layers.{idx}"
|
|
)
|
|
for idx in range(
|
|
self.mtp_start_layer_idx,
|
|
self.mtp_start_layer_idx + self.num_mtp_layers,
|
|
)
|
|
}
|
|
)
|
|
self.embed_tokens = VocabParallelEmbedding(
|
|
config.vocab_size,
|
|
config.hidden_size,
|
|
prefix=maybe_prefix(prefix, "embed_tokens"),
|
|
)
|
|
self.logits_processor = LogitsProcessor(config.vocab_size)
|
|
|
|
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
|
return self.embed_tokens(input_ids)
|
|
|
|
def forward(
|
|
self,
|
|
input_ids: torch.Tensor,
|
|
positions: torch.Tensor,
|
|
previous_hidden_states: torch.Tensor,
|
|
inputs_embeds: torch.Tensor | None = None,
|
|
spec_step_idx: int = 0,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
if inputs_embeds is None:
|
|
inputs_embeds = self.embed_tokens(input_ids)
|
|
current_step_idx = spec_step_idx % self.num_mtp_layers
|
|
return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
|
|
input_ids,
|
|
positions,
|
|
previous_hidden_states,
|
|
inputs_embeds,
|
|
current_step_idx,
|
|
)
|
|
|
|
def compute_logits(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
spec_step_idx: int = 0,
|
|
) -> torch.Tensor:
|
|
current_step_idx = spec_step_idx % self.num_mtp_layers
|
|
mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
|
|
logits = self.logits_processor(mtp_layer.shared_head.head, hidden_states)
|
|
return logits
|
|
|
|
|
|
class KimiK3MTP(nn.Module):
|
|
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
|
super().__init__()
|
|
self.config = vllm_config.model_config.hf_text_config
|
|
self.quant_config = vllm_config.quant_config
|
|
self.model = KimiK3MultiTokenPredictor(
|
|
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
|
|
)
|
|
|
|
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
|
return self.model.embed_input_ids(input_ids)
|
|
|
|
def forward(
|
|
self,
|
|
input_ids: torch.Tensor | None,
|
|
positions: torch.Tensor,
|
|
hidden_states: torch.Tensor,
|
|
intermediate_tensors: IntermediateTensors | None = None,
|
|
inputs_embeds: torch.Tensor | None = None,
|
|
spec_step_idx: int = 0,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
return self.model(
|
|
input_ids,
|
|
positions,
|
|
hidden_states,
|
|
inputs_embeds,
|
|
spec_step_idx,
|
|
)
|
|
|
|
def compute_logits(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
spec_step_idx: int = 0,
|
|
) -> torch.Tensor | None:
|
|
return self.model.compute_logits(hidden_states, spec_step_idx)
|
|
|
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
|
# Mirror KimiLinearForCausalLM.load_weights naming: leading-dot shard
|
|
# names, q_lora-conditional fused QKV, and w1/w2/w3 expert weights.
|
|
kda_config = self.config.linear_attn_config
|
|
use_full_rank_gate = bool(
|
|
kda_config and kda_config.get("use_full_rank_gate", False)
|
|
)
|
|
beta_shard_id = 5 if use_full_rank_gate else 3
|
|
stacked_params_mapping = [
|
|
# (param_name, shard_name, shard_id)
|
|
(".in_proj_qkvgfab", ".q_proj", 0),
|
|
(".in_proj_qkvgfab", ".k_proj", 1),
|
|
(".in_proj_qkvgfab", ".v_proj", 2),
|
|
(".in_proj_qkvgfab", ".b_proj", beta_shard_id),
|
|
(".in_proj_qkvgfab", ".f_a_proj", 4),
|
|
(".conv1d", ".q_conv1d", 0),
|
|
(".conv1d", ".k_conv1d", 1),
|
|
(".conv1d", ".v_conv1d", 2),
|
|
(".gate_up_proj", ".gate_proj", 0),
|
|
(".gate_up_proj", ".up_proj", 1),
|
|
]
|
|
if use_full_rank_gate:
|
|
stacked_params_mapping.append((".in_proj_qkvgfab", ".g_proj", 3))
|
|
if getattr(self.config, "q_lora_rank", None) is not None:
|
|
stacked_params_mapping += [
|
|
(".fused_qkv_a_proj", ".q_a_proj", 0),
|
|
(".fused_qkv_a_proj", ".kv_a_proj_with_mqa", 1),
|
|
]
|
|
|
|
expert_params_mapping = (
|
|
fused_moe_make_expert_params_mapping(
|
|
self,
|
|
ckpt_gate_proj_name="w1",
|
|
ckpt_down_proj_name="w2",
|
|
ckpt_up_proj_name="w3",
|
|
num_experts=self.config.num_experts,
|
|
)
|
|
if self.config.is_moe
|
|
else []
|
|
)
|
|
|
|
pp_missing_layer_names = get_pp_missing_layer_names(self)
|
|
params_dict = dict(self.named_parameters())
|
|
# Under the MXFP4 quant interface the routed experts register unpacked
|
|
# params (``w13_weight``), while the compressed-tensors checkpoint names
|
|
# them ``.weight_packed``. Rebind so the expert mapping resolves; scales
|
|
# already share the ``.weight_scale`` suffix.
|
|
experts_unpacked = not any(n.endswith("w13_weight_packed") for n in params_dict)
|
|
loaded_params: set[str] = set()
|
|
for name, loaded_weight in weights:
|
|
if "rotary_emb.inv_freq" in name:
|
|
continue
|
|
# The multimodal checkpoint prefixes text weights with
|
|
# ``language_model.``; strip it so names match this draft model's
|
|
# parameter paths (``model.layers.{i}.``). Non-text weights
|
|
# (vision_tower, mm_projector, ...) never match a spec layer below.
|
|
if name.startswith("language_model."):
|
|
name = name[len("language_model.") :]
|
|
if experts_unpacked and name.endswith(".weight_packed"):
|
|
name = name.replace(".weight_packed", ".weight")
|
|
spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
|
|
if spec_layer is None:
|
|
continue
|
|
name = self._rewrite_spec_layer_name(spec_layer, name)
|
|
|
|
for param_name, weight_name, shard_id in stacked_params_mapping:
|
|
if weight_name not in name:
|
|
continue
|
|
# Routed experts (``.experts.{i}.w1/w2/w3``) are handled by the
|
|
# expert mapping below; skip them here. Shared experts
|
|
# (``.shared_experts.``) use gate/up_proj and fall through.
|
|
if ".experts." in name:
|
|
continue
|
|
name_mapped = name.replace(weight_name, param_name)
|
|
# Only take this mapping if the fused destination actually
|
|
# exists (e.g. QKV fusion is only present when q_lora is used).
|
|
if name_mapped not in params_dict:
|
|
continue
|
|
if name_mapped in pp_missing_layer_names:
|
|
continue
|
|
name = name_mapped
|
|
param = params_dict[name]
|
|
weight_loader = param.weight_loader
|
|
weight_loader(param, loaded_weight, shard_id)
|
|
break
|
|
else:
|
|
for (
|
|
expert_param_name,
|
|
expert_weight_name,
|
|
expert_id,
|
|
expert_shard_id,
|
|
) in expert_params_mapping:
|
|
if expert_weight_name not in name:
|
|
continue
|
|
name_mapped = name.replace(expert_weight_name, expert_param_name)
|
|
if name_mapped in pp_missing_layer_names:
|
|
continue
|
|
param = params_dict[name_mapped]
|
|
weight_loader = param.weight_loader
|
|
weight_loader(
|
|
param,
|
|
loaded_weight,
|
|
name_mapped,
|
|
shard_id=expert_shard_id,
|
|
expert_id=expert_id,
|
|
)
|
|
name = name_mapped
|
|
break
|
|
else:
|
|
if name.endswith(".bias") and name not in params_dict:
|
|
continue
|
|
remapped_name = maybe_remap_kv_scale_name(name, params_dict)
|
|
if remapped_name is None:
|
|
continue
|
|
name = remapped_name
|
|
|
|
# The embedding is shared across MTP layers; only the first
|
|
# spec layer carries the hoisted (non-".layers") copy.
|
|
if spec_layer != self.model.mtp_start_layer_idx and (
|
|
".layers" not in name
|
|
):
|
|
continue
|
|
if name in pp_missing_layer_names:
|
|
continue
|
|
# The base model uses an attn-residual scheme whose per-layer
|
|
# weights (self_attention_res_*, mlp_res_*) are not used by
|
|
# the draft block; such names have no matching parameter and
|
|
# are safely skipped.
|
|
if name not in params_dict:
|
|
continue
|
|
|
|
param = params_dict[name]
|
|
weight_loader = getattr(
|
|
param, "weight_loader", default_weight_loader
|
|
)
|
|
weight_loader(param, loaded_weight)
|
|
loaded_params.add(name)
|
|
|
|
# Validate that weights were loaded for each expected MTP layer.
|
|
loaded_layers: set[int] = set()
|
|
for param_name in loaded_params:
|
|
spec_layer = get_spec_layer_idx_from_weight_name(self.config, param_name)
|
|
if spec_layer is not None:
|
|
loaded_layers.add(spec_layer)
|
|
for layer_idx in range(
|
|
self.model.mtp_start_layer_idx,
|
|
self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
|
|
):
|
|
if layer_idx not in loaded_layers:
|
|
raise ValueError(
|
|
f"MTP speculative decoding layer {layer_idx} weights "
|
|
f"missing from checkpoint. The checkpoint may not include "
|
|
f"the MTP layer weights. Use a checkpoint that includes "
|
|
f"MTP layer weights, or disable speculative decoding."
|
|
)
|
|
|
|
return loaded_params
|
|
|
|
def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
|
|
"""Rewrite a checkpoint weight name to this module's parameter path.
|
|
|
|
Top-level MTP submodules (enorm/hnorm/eh_proj/shared_head) stay under
|
|
``model.layers.{spec_layer}.*``; the shared ``embed_tokens`` is hoisted
|
|
to ``model.*``; everything else is a transformer-block weight and gets
|
|
``.mtp_block`` inserted.
|
|
"""
|
|
spec_layer_weight_names = [
|
|
"embed_tokens",
|
|
"enorm",
|
|
"hnorm",
|
|
"eh_proj",
|
|
"shared_head",
|
|
]
|
|
shared_weight_names = ["embed_tokens"]
|
|
spec_layer_weight = False
|
|
shared_weight = False
|
|
for weight_name in spec_layer_weight_names:
|
|
if weight_name in name:
|
|
spec_layer_weight = True
|
|
if weight_name in shared_weight_names:
|
|
shared_weight = True
|
|
break
|
|
if not spec_layer_weight:
|
|
name = name.replace(
|
|
f"model.layers.{spec_layer}.",
|
|
f"model.layers.{spec_layer}.mtp_block.",
|
|
)
|
|
elif shared_weight:
|
|
name = name.replace(f"model.layers.{spec_layer}.", "model.")
|
|
return name
|