Files
vllm-omni/vllm_omni/model_executor/models/voxcpm2/minicpm4_paged.py
T

451 lines
16 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""MiniCPM4 with PagedAttention + fp32 RoPE/RMSNorm for VoxCPM2.
Uses vllm Attention for KV cache, keeps fp32 precision ops from
minicpm4_hf_compat.py to match native VoxCPM2 numerics.
"""
from __future__ import annotations
import math
from collections.abc import Iterable
from typing import Any
import torch
import torch.nn as nn
from vllm.config import CacheConfig, VllmConfig
from vllm.logger import init_logger
from vllm.model_executor.layers.attention import Attention
from vllm.model_executor.layers.layernorm import RMSNorm
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.utils import make_empty_intermediate_tensors_factory
from vllm.sequence import IntermediateTensors
from .minicpm4_hf_compat import (
_apply_rotary_pos_emb,
_MiniCPMLongRoPE,
_MiniCPMMLP,
)
logger = init_logger(__name__)
def _resolve_lm_cfg(config: Any) -> Any:
"""Extract lm_config from VoxCPM2Config, converting dict to namespace if needed."""
lm_cfg = getattr(config, "lm_config", config)
if isinstance(lm_cfg, dict):
class _Cfg:
pass
c = _Cfg()
for k, v in lm_cfg.items():
setattr(c, k, v)
return c
return lm_cfg
# ===================================================================
# Attention with vllm PagedAttention backend
# ===================================================================
class _PagedMiniCPM4Attention(nn.Module):
"""PagedAttention + fp32 RoPE with separate q/k/v projections."""
def __init__(
self,
hidden_size: int,
num_attention_heads: int,
num_key_value_heads: int,
kv_channels: int | None,
layer_idx: int,
cache_config: CacheConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.hidden_size = hidden_size
self.num_heads = num_attention_heads
self.head_dim = kv_channels if kv_channels else hidden_size // num_attention_heads
self.num_kv_heads = num_key_value_heads
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.q_proj = nn.Linear(hidden_size, self.q_size, bias=False)
self.k_proj = nn.Linear(hidden_size, self.kv_size, bias=False)
self.v_proj = nn.Linear(hidden_size, self.kv_size, bias=False)
self.o_proj = nn.Linear(self.q_size, hidden_size, bias=False)
self._fused_qkv_weight: torch.Tensor | None = None
self.attn = Attention(
self.num_heads,
self.head_dim,
scale=self.head_dim**-0.5,
num_kv_heads=self.num_kv_heads,
cache_config=cache_config,
prefix=f"{prefix}.attn",
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
rope_emb: _MiniCPMLongRoPE | None = None,
) -> torch.Tensor:
"""Forward: fused QKV → fp32 RoPE → PagedAttention → o_proj."""
if self._fused_qkv_weight is None:
self._fused_qkv_weight = torch.cat(
[
self.q_proj.weight,
self.k_proj.weight,
self.v_proj.weight,
],
dim=0,
).detach()
qkv = nn.functional.linear(hidden_states, self._fused_qkv_weight)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
if rope_emb is not None:
cos, sin = rope_emb(positions)
bsz = q.shape[0]
q_r = q.view(bsz, self.num_heads, self.head_dim)
k_r = k.view(bsz, self.num_kv_heads, self.head_dim)
q_r = q_r.unsqueeze(0).transpose(1, 2) # [1, heads, n_tokens, dim]
k_r = k_r.unsqueeze(0).transpose(1, 2) # [1, kv_heads, n_tokens, dim]
q_r, k_r = _apply_rotary_pos_emb(q_r, k_r, cos, sin)
q = q_r.transpose(1, 2).squeeze(0).reshape(bsz, -1) # [n_tokens, q_size]
k = k_r.transpose(1, 2).squeeze(0).reshape(bsz, -1) # [n_tokens, kv_size]
attn_output = self.attn(q, k, v)
output = self.o_proj(attn_output)
return output
# ===================================================================
# Decoder Layer
# ===================================================================
class _PagedMiniCPM4DecoderLayer(nn.Module):
"""Decoder layer: PagedAttention + fp32 RMSNorm + muP scale_depth."""
def __init__(
self,
hidden_size: int,
intermediate_size: int,
num_attention_heads: int,
num_key_value_heads: int,
kv_channels: int | None,
rms_norm_eps: float,
layer_idx: int,
num_hidden_layers: int,
use_mup: bool,
scale_depth: float,
cache_config: CacheConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.self_attn = _PagedMiniCPM4Attention(
hidden_size=hidden_size,
num_attention_heads=num_attention_heads,
num_key_value_heads=num_key_value_heads,
kv_channels=kv_channels,
layer_idx=layer_idx,
cache_config=cache_config,
prefix=f"{prefix}.self_attn",
)
self.mlp = _MiniCPMMLP(hidden_size, intermediate_size)
self.input_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
self.post_attention_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
self.use_mup = use_mup
self.scale_depth = scale_depth
self.num_hidden_layers = num_hidden_layers
def _residual_scale(self) -> float:
if self.use_mup:
return self.scale_depth / math.sqrt(self.num_hidden_layers)
return 1.0
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
residual: torch.Tensor | None,
rope_emb: _MiniCPMLongRoPE | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
# Pre-norm + attention
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
hidden_states = self.self_attn(positions, hidden_states, rope_emb)
scale = self._residual_scale()
if scale != 1.0:
hidden_states = residual + hidden_states * scale
else:
hidden_states = residual + hidden_states
# Pre-norm + FFN
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
if scale != 1.0:
hidden_states = residual + hidden_states * scale
else:
hidden_states = residual + hidden_states
return hidden_states, None
# ===================================================================
# Full Model
# ===================================================================
class MiniCPM4PagedForVoxCPM2(nn.Module):
"""PagedAttention base_lm (28 layers) for VoxCPM2 scaffold."""
def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
super().__init__()
config = vllm_config.model_config.hf_config
cache_config = vllm_config.cache_config
self.config = config
lm_cfg = _resolve_lm_cfg(config)
hidden_size = lm_cfg.hidden_size
num_hidden_layers = lm_cfg.num_hidden_layers
kv_channels = getattr(lm_cfg, "kv_channels", None)
self.vocab_size = lm_cfg.vocab_size
self.embed_tokens = nn.Embedding(self.vocab_size, hidden_size)
rope_scaling = getattr(lm_cfg, "rope_scaling", None)
if isinstance(rope_scaling, dict):
rope_scaling_dict = rope_scaling
elif hasattr(rope_scaling, "__dict__"):
rope_scaling_dict = {
"short_factor": rope_scaling.short_factor,
"long_factor": rope_scaling.long_factor,
"original_max_position_embeddings": rope_scaling.original_max_position_embeddings,
}
else:
rope_scaling_dict = {}
no_rope = getattr(lm_cfg, "no_rope", False)
if not no_rope:
self.rope_emb = _MiniCPMLongRoPE(
hidden_size=hidden_size,
num_attention_heads=lm_cfg.num_attention_heads,
kv_channels=kv_channels,
rope_theta=getattr(lm_cfg, "rope_theta", 10000.0),
max_position_embeddings=getattr(lm_cfg, "max_position_embeddings", 32768),
rope_scaling=rope_scaling_dict,
)
else:
self.rope_emb = None
self.layers = nn.ModuleList(
[
_PagedMiniCPM4DecoderLayer(
hidden_size=hidden_size,
intermediate_size=lm_cfg.intermediate_size,
num_attention_heads=lm_cfg.num_attention_heads,
num_key_value_heads=lm_cfg.num_key_value_heads,
kv_channels=kv_channels,
rms_norm_eps=lm_cfg.rms_norm_eps,
layer_idx=i,
num_hidden_layers=num_hidden_layers,
use_mup=getattr(lm_cfg, "use_mup", False),
scale_depth=getattr(lm_cfg, "scale_depth", 1.0),
cache_config=cache_config,
prefix=f"{prefix}.layers.{i}",
)
for i in range(num_hidden_layers)
]
)
self.norm = RMSNorm(hidden_size, eps=lm_cfg.rms_norm_eps)
self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
["hidden_states", "residual"], hidden_size
)
use_mup = getattr(lm_cfg, "use_mup", False)
self._scale_emb = getattr(lm_cfg, "scale_emb", 1.0) if use_mup else 1.0
self._compiled_layers: set[int] = set()
def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor:
return self.embed_tokens(input_ids) * self._scale_emb
def forward(
self,
input_ids: torch.Tensor | None,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**kwargs: Any,
) -> torch.Tensor | IntermediateTensors:
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.embed_input_ids(input_ids)
residual = None
for layer in self.layers:
hidden_states, residual = layer(
positions,
hidden_states,
residual,
self.rope_emb,
)
hidden_states = self.norm(hidden_states)
return hidden_states
def precompute_fused_qkv(self) -> None:
"""Materialize fused QKV weights before CUDA Graph capture."""
for layer in self.layers:
attn = layer.self_attn
if attn._fused_qkv_weight is None:
attn._fused_qkv_weight = torch.cat(
[attn.q_proj.weight, attn.k_proj.weight, attn.v_proj.weight],
dim=0,
).detach()
def compile_selective(self) -> list[str]:
"""Compile the full model ``forward`` as one graph.
``torch.compile`` is applied at the model level so Dynamo unrolls the
per-layer Python loop inside the graph. Graph breaks at PagedAttention
produce sub-graphs, but Dynamo memoises the whole trace once, so the
per-decode-step dispatch drops to just a few instead of one per layer.
"""
if self._compiled_layers:
return []
# Null the fused-qkv caches so the compile sees the real weight layout.
for layer in self.layers:
layer.self_attn._fused_qkv_weight = None
self.forward = torch.compile(self.forward, mode="default", fullgraph=False)
# Mark every layer as compiled so idempotent callers don't double-wrap.
self._compiled_layers.update(range(len(self.layers)))
return ["forward (whole model)"]
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load weights from native checkpoint (base_lm. prefix pre-stripped)."""
params_dict = dict(self.named_parameters(remove_duplicate=False))
loaded: set[str] = set()
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
param = params_dict.get(name)
if param is None:
continue
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded.add(name)
return loaded
# ===================================================================
# Residual LM with PagedAttention (no RoPE, 8 layers)
# ===================================================================
class MiniCPM4PagedResidualLM(nn.Module):
"""PagedAttention residual LM (8 layers, no RoPE) for VoxCPM2."""
def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
super().__init__()
config = vllm_config.model_config.hf_config
cache_config = vllm_config.cache_config
self.config = config
lm_cfg = _resolve_lm_cfg(config)
hidden_size = lm_cfg.hidden_size
num_hidden_layers = getattr(config, "residual_lm_num_layers", 8)
kv_channels = getattr(lm_cfg, "kv_channels", None)
self.rope_emb = None
self.layers = nn.ModuleList(
[
_PagedMiniCPM4DecoderLayer(
hidden_size=hidden_size,
intermediate_size=lm_cfg.intermediate_size,
num_attention_heads=lm_cfg.num_attention_heads,
num_key_value_heads=lm_cfg.num_key_value_heads,
kv_channels=kv_channels,
rms_norm_eps=lm_cfg.rms_norm_eps,
layer_idx=i,
num_hidden_layers=num_hidden_layers,
use_mup=getattr(lm_cfg, "use_mup", False),
scale_depth=getattr(lm_cfg, "scale_depth", 1.0),
cache_config=cache_config,
prefix=f"{prefix}.layers.{i}",
)
for i in range(num_hidden_layers)
]
)
self.norm = RMSNorm(hidden_size, eps=lm_cfg.rms_norm_eps)
self._compiled_layers: set[int] = set()
def forward(
self,
positions: torch.Tensor,
inputs_embeds: torch.Tensor,
) -> torch.Tensor:
hidden_states = inputs_embeds
residual = None
for layer in self.layers:
hidden_states, residual = layer(
positions,
hidden_states,
residual,
self.rope_emb,
)
hidden_states = self.norm(hidden_states)
return hidden_states
def precompute_fused_qkv(self) -> None:
"""Materialize fused QKV weights before CUDA Graph capture."""
for layer in self.layers:
attn = layer.self_attn
if attn._fused_qkv_weight is None:
attn._fused_qkv_weight = torch.cat(
[attn.q_proj.weight, attn.k_proj.weight, attn.v_proj.weight],
dim=0,
).detach()
def compile_selective(self) -> list[str]:
"""Compile the full residual model forward as one graph (same strategy as base_lm)."""
if self._compiled_layers:
return []
for layer in self.layers:
layer.self_attn._fused_qkv_weight = None
self.forward = torch.compile(self.forward, mode="default", fullgraph=False)
self._compiled_layers.update(range(len(self.layers)))
return ["forward (whole residual)"]
def load_weights_from_native(self, native_residual_lm: nn.Module) -> int:
"""Load weights from native residual_lm. Returns param count."""
params_dict = dict(self.named_parameters(remove_duplicate=False))
loaded = 0
for name, param in native_residual_lm.named_parameters():
if "rotary_emb" in name:
continue
target = params_dict.get(name)
if target is None:
continue
weight_loader = getattr(target, "weight_loader", default_weight_loader)
weight_loader(target, param.data)
loaded += 1
return loaded