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

458 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.layer_idx = layer_idx
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.
Earlier versions compiled ``layer.mlp`` + ``layer.self_attn.o_proj``
(PR #2690) and then the whole ``layer`` (perf/voxcpm2-streaming-vae).
Both still paid one Dynamo dispatch per layer per decode step.
V3 profiling showed 1,332 per-layer dispatches (~28 layers × ~47
decode steps) costing ~726 ms of CPU self-time for a long prompt.
Compiling ``forward`` at the model level lets Dynamo unroll the
28-layer Python loop inside the graph. Graph breaks at
PagedAttention produce sub-graphs but Dynamo memoises the whole
trace once, so the per-step dispatch drops from 28 to just a few.
"""
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