2417 lines
99 KiB
Python
2417 lines
99 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||
|
||
import typing
|
||
from collections.abc import Callable, Iterable
|
||
from itertools import islice
|
||
|
||
import regex as re
|
||
import os
|
||
import torch
|
||
import torch.nn as nn
|
||
|
||
from vllm.compilation.decorators import support_torch_compile
|
||
from vllm.config import VllmConfig, get_current_vllm_config
|
||
from vllm.distributed import (
|
||
get_ep_group,
|
||
get_tensor_model_parallel_rank,
|
||
get_tensor_model_parallel_world_size,
|
||
)
|
||
from vllm.forward_context import get_forward_context
|
||
from vllm.model_executor.layers.activation import SiluAndMul, SiluAndMulWithClamp
|
||
from vllm.model_executor.layers.deepseek_v4_attention import (
|
||
DeepseekV4Indexer,
|
||
DeepseekV4MLAModules,
|
||
DeepseekV4MultiHeadLatentAttentionWrapper,
|
||
)
|
||
from vllm.model_executor.layers.fused_moe import FusedMoE, GateLinear
|
||
from vllm.model_executor.layers.fused_moe.layer import UnquantizedFusedMoEMethod
|
||
from vllm.model_executor.layers.fused_moe.router.fused_topk_bias_router import (
|
||
fused_topk_bias,
|
||
)
|
||
from vllm.model_executor.layers.layernorm import RMSNorm
|
||
from vllm.model_executor.layers.linear import (
|
||
ColumnParallelLinear,
|
||
MergedColumnParallelLinear,
|
||
RowParallelLinear,
|
||
)
|
||
from vllm.model_executor.layers.logits_processor import LogitsProcessor
|
||
from vllm.model_executor.layers.quantization import (
|
||
QuantizationConfig,
|
||
QuantizationMethods,
|
||
)
|
||
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
|
||
from vllm.model_executor.layers.quantization.mxfp4 import Mxfp4MoEMethod
|
||
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||
is_layer_skipped,
|
||
)
|
||
from vllm.model_executor.layers.rotary_embedding import get_rope
|
||
from vllm.model_executor.layers.vocab_parallel_embedding import (
|
||
ParallelLMHead,
|
||
VocabParallelEmbedding,
|
||
)
|
||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||
from vllm.model_executor.utils import set_weight_attrs
|
||
from vllm.platforms import current_platform
|
||
from vllm.sequence import IntermediateTensors
|
||
from vllm.triton_utils import tl, triton
|
||
from vllm.utils.torch_utils import direct_register_custom_op
|
||
|
||
from .utils import (
|
||
AutoWeightsLoader,
|
||
WeightsMapper,
|
||
extract_layer_index,
|
||
make_layers,
|
||
maybe_prefix,
|
||
)
|
||
|
||
_DEEPSEEK_V4_EXPERT_DTYPES = ("fp4", "fp8")
|
||
|
||
|
||
class DeepseekV4MLP(nn.Module):
|
||
def __init__(
|
||
self,
|
||
hidden_size: int,
|
||
intermediate_size: int,
|
||
hidden_act: str,
|
||
swiglu_limit: float | None = None,
|
||
quant_config: QuantizationConfig | None = None,
|
||
reduce_results: bool = True,
|
||
is_sequence_parallel: bool = False,
|
||
prefix: str = "",
|
||
) -> None:
|
||
super().__init__()
|
||
|
||
# If is_sequence_parallel, the input and output tensors are sharded
|
||
# across the ranks within the tp_group. In this case the weights are
|
||
# replicated and no collective ops are needed.
|
||
# Otherwise we use standard TP with an allreduce at the end.
|
||
self.gate_up_proj = MergedColumnParallelLinear(
|
||
hidden_size,
|
||
[intermediate_size] * 2,
|
||
bias=False,
|
||
quant_config=quant_config,
|
||
disable_tp=is_sequence_parallel,
|
||
prefix=f"{prefix}.gate_up_proj",
|
||
)
|
||
self.down_proj = RowParallelLinear(
|
||
intermediate_size,
|
||
hidden_size,
|
||
bias=False,
|
||
quant_config=quant_config,
|
||
reduce_results=reduce_results,
|
||
disable_tp=is_sequence_parallel,
|
||
prefix=f"{prefix}.down_proj",
|
||
)
|
||
if hidden_act != "silu":
|
||
raise ValueError(
|
||
f"Unsupported activation: {hidden_act}. Only silu is supported for now."
|
||
)
|
||
if swiglu_limit is not None:
|
||
self.act_fn = SiluAndMulWithClamp(swiglu_limit)
|
||
else:
|
||
self.act_fn = SiluAndMul()
|
||
|
||
def forward(self, x):
|
||
gate_up, _ = self.gate_up_proj(x)
|
||
x = self.act_fn(gate_up)
|
||
x, _ = self.down_proj(x)
|
||
return x
|
||
|
||
|
||
class DeepseekV4FP8Config(Fp8Config):
|
||
"""FP8 config for DeepSeek V4 with expert-dtype-aware MoE dispatch.
|
||
|
||
DeepSeek V4 checkpoints always use FP8 block quantization for
|
||
linear/attention layers. The MoE expert weights vary by checkpoint:
|
||
- ``expert_dtype="fp4"`` (e.g. DeepSeek-V4-Flash): MXFP4 experts
|
||
with ue8m0 (e8m0fnu) FP8 linear scales.
|
||
- ``expert_dtype="fp8"`` (e.g. DeepSeek-V4-Flash-Base): FP8 block
|
||
experts with float32 FP8 linear scales.
|
||
|
||
The dispatch and the linear scale dtype are both keyed off
|
||
``expert_dtype`` from the model's hf_config; missing values default
|
||
to ``"fp4"`` so existing FP4 checkpoints stay unchanged.
|
||
|
||
NOTE: ``expert_dtype`` is resolved lazily because this config is
|
||
constructed during VllmConfig setup, before ``set_current_vllm_config``
|
||
is active. Reading hf_config eagerly in ``__init__`` would always see
|
||
the default ``"fp4"`` and silently misroute Flash-Base checkpoints.
|
||
"""
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self._resolved_expert_dtype: str | None = None
|
||
# ``is_scale_e8m0`` is a property that resolves on first read,
|
||
# by which time the current vllm_config has been set.
|
||
|
||
@property
|
||
def expert_dtype(self) -> str:
|
||
if self._resolved_expert_dtype is None:
|
||
try:
|
||
hf_config = get_current_vllm_config().model_config.hf_config
|
||
except Exception:
|
||
# vllm_config not yet set; return safe default but do NOT
|
||
# cache — a later call inside set_current_vllm_config may
|
||
# resolve differently.
|
||
return "fp4"
|
||
expert_dtype = getattr(hf_config, "expert_dtype", "fp4")
|
||
if expert_dtype not in _DEEPSEEK_V4_EXPERT_DTYPES:
|
||
raise ValueError(
|
||
f"Unsupported DeepSeek V4 expert_dtype={expert_dtype!r}; "
|
||
f"expected one of {_DEEPSEEK_V4_EXPERT_DTYPES}."
|
||
)
|
||
self._resolved_expert_dtype = expert_dtype
|
||
return self._resolved_expert_dtype
|
||
|
||
@property
|
||
def is_scale_e8m0(self) -> bool:
|
||
# FP4 checkpoints store FP8 linear scales as e8m0fnu; FP8 expert
|
||
# checkpoints (Flash-Base) store them as float32.
|
||
return self.expert_dtype == "fp4"
|
||
|
||
@classmethod
|
||
def get_name(cls) -> QuantizationMethods:
|
||
return "deepseek_v4_fp8"
|
||
|
||
@classmethod
|
||
def override_quantization_method(
|
||
cls, hf_quant_cfg, user_quant, hf_config=None
|
||
) -> QuantizationMethods | None:
|
||
if not (
|
||
isinstance(hf_quant_cfg, dict)
|
||
and hf_quant_cfg.get("quant_method") in ("fp8", "deepseek_v4_fp8")
|
||
):
|
||
return None
|
||
model_type = getattr(hf_config, "model_type", None)
|
||
if model_type == "deepseek_v4" or user_quant == "deepseek_v4_fp8":
|
||
return "deepseek_v4_fp8"
|
||
return None
|
||
|
||
def get_quant_method(self, layer, prefix):
|
||
if isinstance(layer, FusedMoE):
|
||
if is_layer_skipped(
|
||
prefix=prefix,
|
||
ignored_layers=self.ignored_layers,
|
||
fused_mapping=self.packed_modules_mapping,
|
||
):
|
||
return UnquantizedFusedMoEMethod(layer.moe_config)
|
||
if self.expert_dtype == "fp4":
|
||
return Mxfp4MoEMethod(layer.moe_config)
|
||
# expert_dtype == "fp8": fall through to Fp8Config which
|
||
# returns Fp8MoEMethod with block-wise float32 scales.
|
||
return super().get_quant_method(layer, prefix)
|
||
|
||
def is_mxfp4_quant(self, prefix, layer):
|
||
return isinstance(layer, FusedMoE) and self.expert_dtype == "fp4"
|
||
|
||
|
||
import triton
|
||
import triton.language as tl
|
||
import torch
|
||
|
||
"""
|
||
NVFP4 staging kernel — full FP4 (E2M1) activations + UE4M3 block16 scales.
|
||
|
||
The mxf4nvf4 PTX instruction requires BOTH A and B to be FP4 (E2M1 packed).
|
||
This kernel quantizes BF16 activations → E2M1 packed uint8 with UE4M3 scales.
|
||
"""
|
||
|
||
|
||
@triton.jit
|
||
def _deepseek_v4_stage_mega_moe_inputs_kernel(
|
||
hidden_states,
|
||
x_fp4, # uint8, shape (M, K//2) — E2M1 packed, 2 values per byte
|
||
x_sf, # int32, shape (M, K//64) — UE4M3 packed, 4 scales per int32
|
||
topk_ids,
|
||
topk_weights,
|
||
topk_idx_out,
|
||
topk_weights_out,
|
||
hidden_stride_m: tl.constexpr,
|
||
hidden_stride_k: tl.constexpr,
|
||
x_stride_m: tl.constexpr,
|
||
x_stride_k: tl.constexpr,
|
||
x_sf_stride_m: tl.constexpr,
|
||
x_sf_stride_k: tl.constexpr,
|
||
topk_ids_stride_m: tl.constexpr,
|
||
topk_ids_stride_k: tl.constexpr,
|
||
topk_weights_stride_m: tl.constexpr,
|
||
topk_weights_stride_k: tl.constexpr,
|
||
topk_idx_stride_m: tl.constexpr,
|
||
topk_idx_stride_k: tl.constexpr,
|
||
topk_weights_out_stride_m: tl.constexpr,
|
||
topk_weights_out_stride_k: tl.constexpr,
|
||
hidden_size: tl.constexpr,
|
||
top_k: tl.constexpr,
|
||
BLOCK_K: tl.constexpr, # 128 elements (loaded from hidden)
|
||
GROUP_K: tl.constexpr, # 16 (NVFP4 group_size)
|
||
BLOCK_TOPK: tl.constexpr,
|
||
) -> None:
|
||
token_id = tl.program_id(0)
|
||
k_block_id = tl.program_id(1)
|
||
|
||
k_offsets = k_block_id * BLOCK_K + tl.arange(0, BLOCK_K)
|
||
k_mask = k_offsets < hidden_size
|
||
hidden = tl.load(
|
||
hidden_states + token_id * hidden_stride_m + k_offsets * hidden_stride_k,
|
||
mask=k_mask,
|
||
other=0.0,
|
||
).to(tl.float32)
|
||
|
||
num_groups: tl.constexpr = BLOCK_K // GROUP_K # 8
|
||
hidden_groups = tl.reshape(hidden, [num_groups, GROUP_K])
|
||
abs_groups = tl.reshape(tl.abs(hidden), [num_groups, GROUP_K])
|
||
amax = tl.max(abs_groups, axis=1)
|
||
amax = tl.maximum(amax, 1.0e-4)
|
||
|
||
# ---- UE4M3 scale computation ----
|
||
# scale = amax / 6.0 (E2M1 max value = 6)
|
||
# Then quantize scale to UE4M3 format
|
||
scale = amax / 6.0
|
||
scale_bits = scale.to(tl.uint32, bitcast=True)
|
||
scale_exp = (scale_bits >> 23) & 0xFF
|
||
scale_mant = scale_bits & 0x7FFFFF
|
||
|
||
# Convert FP32 → E4M3 manually
|
||
e4m3_exp = scale_exp - 120 # FP32 bias=127, E4M3 bias=7
|
||
e4m3_exp = tl.maximum(e4m3_exp, 0)
|
||
e4m3_exp = tl.minimum(e4m3_exp, 15)
|
||
e4m3_mant = scale_mant >> 20
|
||
round_bit = (scale_mant >> 19) & 1
|
||
e4m3_mant = e4m3_mant + round_bit
|
||
overflow = e4m3_mant >= 8
|
||
e4m3_mant = tl.where(overflow, 0, e4m3_mant)
|
||
e4m3_exp = tl.where(overflow, e4m3_exp + 1, e4m3_exp)
|
||
e4m3_exp = tl.minimum(e4m3_exp, 15)
|
||
scale_e4m3_bits = (e4m3_exp << 3) | e4m3_mant
|
||
|
||
# Reconstruct dequantized scale for E2M1 quantization
|
||
e4m3_exp_for_recon = tl.maximum(e4m3_exp.to(tl.int32) - 7, -126)
|
||
two_pow_exp_bits = (e4m3_exp_for_recon + 127).to(tl.uint32) << 23
|
||
two_pow_exp = two_pow_exp_bits.to(tl.float32, bitcast=True)
|
||
normal_value = (1.0 + e4m3_mant.to(tl.float32) / 8.0) * two_pow_exp
|
||
subnormal_value = (e4m3_mant.to(tl.float32) / 8.0) * 0.015625
|
||
e4m3_value = tl.where(e4m3_exp == 0, subnormal_value, normal_value)
|
||
|
||
# ---- E2M1 FP4 quantization ----
|
||
# E2M1 LUT (unsigned): [0, 0.5, 1, 1.5, 2, 3, 4, 6]
|
||
# Nearest-neighbor using thresholds (midpoints between consecutive values)
|
||
scaled = hidden_groups * (1.0 / tl.maximum(e4m3_value, 1e-6))[:, None]
|
||
# Clamp to E2M1 range [-6, 6]
|
||
scaled = tl.maximum(scaled, -6.0)
|
||
scaled = tl.minimum(scaled, 6.0)
|
||
|
||
abs_s = tl.abs(scaled)
|
||
# E2M1 quantization using arithmetic instead of nested tl.where (Triton compile error)
|
||
# LUT: [0, 0.5, 1, 1.5, 2, 3, 4, 6] → thresholds at midpoints
|
||
# idx = sum(abs_s >= threshold_i) for thresholds [0.25, 0.75, 1.25, 1.75, 2.5, 3.5, 5.0]
|
||
e2m1_idx = ((abs_s >= 0.25).to(tl.int32) + (abs_s >= 0.75).to(tl.int32) +
|
||
(abs_s >= 1.25).to(tl.int32) + (abs_s >= 1.75).to(tl.int32) +
|
||
(abs_s >= 2.5).to(tl.int32) + (abs_s >= 3.5).to(tl.int32) +
|
||
(abs_s >= 5.0).to(tl.int32))
|
||
sign_bit = (scaled < 0).to(tl.int32)
|
||
e2m1_4bit = (sign_bit << 3) | e2m1_idx # 4-bit: (sign << 3) | index
|
||
|
||
# Pack 2 E2M1 values per byte: even→low nibble, odd→high nibble
|
||
PACKED_K: tl.constexpr = BLOCK_K // 2 # 64
|
||
e2m1_pairs = tl.reshape(e2m1_4bit, [PACKED_K, 2])
|
||
even, odd = tl.split(e2m1_pairs) # splits last axis (size 2) into two [PACKED_K] tensors
|
||
packed_byte = (odd.to(tl.uint8) << 4) | even.to(tl.uint8)
|
||
|
||
packed_k_offsets = k_block_id * PACKED_K + tl.arange(0, PACKED_K)
|
||
packed_k_mask = packed_k_offsets < (hidden_size // 2)
|
||
tl.store(
|
||
x_fp4 + token_id * x_stride_m + packed_k_offsets * x_stride_k,
|
||
packed_byte,
|
||
mask=packed_k_mask,
|
||
)
|
||
|
||
# Pack UE4M3 bytes into int32 (NVFP4: group_size=16, 4 groups per 64 elements)
|
||
# 8 groups per k_block of 128 → 2 int32s per k_block
|
||
# int32 can only pack 4 bytes (shifts >= 32 are UB on GPU), so split into two packs
|
||
scale_offsets = tl.arange(0, num_groups) # [0..7]
|
||
first_half = scale_offsets < 4 # groups 0-3 → int32[0]
|
||
second_half = scale_offsets >= 4 # groups 4-7 → int32[1]
|
||
|
||
packed_lo = tl.sum(
|
||
tl.where(first_half, scale_e4m3_bits.to(tl.int32) << (scale_offsets * 8), 0),
|
||
axis=0,
|
||
).to(tl.int32)
|
||
packed_hi = tl.sum(
|
||
tl.where(second_half, scale_e4m3_bits.to(tl.int32) << ((scale_offsets - 4) * 8), 0),
|
||
axis=0,
|
||
).to(tl.int32)
|
||
|
||
# Write 2 int32s per k_block: x_sf shape is (M, K//64) = (M, num_k_blocks * 2)
|
||
sf_base = token_id * x_sf_stride_m + k_block_id * 2 * x_sf_stride_k
|
||
tl.store(x_sf + sf_base, packed_lo)
|
||
tl.store(x_sf + sf_base + x_sf_stride_k, packed_hi)
|
||
|
||
if k_block_id == 0:
|
||
topk_offsets = tl.arange(0, BLOCK_TOPK)
|
||
topk_mask = topk_offsets < top_k
|
||
|
||
ids = tl.load(
|
||
topk_ids + token_id * topk_ids_stride_m + topk_offsets * topk_ids_stride_k,
|
||
mask=topk_mask,
|
||
other=0,
|
||
).to(tl.int64)
|
||
tl.store(
|
||
topk_idx_out
|
||
+ token_id * topk_idx_stride_m
|
||
+ topk_offsets * topk_idx_stride_k,
|
||
ids,
|
||
mask=topk_mask,
|
||
)
|
||
|
||
weights = tl.load(
|
||
topk_weights
|
||
+ token_id * topk_weights_stride_m
|
||
+ topk_offsets * topk_weights_stride_k,
|
||
mask=topk_mask,
|
||
other=0.0,
|
||
)
|
||
tl.store(
|
||
topk_weights_out
|
||
+ token_id * topk_weights_out_stride_m
|
||
+ topk_offsets * topk_weights_out_stride_k,
|
||
weights,
|
||
mask=topk_mask,
|
||
)
|
||
|
||
|
||
def _stage_deepseek_v4_mega_moe_inputs(
|
||
hidden_states: torch.Tensor,
|
||
topk_weights: torch.Tensor,
|
||
topk_ids: torch.Tensor,
|
||
x_fp4: torch.Tensor, # uint8, shape (M, K//2)
|
||
x_sf: torch.Tensor, # int32, shape (M, K//64)
|
||
topk_idx_out: torch.Tensor,
|
||
topk_weights_out: torch.Tensor,
|
||
) -> None:
|
||
num_tokens, hidden_size = hidden_states.shape
|
||
if num_tokens == 0:
|
||
return
|
||
if hidden_size % 128 != 0:
|
||
raise ValueError(
|
||
"DeepSeek V4 MegaMoE input staging requires hidden_size to be "
|
||
"a multiple of 128."
|
||
)
|
||
top_k = topk_ids.shape[1]
|
||
if topk_weights.shape != topk_ids.shape:
|
||
raise ValueError(
|
||
"DeepSeek V4 MegaMoE input staging requires topk_weights and "
|
||
"topk_ids to have the same shape."
|
||
)
|
||
|
||
block_k = 128
|
||
grid = (num_tokens, triton.cdiv(hidden_size, block_k))
|
||
block_topk = triton.next_power_of_2(top_k)
|
||
_deepseek_v4_stage_mega_moe_inputs_kernel[grid](
|
||
hidden_states,
|
||
x_fp4,
|
||
x_sf,
|
||
topk_ids,
|
||
topk_weights,
|
||
topk_idx_out,
|
||
topk_weights_out,
|
||
hidden_states.stride(0),
|
||
hidden_states.stride(1),
|
||
x_fp4.stride(0),
|
||
x_fp4.stride(1),
|
||
x_sf.stride(0),
|
||
x_sf.stride(1),
|
||
topk_ids.stride(0),
|
||
topk_ids.stride(1),
|
||
topk_weights.stride(0),
|
||
topk_weights.stride(1),
|
||
topk_idx_out.stride(0),
|
||
topk_idx_out.stride(1),
|
||
topk_weights_out.stride(0),
|
||
topk_weights_out.stride(1),
|
||
hidden_size,
|
||
top_k,
|
||
BLOCK_K=block_k,
|
||
GROUP_K=16, # NVFP4: group_size=16 (scale_vec::4X)
|
||
BLOCK_TOPK=block_topk,
|
||
num_warps=4,
|
||
)
|
||
|
||
|
||
def make_deepseek_v4_expert_params_mapping(
|
||
num_experts: int,
|
||
) -> list[tuple[str, str, int, str]]:
|
||
return [
|
||
(
|
||
"experts.w13_" if shard_id in ("w1", "w3") else "experts.w2_",
|
||
f"experts.{expert_id}.{weight_name}.",
|
||
expert_id,
|
||
shard_id,
|
||
)
|
||
for expert_id in range(num_experts)
|
||
for shard_id, weight_name in [
|
||
("w1", "w1"),
|
||
("w2", "w2"),
|
||
("w3", "w3"),
|
||
]
|
||
]
|
||
|
||
|
||
class DeepseekV4MegaMoEExperts(nn.Module):
|
||
"""MegaMoE experts for DeepSeek V4 with NVFP4 quantization.
|
||
|
||
Loads NVFP4 expert weights (E2M1 packed uint8 + float8_e4m3fn block scales
|
||
+ float32 global scales) and feeds them natively to the DeepGEMM
|
||
fp8_nvfp4_mega_moe kernel (kind::mxf4nvf4.scale_vec::4X).
|
||
|
||
No conversion to MXFP4. Experts stay NVFP4. The global scale (weight_scale_2)
|
||
is folded into the block scales before kernel consumption.
|
||
"""
|
||
_symm_buffer_cache: dict[tuple[int, int, int, int, int, int, int], object] = {}
|
||
|
||
# NVFP4 E2M1 lookup table (positive values, sign from bit 3)
|
||
E2M1_LUT = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0]
|
||
# MXFP4 E2M1 is the same format
|
||
|
||
def __init__(
|
||
self,
|
||
vllm_config: VllmConfig,
|
||
*,
|
||
num_experts: int,
|
||
num_local_experts: int,
|
||
experts_start_idx: int,
|
||
top_k: int,
|
||
hidden_size: int,
|
||
intermediate_size: int,
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
self.prefix = prefix
|
||
self.num_experts = num_experts
|
||
self.num_local_experts = num_local_experts
|
||
self.experts_start_idx = experts_start_idx
|
||
self.experts_end_idx = experts_start_idx + num_local_experts
|
||
self.top_k = top_k
|
||
self.hidden_size = hidden_size
|
||
self.intermediate_size = intermediate_size
|
||
self.max_num_tokens = vllm_config.scheduler_config.max_num_batched_tokens
|
||
|
||
weight_attrs = {"weight_loader": self.weight_loader}
|
||
|
||
# NVFP4 weights: E2M1 packed as uint8, 2 values per byte
|
||
self.w13_weight = nn.Parameter(
|
||
torch.zeros(
|
||
num_local_experts,
|
||
2 * intermediate_size,
|
||
hidden_size // 2,
|
||
dtype=torch.int8,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
set_weight_attrs(self.w13_weight, weight_attrs)
|
||
|
||
# NVFP4 block scales: float8_e4m3fn, group_size=16
|
||
# Shape: [num_local_experts, 2*intermediate_size, hidden_size // 16]
|
||
self.w13_weight_scale = nn.Parameter(
|
||
torch.zeros(
|
||
num_local_experts,
|
||
2 * intermediate_size,
|
||
hidden_size // 16,
|
||
dtype=torch.float8_e4m3fn,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
set_weight_attrs(self.w13_weight_scale, weight_attrs)
|
||
self.w13_weight_scale.quant_method = "block"
|
||
|
||
# NVFP4 global scales: float32, per-expert
|
||
self.w13_weight_scale_2 = nn.Parameter(
|
||
torch.zeros(num_local_experts, dtype=torch.float32),
|
||
requires_grad=False,
|
||
)
|
||
set_weight_attrs(self.w13_weight_scale_2, weight_attrs)
|
||
|
||
# NVFP4 activation scales: float32, per-expert
|
||
self.w13_input_scale = nn.Parameter(
|
||
torch.zeros(num_local_experts, dtype=torch.float32),
|
||
requires_grad=False,
|
||
)
|
||
set_weight_attrs(self.w13_input_scale, weight_attrs)
|
||
|
||
self.w2_weight = nn.Parameter(
|
||
torch.zeros(
|
||
num_local_experts,
|
||
hidden_size,
|
||
intermediate_size // 2,
|
||
dtype=torch.int8,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
set_weight_attrs(self.w2_weight, weight_attrs)
|
||
|
||
# NVFP4 block scales for w2
|
||
self.w2_weight_scale = nn.Parameter(
|
||
torch.zeros(
|
||
num_local_experts,
|
||
hidden_size,
|
||
intermediate_size // 16,
|
||
dtype=torch.float8_e4m3fn,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
set_weight_attrs(self.w2_weight_scale, weight_attrs)
|
||
self.w2_weight_scale.quant_method = "block"
|
||
|
||
self.w2_weight_scale_2 = nn.Parameter(
|
||
torch.zeros(num_local_experts, dtype=torch.float32),
|
||
requires_grad=False,
|
||
)
|
||
set_weight_attrs(self.w2_weight_scale_2, weight_attrs)
|
||
|
||
self.w2_input_scale = nn.Parameter(
|
||
torch.zeros(num_local_experts, dtype=torch.float32),
|
||
requires_grad=False,
|
||
)
|
||
set_weight_attrs(self.w2_input_scale, weight_attrs)
|
||
|
||
self._transformed_l1_weights: tuple[torch.Tensor, torch.Tensor] | None = None
|
||
self._transformed_l2_weights: tuple[torch.Tensor, torch.Tensor] | None = None
|
||
|
||
# Register in the static forward context so the custom-op wrapper
|
||
# can look up this module by name from within a torch.compile graph.
|
||
compilation_config = vllm_config.compilation_config
|
||
if prefix in compilation_config.static_forward_context:
|
||
raise ValueError(f"Duplicate layer name: {prefix}")
|
||
compilation_config.static_forward_context[prefix] = self
|
||
|
||
def _map_global_expert_id(self, expert_id: int) -> int:
|
||
if expert_id < self.experts_start_idx or expert_id >= self.experts_end_idx:
|
||
return -1
|
||
return expert_id - self.experts_start_idx
|
||
|
||
def weight_loader(
|
||
self,
|
||
param: nn.Parameter,
|
||
loaded_weight: torch.Tensor,
|
||
weight_name: str,
|
||
shard_id: str,
|
||
expert_id: int,
|
||
) -> bool:
|
||
local_expert_id = self._map_global_expert_id(expert_id)
|
||
if local_expert_id == -1:
|
||
return False
|
||
|
||
# Scalar params (weight_scale_2, input_scale): 1D per-expert
|
||
if "weight_scale_2" in weight_name or "input_scale" in weight_name:
|
||
param.data[local_expert_id].copy_(loaded_weight)
|
||
return True
|
||
|
||
expert_data = param.data[local_expert_id]
|
||
if shard_id in ("w1", "w3"):
|
||
if "w13_" not in weight_name:
|
||
return False
|
||
shard_offset = 0 if shard_id == "w1" else self.intermediate_size
|
||
expert_data = expert_data.narrow(0, shard_offset, self.intermediate_size)
|
||
elif shard_id == "w2":
|
||
if "w2_" not in weight_name:
|
||
return False
|
||
else:
|
||
raise ValueError(f"Unsupported expert shard id: {shard_id}")
|
||
|
||
if expert_data.shape != loaded_weight.shape:
|
||
raise ValueError(
|
||
f"DeepSeek V4 MegaMoE expert weight shape mismatch for "
|
||
f"{weight_name}: parameter shard {tuple(expert_data.shape)} "
|
||
f"vs checkpoint {tuple(loaded_weight.shape)}"
|
||
)
|
||
expert_data.copy_(loaded_weight)
|
||
return True
|
||
|
||
def _check_runtime_supported(self) -> None:
|
||
if not torch.cuda.is_available():
|
||
raise NotImplementedError("DeepSeek V4 MegaMoE requires CUDA.")
|
||
device = self.w13_weight.device
|
||
if device.type != "cuda":
|
||
raise NotImplementedError(
|
||
"DeepSeek V4 MegaMoE expert weights must be loaded on CUDA."
|
||
)
|
||
if torch.cuda.get_device_capability(device)[0] < 10:
|
||
raise NotImplementedError("DeepGEMM MegaMoE requires SM100 GPUs.")
|
||
if self.hidden_size % 128 != 0 or self.intermediate_size % 128 != 0:
|
||
raise ValueError(
|
||
"DeepGEMM MegaMoE requires hidden and intermediate sizes "
|
||
"to be multiples of 128."
|
||
)
|
||
|
||
def finalize_weights(self) -> None:
|
||
if self._transformed_l1_weights is not None:
|
||
return
|
||
|
||
self._check_runtime_supported()
|
||
from nvfp4_megamoe_kernel import transform_nvfp4_weights_for_mega_moe
|
||
|
||
# === Native NVFP4 path ===
|
||
# The DeepGEMM nvfp4 mega_moe kernel consumes NVFP4 directly:
|
||
# - E2M1 packed uint8 (same as checkpoint)
|
||
# - UE4M3 block scales (float8_e4m3fn), group_size=16
|
||
# - float32 global scale folded into block scales
|
||
# No conversion to MXFP4. Experts stay NVFP4.
|
||
|
||
# Fold global scales into block scales and transform for the kernel
|
||
self._transformed_l1_weights, self._transformed_l2_weights = (
|
||
transform_nvfp4_weights_for_mega_moe(
|
||
(self.w13_weight.data.contiguous(),
|
||
self.w13_weight_scale.data.contiguous()),
|
||
(self.w2_weight.data.contiguous(),
|
||
self.w2_weight_scale.data.contiguous()),
|
||
l1_weight_scale_2=self.w13_weight_scale_2.data.contiguous(),
|
||
l2_weight_scale_2=self.w2_weight_scale_2.data.contiguous(),
|
||
)
|
||
)
|
||
|
||
# Drop the original loader-side parameters
|
||
self.w13_weight = None
|
||
self.w13_weight_scale = None
|
||
self.w13_weight_scale_2 = None
|
||
self.w13_input_scale = None
|
||
self.w2_weight = None
|
||
self.w2_weight_scale = None
|
||
self.w2_weight_scale_2 = None
|
||
self.w2_input_scale = None
|
||
|
||
@staticmethod
|
||
def _ue8m0_to_float32(sf: torch.Tensor) -> torch.Tensor:
|
||
"""Convert NVFP4 block scales (float8_e4m3fn / UE4M3) to float32.
|
||
|
||
Checkpoint stores float8_e4m3fn (standard NVFP4 spec, NOT UE8M0).
|
||
Simple .to(float32) is correct — shift-by-23 was wrong (Bug #7 fix).
|
||
"""
|
||
return sf.to(torch.float32)
|
||
|
||
|
||
|
||
def get_symm_buffer(self):
|
||
import nvfp4_megamoe_kernel as deep_gemm
|
||
from nvfp4_megamoe_kernel import SymmBuffer, get_symm_buffer_for_nvfp4_mega_moe
|
||
|
||
group = get_ep_group().device_group
|
||
device = torch.accelerator.current_device_index()
|
||
key = (
|
||
id(group),
|
||
device,
|
||
self.num_experts,
|
||
self.max_num_tokens,
|
||
self.top_k,
|
||
self.hidden_size,
|
||
self.intermediate_size,
|
||
)
|
||
symm_buffer = self._symm_buffer_cache.get(key)
|
||
if symm_buffer is None:
|
||
# NVFP4 SymmBuffer: 2x SF size due to group_size=16
|
||
symm_buffer = get_symm_buffer_for_nvfp4_mega_moe(
|
||
group,
|
||
self.num_experts,
|
||
self.max_num_tokens,
|
||
self.top_k,
|
||
self.hidden_size,
|
||
self.intermediate_size,
|
||
)
|
||
self._symm_buffer_cache[key] = symm_buffer
|
||
return symm_buffer
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
topk_weights: torch.Tensor,
|
||
topk_ids: torch.Tensor,
|
||
*,
|
||
activation_clamp: float | None,
|
||
fast_math: bool = True,
|
||
) -> torch.Tensor:
|
||
if hidden_states.shape[0] > self.max_num_tokens:
|
||
raise ValueError(
|
||
f"DeepSeek V4 MegaMoE got {hidden_states.shape[0]} tokens, "
|
||
f"but the symmetric buffer was sized for {self.max_num_tokens}."
|
||
)
|
||
y = torch.empty_like(hidden_states, dtype=torch.bfloat16)
|
||
torch.ops.vllm.deepseek_v4_mega_moe_experts(
|
||
hidden_states,
|
||
topk_weights,
|
||
topk_ids,
|
||
y,
|
||
self.prefix,
|
||
activation_clamp,
|
||
fast_math,
|
||
)
|
||
return y
|
||
|
||
def _run_mega_moe(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
topk_weights: torch.Tensor,
|
||
topk_ids: torch.Tensor,
|
||
y: torch.Tensor,
|
||
activation_clamp: float | None,
|
||
fast_math: bool,
|
||
) -> None:
|
||
import nvfp4_megamoe_kernel as deep_gemm
|
||
|
||
symm_buffer = self.get_symm_buffer()
|
||
num_tokens = hidden_states.shape[0]
|
||
_stage_deepseek_v4_mega_moe_inputs(
|
||
hidden_states,
|
||
topk_weights,
|
||
topk_ids,
|
||
symm_buffer.x[:num_tokens],
|
||
symm_buffer.x_sf[:num_tokens],
|
||
symm_buffer.topk_idx[:num_tokens],
|
||
symm_buffer.topk_weights[:num_tokens],
|
||
)
|
||
|
||
# Debug: check staging output
|
||
import os
|
||
if int(os.environ.get('MEGA_MOE_DEBUG', '0')):
|
||
print(f"[MEGA_MOE_DEBUG] After staging: x dtype={symm_buffer.x.dtype} shape={symm_buffer.x.shape}")
|
||
print(f"[MEGA_MOE_DEBUG] x_sf dtype={symm_buffer.x_sf.dtype} shape={symm_buffer.x_sf.shape}")
|
||
print(f"[MEGA_MOE_DEBUG] topk_idx dtype={symm_buffer.topk_idx.dtype} shape={symm_buffer.topk_idx.shape}")
|
||
print(f"[MEGA_MOE_DEBUG] topk_weights dtype={symm_buffer.topk_weights.dtype} shape={symm_buffer.topk_weights.shape}")
|
||
# Check for NaN/Inf in the staging output
|
||
x_sample = symm_buffer.x[:num_tokens]
|
||
sf_sample = symm_buffer.x_sf[:num_tokens]
|
||
print(f"[MEGA_MOE_DEBUG] x range: min={x_sample.min().item()} max={x_sample.max().item()}")
|
||
if sf_sample.numel() > 0:
|
||
print(f"[MEGA_MOE_DEBUG] x_sf range: min={sf_sample.to(torch.float32).min().item()} max={sf_sample.to(torch.float32).max().item}")
|
||
topk_sample = symm_buffer.topk_idx[:num_tokens]
|
||
print(f"[MEGA_MOE_DEBUG] topk_idx range: min={topk_sample.min().item()} max={topk_sample.max().item()}")
|
||
torch.cuda.synchronize()
|
||
print("[MEGA_MOE_DEBUG] Staging CUDA sync OK")
|
||
|
||
# This method must have been already called during the weight loading phase.
|
||
# We call it again here to cover the dummy weight loading case.
|
||
self.finalize_weights()
|
||
|
||
assert self._transformed_l1_weights is not None
|
||
assert self._transformed_l2_weights is not None
|
||
from nvfp4_megamoe_kernel import nvfp4_mega_moe_full as fp8_nvfp4_mega_moe
|
||
|
||
# Debug: dump shapes before mega_moe
|
||
import os
|
||
if int(os.environ.get('MEGA_MOE_DEBUG', '0')):
|
||
l1_w, l1_sf = self._transformed_l1_weights
|
||
l2_w, l2_sf = self._transformed_l2_weights
|
||
print(f"[MEGA_MOE_DEBUG] num_tokens={num_tokens}, hidden={hidden_states.shape[1]}")
|
||
print(f"[MEGA_MOE_DEBUG] l1_w: dtype={l1_w.dtype} shape={l1_w.shape} stride={l1_w.stride()}")
|
||
print(f"[MEGA_MOE_DEBUG] l1_sf: dtype={l1_sf.dtype} shape={l1_sf.shape} stride={l1_sf.stride()}")
|
||
print(f"[MEGA_MOE_DEBUG] l2_w: dtype={l2_w.dtype} shape={l2_w.shape} stride={l2_w.stride()}")
|
||
print(f"[MEGA_MOE_DEBUG] l2_sf: dtype={l2_sf.dtype} shape={l2_sf.shape} stride={l2_sf.stride()}")
|
||
print(f"[MEGA_MOE_DEBUG] symm_buffer nbytes={symm_buffer.buffer.nbytes} rank={symm_buffer.group.rank()}")
|
||
print(f"[MEGA_MOE_DEBUG] num_experts={self.num_experts} topk={topk_ids.shape[1]} max_tokens={self.max_num_tokens}")
|
||
print(f"[MEGA_MOE_DEBUG] y: dtype={y.dtype} shape={y.shape}")
|
||
# Force CUDA sync to catch any prior async errors
|
||
torch.cuda.synchronize()
|
||
print("[MEGA_MOE_DEBUG] CUDA sync OK — prior ops clean")
|
||
|
||
# MEGA_MOE_STATIC: skip the kernel entirely, return zeros
|
||
# Tests whether the crash is in the kernel launch or in prior data prep
|
||
if int(os.environ.get('MEGA_MOE_STATIC', '0')):
|
||
print(f"[MEGA_MOE_STATIC] Skipping fp8_nvfp4_mega_moe, returning zeros")
|
||
y.zero_()
|
||
return
|
||
|
||
fp8_nvfp4_mega_moe(
|
||
y,
|
||
self._transformed_l1_weights,
|
||
self._transformed_l2_weights,
|
||
symm_buffer,
|
||
activation_clamp=activation_clamp,
|
||
fast_math=fast_math,
|
||
)
|
||
if os.environ.get('NVFP4_DEBUG_SYNC', '') == '1':
|
||
torch.cuda.synchronize()
|
||
|
||
|
||
DeepseekV4MegaMoEExperts.weight_loader.supports_moe_loading = True # type: ignore[attr-defined]
|
||
|
||
|
||
def _deepseek_v4_mega_moe_experts_op(
|
||
hidden_states: torch.Tensor,
|
||
topk_weights: torch.Tensor,
|
||
topk_ids: torch.Tensor,
|
||
out: torch.Tensor,
|
||
layer_name: str,
|
||
activation_clamp: float | None,
|
||
fast_math: bool,
|
||
) -> None:
|
||
self = get_forward_context().no_compile_layers[layer_name]
|
||
self._run_mega_moe(
|
||
hidden_states,
|
||
topk_weights,
|
||
topk_ids,
|
||
out,
|
||
activation_clamp,
|
||
fast_math,
|
||
)
|
||
|
||
|
||
def _deepseek_v4_mega_moe_experts_op_fake(
|
||
hidden_states: torch.Tensor,
|
||
topk_weights: torch.Tensor,
|
||
topk_ids: torch.Tensor,
|
||
out: torch.Tensor,
|
||
layer_name: str,
|
||
activation_clamp: float | None,
|
||
fast_math: bool,
|
||
) -> None:
|
||
return None
|
||
|
||
|
||
direct_register_custom_op(
|
||
op_name="deepseek_v4_mega_moe_experts",
|
||
op_func=_deepseek_v4_mega_moe_experts_op,
|
||
mutates_args=["out"],
|
||
fake_impl=_deepseek_v4_mega_moe_experts_op_fake,
|
||
)
|
||
|
||
|
||
class DeepseekV4MoE(nn.Module):
|
||
def __init__(
|
||
self,
|
||
vllm_config: VllmConfig,
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
|
||
self.tp_size = get_tensor_model_parallel_world_size()
|
||
config = vllm_config.model_config.hf_config
|
||
quant_config = vllm_config.quant_config
|
||
self.prefix = prefix
|
||
self.use_mega_moe = True # Force mega_moe for NVFP4 pipeline
|
||
if self.use_mega_moe and not vllm_config.parallel_config.enable_expert_parallel:
|
||
raise NotImplementedError(
|
||
"DeepSeek V4 MegaMoE currently requires expert parallel. "
|
||
"Enable it with --enable-expert-parallel, or pick a different "
|
||
"moe backend."
|
||
)
|
||
|
||
self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
|
||
self.hidden_size = config.hidden_size
|
||
|
||
self.n_routed_experts = config.n_routed_experts
|
||
self.n_activated_experts = config.num_experts_per_tok
|
||
self.moe_intermediate_size = config.moe_intermediate_size
|
||
self.swiglu_limit = config.swiglu_limit
|
||
self.renormalize = config.norm_topk_prob
|
||
self.scoring_func = getattr(config, "scoring_func", "sqrtsoftplus")
|
||
if self.use_mega_moe and self.scoring_func != "sqrtsoftplus":
|
||
raise NotImplementedError(
|
||
"DeepSeek V4 MegaMoE currently supports sqrtsoftplus routing only."
|
||
)
|
||
# NVFP4 experts work with mega_moe via NVFP4→MXFP4 conversion in finalize_weights
|
||
|
||
self.gate = GateLinear(
|
||
config.hidden_size,
|
||
config.n_routed_experts,
|
||
out_dtype=torch.float32,
|
||
bias=False,
|
||
prefix=f"{prefix}.gate",
|
||
)
|
||
self.gate.e_score_correction_bias = None
|
||
self.gate.tid2eid = None
|
||
is_hash_moe = extract_layer_index(prefix) < config.num_hash_layers
|
||
self.hash_indices_dtype = torch.int64 if self.use_mega_moe else torch.int32
|
||
|
||
if is_hash_moe:
|
||
# hash MoE doesn't use e_score_correction_bias
|
||
# Use randint instead of empty to avoid garbage values causing
|
||
# invalid memory access in dummy mode (--load-format="dummy")
|
||
self.gate.tid2eid = nn.Parameter(
|
||
torch.randint(
|
||
0,
|
||
config.n_routed_experts,
|
||
(config.vocab_size, config.num_experts_per_tok),
|
||
dtype=self.hash_indices_dtype,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
elif getattr(config, "topk_method", None) == "noaux_tc":
|
||
self.gate.e_score_correction_bias = nn.Parameter(
|
||
torch.empty(config.n_routed_experts, dtype=torch.float32),
|
||
requires_grad=False,
|
||
)
|
||
|
||
if config.n_shared_experts is None:
|
||
self.shared_experts = None
|
||
else:
|
||
intermediate_size = config.moe_intermediate_size * config.n_shared_experts
|
||
|
||
self.shared_experts = DeepseekV4MLP(
|
||
hidden_size=config.hidden_size,
|
||
intermediate_size=intermediate_size,
|
||
hidden_act=config.hidden_act,
|
||
swiglu_limit=self.swiglu_limit,
|
||
quant_config=quant_config,
|
||
reduce_results=self.use_mega_moe,
|
||
prefix=f"{prefix}.shared_experts",
|
||
)
|
||
|
||
if self.use_mega_moe:
|
||
self._init_mega_moe_experts(vllm_config, config, prefix)
|
||
else:
|
||
self._init_fused_moe_experts(config, quant_config, prefix)
|
||
|
||
def _init_mega_moe_experts(
|
||
self,
|
||
vllm_config: VllmConfig,
|
||
config,
|
||
prefix: str,
|
||
) -> None:
|
||
self.ep_group = get_ep_group()
|
||
self.ep_size = self.ep_group.world_size
|
||
self.ep_rank = self.ep_group.rank_in_group
|
||
assert config.n_routed_experts % self.ep_size == 0
|
||
|
||
self.n_local_experts = config.n_routed_experts // self.ep_size
|
||
self.experts_start_idx = self.ep_rank * self.n_local_experts
|
||
self.experts_end_idx = self.experts_start_idx + self.n_local_experts
|
||
|
||
self.experts = DeepseekV4MegaMoEExperts(
|
||
vllm_config,
|
||
num_experts=config.n_routed_experts,
|
||
num_local_experts=self.n_local_experts,
|
||
experts_start_idx=self.experts_start_idx,
|
||
top_k=config.num_experts_per_tok,
|
||
hidden_size=config.hidden_size,
|
||
intermediate_size=config.moe_intermediate_size,
|
||
prefix=f"{prefix}.experts",
|
||
)
|
||
|
||
def _init_fused_moe_experts(
|
||
self,
|
||
config,
|
||
quant_config,
|
||
prefix: str,
|
||
) -> None:
|
||
self.tp_rank = get_tensor_model_parallel_rank()
|
||
assert config.n_routed_experts % self.tp_size == 0
|
||
|
||
self.n_local_experts = config.n_routed_experts // self.tp_size
|
||
self.experts_start_idx = self.tp_rank * self.n_local_experts
|
||
self.experts_end_idx = self.experts_start_idx + self.n_local_experts
|
||
|
||
self.experts = FusedMoE(
|
||
shared_experts=self.shared_experts,
|
||
gate=self.gate,
|
||
num_experts=config.n_routed_experts,
|
||
top_k=config.num_experts_per_tok,
|
||
hidden_size=config.hidden_size,
|
||
intermediate_size=config.moe_intermediate_size,
|
||
renormalize=config.norm_topk_prob,
|
||
quant_config=quant_config,
|
||
prefix=f"{prefix}.experts",
|
||
scoring_func=self.scoring_func,
|
||
routed_scaling_factor=self.routed_scaling_factor,
|
||
e_score_correction_bias=self.gate.e_score_correction_bias,
|
||
hash_indices_table=self.gate.tid2eid,
|
||
swiglu_limit=self.swiglu_limit,
|
||
router_logits_dtype=torch.float32,
|
||
)
|
||
|
||
def forward(
|
||
self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None = None
|
||
) -> torch.Tensor:
|
||
if self.gate.tid2eid is not None and input_ids is None:
|
||
raise ValueError("DeepSeek V4 hash MoE routing requires input_ids.")
|
||
|
||
if not self.use_mega_moe:
|
||
return self._forward_fused_moe(hidden_states, input_ids)
|
||
|
||
org_shape = hidden_states.shape
|
||
router_logits, _ = self.gate(hidden_states)
|
||
topk_weights, topk_ids = fused_topk_bias(
|
||
hidden_states=hidden_states,
|
||
gating_output=router_logits,
|
||
scoring_func=self.scoring_func,
|
||
e_score_correction_bias=self.gate.e_score_correction_bias.data
|
||
if self.gate.e_score_correction_bias is not None
|
||
else None,
|
||
topk=self.n_activated_experts,
|
||
renormalize=self.renormalize,
|
||
indices_type=self.hash_indices_dtype,
|
||
input_tokens=input_ids,
|
||
hash_indices_table=self.gate.tid2eid,
|
||
routed_scaling_factor=self.routed_scaling_factor,
|
||
)
|
||
activation_clamp = (
|
||
float(self.swiglu_limit) if self.swiglu_limit is not None else None
|
||
)
|
||
final_hidden_states = self.experts(
|
||
hidden_states,
|
||
topk_weights,
|
||
topk_ids,
|
||
activation_clamp=activation_clamp,
|
||
)
|
||
|
||
if self.shared_experts is not None:
|
||
shared_output = self.shared_experts(hidden_states)
|
||
final_hidden_states += shared_output
|
||
|
||
return final_hidden_states.view(org_shape)
|
||
|
||
def _forward_fused_moe(
|
||
self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None = None
|
||
) -> torch.Tensor:
|
||
org_shape = hidden_states.shape
|
||
if self.experts.is_internal_router:
|
||
# In this case, the gate/router runs inside the FusedMoE class
|
||
final_hidden_states = self.experts(
|
||
hidden_states=hidden_states,
|
||
router_logits=hidden_states,
|
||
input_ids=input_ids,
|
||
)
|
||
else:
|
||
router_logits, _ = self.gate(hidden_states)
|
||
final_hidden_states = self.experts(
|
||
hidden_states=hidden_states,
|
||
router_logits=router_logits,
|
||
input_ids=input_ids,
|
||
)
|
||
|
||
return final_hidden_states.view(org_shape)
|
||
|
||
def finalize_mega_moe_weights(self) -> None:
|
||
if self.use_mega_moe:
|
||
self.experts.finalize_weights()
|
||
|
||
|
||
class DeepseekV4Attention(nn.Module):
|
||
def __init__(
|
||
self,
|
||
vllm_config: VllmConfig,
|
||
prefix: str,
|
||
topk_indices_buffer: torch.Tensor | None = None,
|
||
aux_stream_list: list[torch.cuda.Stream] | None = None,
|
||
):
|
||
super().__init__()
|
||
config = vllm_config.model_config.hf_config
|
||
quant_config = vllm_config.quant_config
|
||
layer_id = extract_layer_index(prefix)
|
||
|
||
self.layer_id = layer_id
|
||
self.hidden_size = config.hidden_size
|
||
self.n_heads = config.num_attention_heads
|
||
tp_size = get_tensor_model_parallel_world_size()
|
||
assert self.n_heads % tp_size == 0
|
||
|
||
self.n_local_heads = self.n_heads // tp_size
|
||
self.q_lora_rank = config.q_lora_rank
|
||
self.o_lora_rank = config.o_lora_rank
|
||
self.head_dim = config.head_dim
|
||
self.rope_head_dim = config.qk_rope_head_dim
|
||
self.nope_head_dim = self.head_dim - self.rope_head_dim
|
||
self.n_groups = config.o_groups
|
||
self.n_local_groups = self.n_groups // tp_size
|
||
self.window_size = config.sliding_window
|
||
# NOTE(zyongye) Compress ratio can't be 0
|
||
# we do this for because MTP layer is not included
|
||
# in the compress ratio list
|
||
if layer_id < config.num_hidden_layers:
|
||
self.compress_ratio = max(1, config.compress_ratios[layer_id])
|
||
else:
|
||
self.compress_ratio = 1
|
||
self.eps = config.rms_norm_eps
|
||
self.max_position_embeddings = config.max_position_embeddings
|
||
|
||
# Padded to min 64 heads for FlashMLA, initialized to -inf
|
||
# (no sink effect). Weight loading fills the first n_local_heads slots.
|
||
padded_heads = max(self.n_local_heads, 64)
|
||
self.attn_sink = nn.Parameter(
|
||
torch.full((padded_heads,), -float("inf"), dtype=torch.float32),
|
||
requires_grad=False,
|
||
)
|
||
|
||
self.fused_wqa_wkv = MergedColumnParallelLinear(
|
||
self.hidden_size,
|
||
[self.q_lora_rank, self.head_dim],
|
||
bias=False,
|
||
quant_config=quant_config,
|
||
prefix=f"{prefix}.fused_wqa_wkv",
|
||
disable_tp=True, # fused ReplicatedLinear
|
||
)
|
||
self.q_norm = RMSNorm(self.q_lora_rank, self.eps)
|
||
self.wq_b = ColumnParallelLinear(
|
||
self.q_lora_rank,
|
||
self.n_heads * self.head_dim,
|
||
bias=False,
|
||
quant_config=quant_config,
|
||
return_bias=False,
|
||
prefix=f"{prefix}.wq_b",
|
||
)
|
||
|
||
self.kv_norm = RMSNorm(self.head_dim, self.eps)
|
||
self.wo_a = ColumnParallelLinear(
|
||
self.n_heads * self.head_dim // self.n_groups,
|
||
self.n_groups * self.o_lora_rank,
|
||
bias=False,
|
||
quant_config=quant_config,
|
||
return_bias=False,
|
||
prefix=f"{prefix}.wo_a",
|
||
)
|
||
self.wo_a.is_bmm = True
|
||
self.wo_a.bmm_batch_size = self.n_local_groups
|
||
self.wo_b = RowParallelLinear(
|
||
self.n_groups * self.o_lora_rank,
|
||
self.hidden_size,
|
||
bias=False,
|
||
quant_config=quant_config,
|
||
return_bias=False,
|
||
prefix=f"{prefix}.wo_b",
|
||
)
|
||
self.softmax_scale = self.head_dim**-0.5
|
||
self.scale_fmt = config.quantization_config["scale_fmt"]
|
||
|
||
self.rope_parameters = config.rope_scaling
|
||
|
||
# Initialize rotary embedding BEFORE DeepseekV4MLAModules (which needs it)
|
||
rope_parameters = dict(config.rope_parameters)
|
||
rope_parameters["rope_theta"] = (
|
||
config.compress_rope_theta if self.compress_ratio > 1 else config.rope_theta
|
||
)
|
||
if config.rope_parameters["rope_type"] != "default":
|
||
config.rope_parameters["rope_type"] = (
|
||
"deepseek_yarn"
|
||
if config.rope_parameters.get("apply_yarn_scaling", True)
|
||
else "deepseek_llama_scaling"
|
||
)
|
||
rope_parameters["mscale"] = 0 # Disable mscale
|
||
rope_parameters["mscale_all_dim"] = 0 # Disable mscale
|
||
rope_parameters["is_deepseek_v4"] = True
|
||
rope_parameters["rope_dim"] = self.rope_head_dim
|
||
self.rotary_emb = get_rope(
|
||
self.head_dim,
|
||
max_position=self.max_position_embeddings,
|
||
rope_parameters=rope_parameters,
|
||
is_neox_style=False,
|
||
)
|
||
|
||
self.indexer = None
|
||
if self.compress_ratio == 4:
|
||
# Only C4A uses sparse attention and hence has indexer.
|
||
self.indexer = DeepseekV4Indexer(
|
||
vllm_config,
|
||
config=config,
|
||
hidden_size=self.hidden_size,
|
||
q_lora_rank=self.q_lora_rank,
|
||
quant_config=quant_config,
|
||
cache_config=vllm_config.cache_config,
|
||
topk_indices_buffer=topk_indices_buffer,
|
||
compress_ratio=self.compress_ratio,
|
||
prefix=f"{prefix}.indexer",
|
||
)
|
||
|
||
mla_modules = DeepseekV4MLAModules(
|
||
vllm_config=vllm_config,
|
||
fused_wqa_wkv=self.fused_wqa_wkv,
|
||
q_norm=self.q_norm,
|
||
wq_b=self.wq_b,
|
||
kv_norm=self.kv_norm,
|
||
wo_a=self.wo_a,
|
||
wo_b=self.wo_b,
|
||
attn_sink=self.attn_sink,
|
||
rotary_emb=self.rotary_emb,
|
||
indexer=self.indexer,
|
||
indexer_rotary_emb=self.rotary_emb,
|
||
topk_indices_buffer=topk_indices_buffer,
|
||
aux_stream_list=aux_stream_list,
|
||
)
|
||
self.mla_attn = DeepseekV4MultiHeadLatentAttentionWrapper(
|
||
hidden_size=self.hidden_size,
|
||
num_heads=self.n_local_heads,
|
||
head_dim=self.head_dim,
|
||
scale=self.softmax_scale,
|
||
qk_nope_head_dim=self.nope_head_dim,
|
||
qk_rope_head_dim=self.rope_head_dim,
|
||
v_head_dim=self.head_dim,
|
||
q_lora_rank=self.q_lora_rank,
|
||
kv_lora_rank=self.head_dim,
|
||
o_lora_rank=self.o_lora_rank,
|
||
mla_modules=mla_modules,
|
||
window_size=self.window_size,
|
||
compress_ratio=self.compress_ratio,
|
||
cache_config=vllm_config.cache_config,
|
||
quant_config=quant_config,
|
||
prefix=prefix,
|
||
)
|
||
|
||
def forward(
|
||
self,
|
||
positions: torch.Tensor,
|
||
hidden_states: torch.Tensor,
|
||
llama_4_scaling: torch.Tensor | None,
|
||
):
|
||
return self.mla_attn(positions, hidden_states, llama_4_scaling)
|
||
|
||
|
||
class DeepseekV4DecoderLayer(nn.Module):
|
||
def __init__(
|
||
self,
|
||
vllm_config,
|
||
prefix,
|
||
topk_indices_buffer: torch.Tensor | None = None,
|
||
aux_stream_list: list[torch.cuda.Stream] | None = None,
|
||
):
|
||
super().__init__()
|
||
|
||
# Lazy import to avoid top-level tilelang dependency.
|
||
# Registers both torch.ops.vllm.mhc_pre and mhc_post
|
||
import vllm.model_executor.layers.mhc # noqa: F401
|
||
|
||
config = vllm_config.model_config.hf_config
|
||
self.hidden_size = config.hidden_size
|
||
|
||
self.rms_norm_eps = config.rms_norm_eps
|
||
self.attn = DeepseekV4Attention(
|
||
vllm_config,
|
||
prefix=f"{prefix}.attn",
|
||
topk_indices_buffer=topk_indices_buffer,
|
||
aux_stream_list=aux_stream_list,
|
||
)
|
||
self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn")
|
||
|
||
self.attn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
|
||
self.ffn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
|
||
self.hc_mult = config.hc_mult
|
||
self.hc_sinkhorn_iters = config.hc_sinkhorn_iters
|
||
self.hc_eps = config.hc_eps
|
||
self.hc_post_alpha = 2.0
|
||
mix_hc = (2 + self.hc_mult) * self.hc_mult
|
||
hc_dim = self.hc_mult * self.hidden_size
|
||
self.hc_attn_fn = nn.Parameter(
|
||
torch.empty(
|
||
(mix_hc, hc_dim),
|
||
dtype=torch.float32,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
self.hc_ffn_fn = nn.Parameter(
|
||
torch.empty(
|
||
(mix_hc, hc_dim),
|
||
dtype=torch.float32,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
self.hc_attn_base = nn.Parameter(
|
||
torch.empty(
|
||
mix_hc,
|
||
dtype=torch.float32,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
self.hc_ffn_base = nn.Parameter(
|
||
torch.empty(
|
||
mix_hc,
|
||
dtype=torch.float32,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
self.hc_attn_scale = nn.Parameter(
|
||
torch.empty(
|
||
3,
|
||
dtype=torch.float32,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
self.hc_ffn_scale = nn.Parameter(
|
||
torch.empty(
|
||
3,
|
||
dtype=torch.float32,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
|
||
def hc_pre(
|
||
self,
|
||
x: torch.Tensor,
|
||
hc_fn: torch.Tensor,
|
||
hc_scale: torch.Tensor,
|
||
hc_base: torch.Tensor,
|
||
):
|
||
post_mix, res_mix, layer_input = torch.ops.vllm.mhc_pre(
|
||
residual=x,
|
||
fn=hc_fn,
|
||
hc_scale=hc_scale,
|
||
hc_base=hc_base,
|
||
rms_eps=self.rms_norm_eps,
|
||
hc_pre_eps=self.hc_eps,
|
||
hc_sinkhorn_eps=self.hc_eps,
|
||
hc_post_mult_value=self.hc_post_alpha,
|
||
sinkhorn_repeat=self.hc_sinkhorn_iters,
|
||
)
|
||
return layer_input, post_mix, res_mix
|
||
|
||
def hc_post(
|
||
self,
|
||
x: torch.Tensor,
|
||
residual: torch.Tensor,
|
||
post: torch.Tensor,
|
||
comb: torch.Tensor,
|
||
):
|
||
return torch.ops.vllm.mhc_post(x, residual, post, comb)
|
||
|
||
def forward(
|
||
self,
|
||
x: torch.Tensor,
|
||
positions: torch.Tensor,
|
||
input_ids: torch.Tensor | None,
|
||
) -> torch.Tensor:
|
||
# DEBUG: skip attention entirely, just run FFN on raw input
|
||
if int(os.environ.get('SKIP_ATTENTION', '0')):
|
||
# Flatten to 2D for ffn, then restore
|
||
org_shape = x.shape
|
||
x_2d = x.view(-1, x.shape[-1])
|
||
x_2d = self.ffn_norm(x_2d)
|
||
x_2d = self.ffn(x_2d, input_ids)
|
||
return x_2d.view(org_shape)
|
||
|
||
residual = x
|
||
x, post, comb = self.hc_pre(
|
||
x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base
|
||
)
|
||
x = self.attn_norm(x)
|
||
x = self.attn(positions, x, None)
|
||
x = self.hc_post(x, residual, post, comb)
|
||
|
||
residual = x
|
||
x, post, comb = self.hc_pre(
|
||
x, self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base
|
||
)
|
||
x = self.ffn_norm(x)
|
||
x = self.ffn(x, input_ids)
|
||
x = self.hc_post(x, residual, post, comb)
|
||
return x
|
||
|
||
|
||
@support_torch_compile
|
||
class DeepseekV4Model(nn.Module):
|
||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||
super().__init__()
|
||
|
||
config = vllm_config.model_config.hf_config
|
||
quant_config = vllm_config.quant_config
|
||
self.config = config
|
||
self.use_mega_moe = True # Force mega_moe for NVFP4 pipeline
|
||
if self.use_mega_moe and not vllm_config.parallel_config.enable_expert_parallel:
|
||
raise NotImplementedError(
|
||
"DeepSeek V4 MegaMoE currently requires expert parallel. "
|
||
"Enable it with --enable-expert-parallel, or pick a different "
|
||
"moe backend."
|
||
)
|
||
self.vocab_size = config.vocab_size
|
||
self.hc_eps = config.hc_eps
|
||
self.hc_mult = config.hc_mult
|
||
self.hc_dim = self.hc_mult * config.hidden_size
|
||
self.rms_norm_eps = config.rms_norm_eps
|
||
|
||
# Three aux streams: one per non-default input GEMM in
|
||
# DeepseekV4MultiHeadLatentAttentionWrapper.attn_gemm_parallel_execute
|
||
# (compressor kv_score, indexer.weights_proj, indexer.compressor
|
||
# kv_score). fused_wqa_wkv stays on the default stream.
|
||
aux_stream_list = [torch.cuda.Stream() for _ in range(3)]
|
||
|
||
self.device = current_platform.device_type
|
||
# Reserved topk indices buffer for all Indexer layers to reuse.
|
||
self.topk_indices_buffer = torch.empty(
|
||
vllm_config.scheduler_config.max_num_batched_tokens,
|
||
config.index_topk,
|
||
dtype=torch.int32,
|
||
device=self.device,
|
||
)
|
||
|
||
self.embed_tokens = VocabParallelEmbedding(
|
||
config.vocab_size,
|
||
config.hidden_size,
|
||
quant_config=quant_config,
|
||
prefix=f"{prefix}.embed_tokens",
|
||
)
|
||
|
||
self.start_layer, self.end_layer, self.layers = make_layers(
|
||
config.num_hidden_layers,
|
||
lambda prefix: DeepseekV4DecoderLayer(
|
||
vllm_config,
|
||
prefix=prefix,
|
||
topk_indices_buffer=self.topk_indices_buffer,
|
||
aux_stream_list=aux_stream_list,
|
||
),
|
||
prefix=f"{prefix}.layers",
|
||
)
|
||
|
||
self.norm = RMSNorm(config.hidden_size, self.rms_norm_eps)
|
||
|
||
self.hc_head_fn = nn.Parameter(
|
||
torch.empty(
|
||
self.hc_mult,
|
||
self.hc_dim,
|
||
dtype=torch.float32,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
self.hc_head_base = nn.Parameter(
|
||
torch.empty(
|
||
self.hc_mult,
|
||
dtype=torch.float32,
|
||
),
|
||
requires_grad=False,
|
||
)
|
||
self.hc_head_scale = nn.Parameter(
|
||
torch.empty(1, dtype=torch.float32),
|
||
requires_grad=False,
|
||
)
|
||
|
||
# Pre-hc_head residual stream buffer for the MTP draft. Stable
|
||
# address (outside the cudagraph pool) so the copy_ in forward()
|
||
# refreshes it correctly across captured shapes.
|
||
self._mtp_hidden_buffer = torch.empty(
|
||
vllm_config.scheduler_config.max_num_batched_tokens,
|
||
self.hc_dim,
|
||
dtype=vllm_config.model_config.dtype,
|
||
device=self.device,
|
||
)
|
||
|
||
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,
|
||
intermediate_tensors: IntermediateTensors | None,
|
||
inputs_embeds: torch.Tensor | None = None,
|
||
) -> torch.Tensor | IntermediateTensors:
|
||
hidden_states = self.embed_input_ids(input_ids)
|
||
hidden_states = hidden_states.unsqueeze(-2).repeat(1, self.hc_mult, 1)
|
||
if self.use_mega_moe:
|
||
input_ids = input_ids.to(torch.int64)
|
||
for layer in islice(self.layers, self.start_layer, self.end_layer):
|
||
hidden_states = layer(
|
||
hidden_states,
|
||
positions,
|
||
input_ids,
|
||
)
|
||
|
||
# Stash pre-hc_head residual for the MTP draft (captured copy_).
|
||
num_tokens = hidden_states.shape[0]
|
||
self._mtp_hidden_buffer[:num_tokens].copy_(hidden_states.flatten(1))
|
||
|
||
hidden_states = hc_head(
|
||
hidden_states,
|
||
self.hc_head_fn,
|
||
self.hc_head_scale,
|
||
self.hc_head_base,
|
||
self.rms_norm_eps,
|
||
self.hc_eps,
|
||
)
|
||
hidden_states = self.norm(hidden_states)
|
||
return hidden_states
|
||
|
||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||
stacked_params_mapping = [
|
||
# (param_name, shard_name, shard_id)
|
||
("gate_up_proj", "w1", 0),
|
||
("gate_up_proj", "w3", 1),
|
||
("attn.fused_wqa_wkv", "attn.wq_a", 0),
|
||
("attn.fused_wqa_wkv", "attn.wkv", 1),
|
||
("compressor.fused_wkv_wgate", "compressor.wkv", 0),
|
||
("compressor.fused_wkv_wgate", "compressor.wgate", 1),
|
||
]
|
||
params_dict = dict(self.named_parameters())
|
||
loaded_params: set[str] = set()
|
||
|
||
# TP for attention
|
||
tp_size = get_tensor_model_parallel_world_size()
|
||
tp_rank = get_tensor_model_parallel_rank()
|
||
n_head = self.config.num_attention_heads
|
||
n_local_head = n_head // tp_size
|
||
head_rank_start = n_local_head * tp_rank
|
||
head_rank_end = n_local_head * (tp_rank + 1)
|
||
|
||
# Pre-compute expert mapping ONCE.
|
||
expert_mapping = self.get_expert_mapping()
|
||
|
||
for name, loaded_weight in weights:
|
||
for param_name, weight_name, shard_id in stacked_params_mapping:
|
||
# Skip non-stacked layers and experts (experts handled below).
|
||
if ".experts." in name:
|
||
continue
|
||
if weight_name not in name:
|
||
continue
|
||
name = name.replace(weight_name, param_name)
|
||
|
||
param = params_dict[name]
|
||
weight_loader = param.weight_loader
|
||
|
||
# ModelOpt NVFP4 packed weight fix for MergedColumnParallelLinear.
|
||
#
|
||
# modelopt exports NVFP4 packed weights as uint8 (2 values/byte
|
||
# along the column dim). But MergedColumnParallelLinear creates
|
||
# the weight param as bfloat16 (ModelWeightParameter), because
|
||
# ModelOptNvFp4Config only patches Linear, not
|
||
# MergedColumnParallelLinear.
|
||
#
|
||
# When loading uint8 packed weights into a bf16 param, we need to
|
||
# unpack them. Each uint8 byte contains 2 E2M1 FP4 values.
|
||
# We unpack using the LUT and return bf16.
|
||
#
|
||
# The weight_scale is loaded separately and process_weights_after_loading
|
||
# will handle the actual NVFP4 quantization.
|
||
if (loaded_weight.dtype == torch.uint8
|
||
and param.data.dtype != torch.uint8
|
||
and loaded_weight.shape[-1] * 2 == param.data.shape[-1]):
|
||
# Unpack NVFP4 (E2M1) → BF16
|
||
# E2M1 LUT: 0→0, 1→0.5, 2→1, 3→1.5, 4→2, 5→3, 6→4, 7→6
|
||
# Sign bit in bit 3 (indices 8-15 are negatives)
|
||
FP4_LUT = torch.tensor([
|
||
0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0,
|
||
-0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0,
|
||
], dtype=torch.float32, device=loaded_weight.device)
|
||
lower = FP4_LUT[(loaded_weight & 0x0F).long()] # (..., in_packed, )
|
||
upper = FP4_LUT[((loaded_weight >> 4) & 0x0F).long()]
|
||
# Interleave: [lower_0, upper_0, lower_1, upper_1, ...]
|
||
out = torch.empty(
|
||
*loaded_weight.shape[:-1], loaded_weight.shape[-1] * 2,
|
||
dtype=torch.float32, device=loaded_weight.device,
|
||
)
|
||
out[..., 0::2] = lower
|
||
out[..., 1::2] = upper
|
||
loaded_weight = out.to(torch.bfloat16)
|
||
|
||
try:
|
||
weight_loader(param, loaded_weight, shard_id)
|
||
except (AssertionError, ValueError, RuntimeError) as e:
|
||
print(f'[DEBUG-STACK] FAILED: name={name} shard_id={shard_id} '
|
||
f'param.shape={param.shape} param.dtype={param.data.dtype} '
|
||
f'loaded.shape={loaded_weight.shape} loaded.dtype={loaded_weight.dtype} err={e}')
|
||
raise
|
||
loaded_params.add(name)
|
||
break
|
||
else:
|
||
if ".experts." in name:
|
||
# E8M0 scales are stored as float8_e8m0fnu in
|
||
# MXFP4 checkpoints but NVFP4 uses float8_e4m3fn.
|
||
# The uint8 view+copy path is only valid for MXFP4;
|
||
# for NVFP4 it would paste raw E8M0 bytes into an
|
||
# E4M3 buffer, producing garbage.
|
||
if (
|
||
"weight_scale" in name
|
||
and loaded_weight.dtype == torch.float8_e8m0fnu
|
||
):
|
||
assert False, (
|
||
f"E8M0 weight_scale encountered for NVFP4 experts "
|
||
f"({name}) — this is only valid for MXFP4. "
|
||
f"Check checkpoint dtype."
|
||
)
|
||
for mapping in expert_mapping:
|
||
param_name, weight_name, expert_id, shard_id = mapping
|
||
if weight_name not in name:
|
||
continue
|
||
name_mapped = name.replace(weight_name, param_name)
|
||
if name_mapped not in params_dict:
|
||
continue
|
||
param = params_dict[name_mapped]
|
||
# We should ask the weight loader to return success or not
|
||
# here since otherwise we may skip experts with other
|
||
# available replicas.
|
||
weight_loader = typing.cast(
|
||
Callable[..., bool], param.weight_loader
|
||
)
|
||
success = weight_loader(
|
||
param,
|
||
loaded_weight,
|
||
name_mapped,
|
||
shard_id=shard_id,
|
||
expert_id=expert_id,
|
||
)
|
||
if success:
|
||
name = name_mapped
|
||
break
|
||
loaded_params.add(name_mapped)
|
||
continue
|
||
elif "attn_sink" in name:
|
||
narrow_weight = loaded_weight[head_rank_start:head_rank_end]
|
||
n = narrow_weight.shape[0]
|
||
params_dict[name][:n].copy_(narrow_weight)
|
||
loaded_params.add(name)
|
||
continue
|
||
else:
|
||
if name not in params_dict:
|
||
# ModelOpt NVFP4 export includes params not in the
|
||
# vllm model (e.g., compressor.position_bias).
|
||
# Skip them silently.
|
||
continue
|
||
param = params_dict[name]
|
||
|
||
# Handle bf16 → uint8 mismatch for o_a_proj:
|
||
# modelopt didn't quantize o_a_proj (bf16, no scales),
|
||
# but ModelOptNvFp4Config creates wo_a with NVFP4 quant
|
||
# (uint8 weight + scales). We quantize the bf16 weight
|
||
# to NVFP4 at load time so the layer runs in NVFP4 path.
|
||
if (name.endswith(".weight")
|
||
and loaded_weight.dtype != torch.uint8
|
||
and param.data.dtype == torch.uint8):
|
||
# Quantize bf16 → NVFP4 (E2M1 packed uint8 + scales)
|
||
w_bf16 = loaded_weight
|
||
out_dim, in_dim = w_bf16.shape
|
||
block_size = 16
|
||
assert in_dim % block_size == 0
|
||
n_blocks = in_dim // block_size
|
||
|
||
# Reshape into blocks
|
||
w_blocks = w_bf16.reshape(out_dim, n_blocks, block_size)
|
||
|
||
# Compute per-block amax
|
||
amax = w_blocks.abs().amax(dim=-1) # [out, n_blocks]
|
||
|
||
# Global scale (weight_scale_2): max amax / (6.0 * 448.0)
|
||
global_amax = amax.max()
|
||
# Use 448.0 as the max e4m3 value for scale computation
|
||
weight_scale_2_val = global_amax / (6.0 * 448.0)
|
||
weight_scale_2 = weight_scale_2_val.to(torch.float32)
|
||
|
||
# Per-block scale (weight_scale): UE4M3 format (standard NVFP4)
|
||
# block_scale = amax / (6.0 * weight_scale_2)
|
||
block_scale = amax / (6.0 * weight_scale_2_val)
|
||
weight_scale = block_scale.clamp(0.0, 448.0).to(torch.float8_e4m3fn)
|
||
|
||
# Quantize to FP4 (E2M1)
|
||
# E2M1 LUT: 0, 0.5, 1, 1.5, 2, 3, 4, 6 (positive)
|
||
FP4_POS = torch.tensor(
|
||
[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0],
|
||
dtype=torch.float32, device=w_bf16.device,
|
||
)
|
||
# Scale the weight values: normalized = w / (block_scale * weight_scale_2)
|
||
block_scale_f32 = block_scale.clamp(0.0, 448.0)
|
||
scaled = w_blocks / (block_scale_f32.unsqueeze(-1) * weight_scale_2_val)
|
||
# Find nearest FP4 index (0-7 for magnitude)
|
||
# Use absolute value for matching, then apply sign
|
||
scaled_abs = scaled.abs()
|
||
# Find closest FP4 value
|
||
diff = (scaled_abs.unsqueeze(-1) - FP4_POS).abs()
|
||
fp4_idx = diff.argmin(dim=-1) # [out, n_blocks, block_size]
|
||
# Apply sign: negative values get bit 3 set
|
||
sign = (scaled < 0).int()
|
||
fp4_val = (sign << 3) | fp4_idx.int()
|
||
# Pack: 2 FP4 values per uint8 byte
|
||
# Even positions → lower nibble, Odd → upper nibble
|
||
fp4_flat = fp4_val.reshape(out_dim, -1) # [out, in_dim]
|
||
assert fp4_flat.shape[1] % 2 == 0
|
||
even = fp4_flat[:, 0::2] # lower nibble
|
||
odd = fp4_flat[:, 1::2] # upper nibble
|
||
packed = (odd << 4) | even
|
||
weight_packed = packed.to(torch.uint8).view(torch.int8)
|
||
|
||
# Reshape weight_scale to [out, n_blocks]
|
||
weight_scale_2d = weight_scale.reshape(out_dim, n_blocks)
|
||
|
||
# Load the quantized weight into the uint8 param
|
||
weight_loader = param.weight_loader
|
||
weight_loader(param, weight_packed)
|
||
loaded_params.add(name)
|
||
|
||
# Load scales into sibling params
|
||
base = name.rsplit(".", 1)[0]
|
||
# weight_scale
|
||
ws_name = f"{base}.weight_scale"
|
||
if ws_name in params_dict:
|
||
ws_param = params_dict[ws_name]
|
||
ws_loader = getattr(ws_param, "weight_loader", default_weight_loader)
|
||
ws_loader(ws_param, weight_scale_2d)
|
||
loaded_params.add(ws_name)
|
||
# weight_scale_2
|
||
ws2_name = f"{base}.weight_scale_2"
|
||
if ws2_name in params_dict:
|
||
ws2_param = params_dict[ws2_name]
|
||
ws2_loader = getattr(ws2_param, "weight_loader", default_weight_loader)
|
||
ws2_loader(ws2_param, weight_scale_2.reshape(1))
|
||
loaded_params.add(ws2_name)
|
||
# input_scale: use 1.0 default (dynamic quant)
|
||
is_name = f"{base}.input_scale"
|
||
if is_name in params_dict:
|
||
is_param = params_dict[is_name]
|
||
is_loader = getattr(is_param, "weight_loader", default_weight_loader)
|
||
is_loader(is_param, torch.tensor(1.0, dtype=torch.float32))
|
||
loaded_params.add(is_name)
|
||
continue
|
||
|
||
weight_loader = getattr(
|
||
param, "weight_loader", default_weight_loader
|
||
)
|
||
weight_loader(param, loaded_weight)
|
||
loaded_params.add(name)
|
||
continue
|
||
|
||
return loaded_params
|
||
|
||
def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
|
||
first_layer = next(iter(islice(self.layers, self.start_layer, self.end_layer)))
|
||
if first_layer.ffn.use_mega_moe:
|
||
return make_deepseek_v4_expert_params_mapping(self.config.n_routed_experts)
|
||
# Params for weights, fp8 weight scales, fp8 activation scales
|
||
# (param_name, weight_name, expert_id, shard_id)
|
||
return FusedMoE.make_expert_params_mapping(
|
||
self,
|
||
ckpt_gate_proj_name="w1",
|
||
ckpt_down_proj_name="w2",
|
||
ckpt_up_proj_name="w3",
|
||
num_experts=self.config.n_routed_experts,
|
||
)
|
||
|
||
def finalize_mega_moe_weights(self) -> None:
|
||
for layer in islice(self.layers, self.start_layer, self.end_layer):
|
||
layer.ffn.finalize_mega_moe_weights()
|
||
|
||
def _convert_nvfp4_post_load(self):
|
||
"""Post-load conversion of NVFP4 weights for vLLM compatibility.
|
||
|
||
Strategy:
|
||
- wo_a: Convert to FP8 (attention forward reads weight/weight_scale_inv
|
||
directly and passes to deepseek_v4_fp8_einsum, bypassing quant_method)
|
||
- fused_wqa_wkv, wq_b, wo_b: Dequant NVFP4->bf16 (called via
|
||
.forward() which goes through quant_method; FP8 would dtype-mismatch)
|
||
- compressor.fused_wkv_wgate: Dequant NVFP4->bf16 (used via direct
|
||
torch.mm in attention parallel stream)
|
||
- shared_experts (gate_up_proj, down_proj): Stay native NVFP4 via DeepGEMM fp8_fp4_gemm
|
||
- MoE experts: Handled by DeepseekV4MegaMoEExperts (NVFP4→MXFP4)
|
||
"""
|
||
E2M1_LUT = torch.tensor(
|
||
[0, 0.5, 1, 1.5, 2, 3, 4, 6], dtype=torch.bfloat16
|
||
)
|
||
FP8_MAX = torch.finfo(torch.float8_e4m3fn).max
|
||
|
||
# wo_a: attention forward reads .weight and .weight_scale_inv directly
|
||
# for fp8_einsum. Only layer that needs FP8 conversion.
|
||
fp8_proj_names = {"wo_a"}
|
||
# Attention layers called via .forward() — need bf16
|
||
# cuBLAS BF16 is broken on Blackwell — nothing gets dequantized to BF16.
|
||
# Everything stays native NVFP4/FP8 via FlashInfer CUTLASS.
|
||
bf16_proj_names = set()
|
||
bf16_shared_names = set()
|
||
|
||
fp8_converted = 0
|
||
fp8_from_bf16 = 0
|
||
bf16_converted = 0
|
||
compressor_converted = 0
|
||
|
||
# Build shard index once for compressor reconstruction (avoids N×M full-shard loads)
|
||
_shard_index = self._build_shard_index("/model") if os.path.isdir("/model") else None
|
||
|
||
for layer_idx, layer in enumerate(self.layers):
|
||
attn = layer.attn
|
||
|
||
# FP8 conversion: only wo_a
|
||
for proj_name in fp8_proj_names:
|
||
if not hasattr(attn, proj_name):
|
||
continue
|
||
mod = getattr(attn, proj_name)
|
||
if not hasattr(mod, "weight"):
|
||
continue
|
||
if mod.weight.dtype in (torch.uint8, torch.int8):
|
||
# NVFP4 -> dequant to bf16 -> requant to FP8
|
||
self._convert_nvfp4_to_fp8(mod, E2M1_LUT, FP8_MAX)
|
||
fp8_converted += 1
|
||
elif mod.weight.dtype == torch.bfloat16:
|
||
self._convert_bf16_to_fp8(mod, FP8_MAX)
|
||
fp8_from_bf16 += 1
|
||
|
||
# BF16 conversion: attention layers via .forward()
|
||
for proj_name in bf16_proj_names:
|
||
if not hasattr(attn, proj_name):
|
||
continue
|
||
mod = getattr(attn, proj_name)
|
||
if not hasattr(mod, "weight") or mod.weight.dtype not in (torch.uint8, torch.int8):
|
||
continue
|
||
self._dequant_nvfp4_to_bf16(mod, E2M1_LUT)
|
||
bf16_converted += 1
|
||
|
||
# Compressor: fused_wkv_wgate used via direct torch.mm
|
||
# Compressor weights were SKIPPED during loading (skip patterns)
|
||
# because the stacking weight_loader corrupts NVFP4 uint8 data.
|
||
# We reconstruct the bf16 weight from the individual sub-weights
|
||
# that were loaded separately before stacking.
|
||
# Note: compressor.kv_proj.weight and compressor.gate_proj.weight
|
||
# are skipped, so fused_wkv_wgate.weight is zeros (empty tensor).
|
||
# We need to manually create it.
|
||
mla_attn = getattr(attn, "mla_attn", None)
|
||
if mla_attn is not None:
|
||
compressor = getattr(mla_attn, "compressor", None)
|
||
if compressor is not None and hasattr(compressor, "fused_wkv_wgate"):
|
||
compressor_converted += self._reconstruct_compressor_weight(
|
||
compressor.fused_wkv_wgate, attn, layer_idx, E2M1_LUT, _shard_index=_shard_index)
|
||
# Indexer compressor (C4A layers only)
|
||
indexer = getattr(mla_attn, "indexer", None)
|
||
if indexer is not None:
|
||
idx_compressor = getattr(indexer, "compressor", None)
|
||
if idx_compressor is not None and hasattr(idx_compressor, "fused_wkv_wgate"):
|
||
compressor_converted += self._reconstruct_compressor_weight(
|
||
idx_compressor.fused_wkv_wgate, indexer, layer_idx, E2M1_LUT, sub_path=".indexer", _shard_index=_shard_index)
|
||
|
||
# Shared experts: dequantize NVFP4 → BF16
|
||
ffn = layer.ffn
|
||
if hasattr(ffn, "shared_experts") and ffn.shared_experts is not None:
|
||
for proj_name in bf16_shared_names:
|
||
if not hasattr(ffn.shared_experts, proj_name):
|
||
continue
|
||
mod = getattr(ffn.shared_experts, proj_name)
|
||
if not hasattr(mod, "weight") or mod.weight.dtype not in (torch.uint8, torch.int8):
|
||
continue
|
||
self._dequant_nvfp4_to_bf16(mod, E2M1_LUT)
|
||
bf16_converted += 1
|
||
|
||
total_fp8 = fp8_converted + fp8_from_bf16
|
||
total_bf16 = bf16_converted + compressor_converted
|
||
if total_fp8 > 0 or total_bf16 > 0:
|
||
print(f"NVFP4 post-load: {fp8_converted} NVFP4->FP8, "
|
||
f"{fp8_from_bf16} BF16->FP8, "
|
||
f"{bf16_converted} attn/shared->BF16, "
|
||
f"{compressor_converted} compressor->BF16")
|
||
|
||
|
||
def _dequant_nvfp4_to_bf16(self, mod, e2m1_lut):
|
||
"""Dequantize NVFP4 weight to bf16 for normal .forward() path."""
|
||
w_uint8 = mod.weight.data
|
||
device = w_uint8.device
|
||
w_bf16 = self._unpack_nvfp4_to_bf16(w_uint8, e2m1_lut, device)
|
||
|
||
# Dequantize with scales
|
||
if hasattr(mod, "weight_scale") and hasattr(mod, "weight_scale_2"):
|
||
# NVFP4 block scales are float8_e4m3fn (UE4M3) — standard spec.
|
||
# .to(float32) is correct (Bug #7: shift-by-23 was wrong, reverted)
|
||
block_scale = self._ue8m0_to_float32(mod.weight_scale.data)
|
||
if block_scale.dim() == 2 and w_bf16.dim() == 2:
|
||
block_size = w_bf16.shape[1] // block_scale.shape[1]
|
||
block_scale_expanded = block_scale.unsqueeze(-1).expand(
|
||
-1, -1, block_size
|
||
).reshape(w_bf16.shape)
|
||
else:
|
||
block_scale_expanded = block_scale
|
||
global_scale = mod.weight_scale_2.data.max().item()
|
||
input_scale = (
|
||
mod.input_scale.data.max().item()
|
||
if hasattr(mod, "input_scale")
|
||
else 1.0
|
||
)
|
||
# NOTE: input_scale is for ACTIVATIONS, not weights.
|
||
# Weight dequant = e2m1 * block_scale * global_scale (NO input_scale)
|
||
w_dequant = w_bf16.float() * block_scale_expanded * global_scale
|
||
w_dequant = w_dequant.to(torch.bfloat16)
|
||
else:
|
||
w_dequant = w_bf16
|
||
|
||
# Free source tensors eagerly to avoid holding uint8+bf16+fp32 simultaneously
|
||
del w_uint8, w_bf16
|
||
mod.weight = torch.nn.Parameter(w_dequant, requires_grad=False)
|
||
del w_dequant
|
||
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
|
||
mod.quant_method = UnquantizedLinearMethod()
|
||
for attr in ("weight_scale", "weight_scale_2", "input_scale",
|
||
"weight_scale_inv"):
|
||
if hasattr(mod, attr):
|
||
delattr(mod, attr)
|
||
|
||
def _convert_nvfp4_to_fp8(self, mod, e2m1_lut, fp8_max):
|
||
"""Convert NVFP4 weight to FP8 for fp8_einsum path (wo_a only).
|
||
|
||
Uses DeepGEMM's deepgemm_post_process_fp8_weight_block to ensure
|
||
correct weight and scale format for fp8_einsum with BMM.
|
||
"""
|
||
w_uint8 = mod.weight.data
|
||
device = w_uint8.device
|
||
w_bf16 = self._unpack_nvfp4_to_bf16(w_uint8, e2m1_lut, device)
|
||
|
||
# Dequantize with scales
|
||
if hasattr(mod, "weight_scale") and hasattr(mod, "weight_scale_2"):
|
||
# NVFP4 block scales: float8_e4m3fn → .to(float32) (Bug #7 reverted)
|
||
block_scale = self._ue8m0_to_float32(mod.weight_scale.data)
|
||
if block_scale.dim() == 2 and w_bf16.dim() == 2:
|
||
block_size = w_bf16.shape[1] // block_scale.shape[1]
|
||
block_scale_expanded = block_scale.unsqueeze(-1).expand(
|
||
-1, -1, block_size
|
||
).reshape(w_bf16.shape)
|
||
else:
|
||
block_scale_expanded = block_scale
|
||
global_scale = mod.weight_scale_2.data.max().item()
|
||
input_scale = (
|
||
mod.input_scale.data.max().item()
|
||
if hasattr(mod, "input_scale")
|
||
else 1.0
|
||
)
|
||
# NOTE: input_scale is for ACTIVATIONS, not weights.
|
||
# Weight dequant = e2m1 * block_scale * global_scale (NO input_scale)
|
||
w_dequant = w_bf16.float() * block_scale_expanded * global_scale
|
||
w_dequant = w_dequant.to(torch.bfloat16)
|
||
else:
|
||
w_dequant = w_bf16
|
||
|
||
# Re-quantize bf16 -> FP8 e4m3 with block quantization
|
||
# DeepGEMM expects block-scale format: weight_scale (FP8 e4m3 block scale)
|
||
# and weight_scale_inv (per-tensor scale).
|
||
# We do per-tensor quantization, so block_scale is all-ones.
|
||
w_amax = w_dequant.abs().amax()
|
||
if w_amax == 0:
|
||
w_amax = torch.tensor(1.0, device=device)
|
||
fp8_scale = w_amax / fp8_max
|
||
w_fp8 = (w_dequant / fp8_scale).to(torch.float8_e4m3fn)
|
||
|
||
# Create block scale filled with the per-tensor fp8_scale value.
|
||
# DeepGEMM divides by the block scale, so each block gets fp8_scale.
|
||
BLOCK_SIZE = 128
|
||
is_bmm = getattr(mod, "is_bmm", False)
|
||
bmm_batch_size = getattr(mod, "bmm_batch_size", 0)
|
||
|
||
# Weight is 2D (output_size, input_size) before BMM reshape
|
||
# Block scale shape: (output_size / BLOCK_SIZE, input_size / BLOCK_SIZE)
|
||
rows = w_fp8.size(0)
|
||
cols = w_fp8.size(1)
|
||
block_rows = rows // BLOCK_SIZE
|
||
block_cols = cols // BLOCK_SIZE
|
||
|
||
# Fill block scale with the per-tensor fp8_scale (NOT all-ones!)
|
||
# This is correct because we requantized with a single per-tensor scale,
|
||
# so every 128x128 block has the same scale = fp8_scale.
|
||
ws = torch.full((block_rows, block_cols), fp8_scale.item(), dtype=torch.float32, device=device)
|
||
|
||
# Use DeepGEMM's post-processing for proper layout transformation
|
||
from vllm.model_executor.layers.quantization.utils.fp8_utils import (
|
||
deepgemm_post_process_fp8_weight_block,
|
||
)
|
||
w_fp8, ws = deepgemm_post_process_fp8_weight_block(
|
||
wq=w_fp8,
|
||
ws=ws,
|
||
quant_block_shape=(BLOCK_SIZE, BLOCK_SIZE),
|
||
use_e8m0=True, # scale_fmt=ue8m0
|
||
is_bmm=is_bmm,
|
||
bmm_batch_size=bmm_batch_size,
|
||
)
|
||
|
||
# Free source tensors eagerly
|
||
del w_uint8, w_bf16, w_dequant
|
||
mod.weight = torch.nn.Parameter(w_fp8, requires_grad=False)
|
||
del w_fp8
|
||
# weight_scale_inv is what the attention runtime reads as b_scale
|
||
# for deepseek_v4_fp8_einsum -> DeepGEMM fp8_einsum.
|
||
# It must be the DeepGEMM-formatted block scale (dg_ws), NOT the
|
||
# per-tensor scalar. See: deepseek_v4_attention.py line 319.
|
||
mod.weight_scale_inv = torch.nn.Parameter(ws, requires_grad=False)
|
||
del ws
|
||
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
|
||
mod.quant_method = UnquantizedLinearMethod()
|
||
for attr in ("weight_scale", "weight_scale_2", "input_scale"):
|
||
if hasattr(mod, attr):
|
||
delattr(mod, attr)
|
||
|
||
@staticmethod
|
||
def _build_shard_index(ckpt_dir: str) -> dict[str, str]:
|
||
"""Build key→shard_path index from safetensors metadata (no tensor I/O)."""
|
||
import glob
|
||
from safetensors import safe_open
|
||
index = {}
|
||
for shard_file in sorted(glob.glob(os.path.join(ckpt_dir, "model-*.safetensors"))):
|
||
try:
|
||
with safe_open(shard_file, framework="pt") as f:
|
||
for key in f.keys():
|
||
index[key] = shard_file
|
||
except Exception:
|
||
continue
|
||
return index
|
||
|
||
def _reconstruct_compressor_weight(self, fused_mod, parent_mod, layer_idx, e2m1_lut, sub_path="", _shard_index=None):
|
||
"""Reconstruct compressor fused_wkv_wgate from checkpoint.
|
||
|
||
Compressor weights are SKIPPED during loading because NVFP4 uint8 data
|
||
can't be loaded into bf16 MergedColumnParallelLinear params (shape mismatch).
|
||
We read the original uint8 data from the safetensors checkpoint, unpack
|
||
E2M1, dequantize, and stack into the fused weight param.
|
||
"""
|
||
from safetensors import safe_open
|
||
|
||
# Find the checkpoint directory
|
||
# The model weights are mounted at /model in Docker
|
||
ckpt_dir = "/model"
|
||
if not os.path.isdir(ckpt_dir):
|
||
print(f"WARNING: layer {layer_idx} compressor: checkpoint dir {ckpt_dir} not found")
|
||
return 0
|
||
|
||
# Determine the layer's compressor key prefix in the checkpoint
|
||
# Before mapper: model.layers.N.self_attn.compressor.{kv_proj,gate_proj}
|
||
# After mapper: model.layers.N.attn.mla_attn.compressor.{wkv,wgate}
|
||
# We read from checkpoint (before mapper), so use original names
|
||
layer_prefix = f"model.layers.{layer_idx}.self_attn.compressor{sub_path}"
|
||
|
||
# All keys we need from the checkpoint
|
||
keys = {
|
||
'wkv_uint8': f"{layer_prefix}.kv_proj.weight",
|
||
'wgate_uint8': f"{layer_prefix}.gate_proj.weight",
|
||
'wkv_block_scale': f"{layer_prefix}.kv_proj.weight_scale",
|
||
'wgate_block_scale': f"{layer_prefix}.gate_proj.weight_scale",
|
||
'wkv_global_scale': f"{layer_prefix}.kv_proj.weight_scale_2",
|
||
'wgate_global_scale': f"{layer_prefix}.gate_proj.weight_scale_2",
|
||
'wkv_input_scale': f"{layer_prefix}.kv_proj.input_scale",
|
||
'wgate_input_scale': f"{layer_prefix}.gate_proj.input_scale",
|
||
}
|
||
|
||
# Read tensors using shard index for targeted access (no full-shard loads)
|
||
tensors = {}
|
||
for name, key in keys.items():
|
||
shard_path = (_shard_index or {}).get(key)
|
||
if shard_path is None:
|
||
continue
|
||
try:
|
||
with safe_open(shard_path, framework="pt") as f:
|
||
if key in f.keys():
|
||
tensors[name] = f.get_tensor(key)
|
||
except Exception:
|
||
continue
|
||
|
||
wkv_uint8 = tensors.get('wkv_uint8')
|
||
wgate_uint8 = tensors.get('wgate_uint8')
|
||
|
||
if wkv_uint8 is None or wgate_uint8 is None:
|
||
# Layer might not have a compressor (compress_ratio=1 layers)
|
||
return 0
|
||
|
||
wkv_block_scale = tensors.get('wkv_block_scale')
|
||
wgate_block_scale = tensors.get('wgate_block_scale')
|
||
wkv_global_scale = tensors.get('wkv_global_scale')
|
||
wgate_global_scale = tensors.get('wgate_global_scale')
|
||
wkv_input_scale = tensors.get('wkv_input_scale')
|
||
wgate_input_scale = tensors.get('wgate_input_scale')
|
||
|
||
device = fused_mod.weight.device
|
||
wkv_uint8 = wkv_uint8.to(device)
|
||
wgate_uint8 = wgate_uint8.to(device)
|
||
|
||
# Unpack E2M1 FP4→bf16
|
||
wkv_bf16 = self._unpack_nvfp4_to_bf16(wkv_uint8, e2m1_lut, device)
|
||
wgate_bf16 = self._unpack_nvfp4_to_bf16(wgate_uint8, e2m1_lut, device)
|
||
|
||
# Dequantize with scales
|
||
def _dequant(w_bf16, block_scale, global_scale, input_scale):
|
||
if block_scale is not None and global_scale is not None:
|
||
# NVFP4 block scales: float8_e4m3fn → .to(float32) (Bug #7 reverted)
|
||
block_scale = self._ue8m0_to_float32(block_scale.to(device))
|
||
if block_scale.dim() == 2 and w_bf16.dim() == 2:
|
||
block_size = w_bf16.shape[1] // block_scale.shape[1]
|
||
block_scale_exp = block_scale.unsqueeze(-1).expand(
|
||
-1, -1, block_size
|
||
).reshape(w_bf16.shape)
|
||
else:
|
||
block_scale_exp = block_scale
|
||
gs = global_scale.to(device).max().item()
|
||
# NOTE: input_scale is for activations, not weights.
|
||
# Weight dequant = e2m1 * block_scale * global_scale (NO input_scale)
|
||
w = w_bf16.float() * block_scale_exp * gs
|
||
return w.to(torch.bfloat16)
|
||
return w_bf16
|
||
|
||
wkv_dequant = _dequant(wkv_bf16, wkv_block_scale, wkv_global_scale, wkv_input_scale)
|
||
wgate_dequant = _dequant(wgate_bf16, wgate_block_scale, wgate_global_scale, wgate_input_scale)
|
||
|
||
# Stack: concatenate along output dim (dim 0)
|
||
# fused_wkv_wgate.weight = cat([wkv, wgate], dim=0) → (2*head_dim, hidden_size)
|
||
w_fused = torch.cat([wkv_dequant, wgate_dequant], dim=0)
|
||
|
||
|
||
# Replace the weight
|
||
fused_mod.weight = torch.nn.Parameter(w_fused, requires_grad=False)
|
||
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
|
||
fused_mod.quant_method = UnquantizedLinearMethod()
|
||
for attr in ("weight_scale", "weight_scale_2", "input_scale", "weight_scale_inv"):
|
||
if hasattr(fused_mod, attr):
|
||
delattr(fused_mod, attr)
|
||
return 1
|
||
return 0
|
||
|
||
def _convert_bf16_to_fp8(self, mod, fp8_max):
|
||
"""Convert BF16 weight to FP8 for fp8_einsum path.
|
||
|
||
Used for wo_a which modelopt did NOT quantize (bf16 in checkpoint)
|
||
but which the attention forward reads as FP8 for deepseek_v4_fp8_einsum.
|
||
Uses DeepGEMM's post-processing for proper BMM + scale format.
|
||
"""
|
||
w_bf16 = mod.weight.data
|
||
device = w_bf16.device
|
||
|
||
# Re-quantize bf16 -> FP8 e4m3 with block quantization
|
||
w_amax = w_bf16.abs().amax()
|
||
if w_amax == 0:
|
||
w_amax = torch.tensor(1.0, device=device)
|
||
fp8_scale = w_amax / fp8_max
|
||
w_fp8 = (w_bf16 / fp8_scale).to(torch.float8_e4m3fn)
|
||
|
||
BLOCK_SIZE = 128
|
||
is_bmm = getattr(mod, "is_bmm", False)
|
||
bmm_batch_size = getattr(mod, "bmm_batch_size", 0)
|
||
|
||
rows = w_fp8.size(0)
|
||
cols = w_fp8.size(1)
|
||
block_rows = rows // BLOCK_SIZE
|
||
block_cols = cols // BLOCK_SIZE
|
||
# Fill block scale with per-tensor fp8_scale (NOT all-ones!)
|
||
ws = torch.full((block_rows, block_cols), fp8_scale.item(), dtype=torch.float32, device=device)
|
||
|
||
from vllm.model_executor.layers.quantization.utils.fp8_utils import (
|
||
deepgemm_post_process_fp8_weight_block,
|
||
)
|
||
w_fp8, ws = deepgemm_post_process_fp8_weight_block(
|
||
wq=w_fp8,
|
||
ws=ws,
|
||
quant_block_shape=(BLOCK_SIZE, BLOCK_SIZE),
|
||
use_e8m0=True, # scale_fmt=ue8m0
|
||
is_bmm=is_bmm,
|
||
bmm_batch_size=bmm_batch_size,
|
||
)
|
||
|
||
mod.weight = torch.nn.Parameter(w_fp8, requires_grad=False)
|
||
# weight_scale_inv is what the attention runtime reads as b_scale
|
||
# for deepseek_v4_fp8_einsum -> DeepGEMM fp8_einsum.
|
||
# It must be the DeepGEMM-formatted block scale (dg_ws), NOT the
|
||
# per-tensor scalar. See: deepseek_v4_attention.py line 319.
|
||
mod.weight_scale_inv = torch.nn.Parameter(ws, requires_grad=False)
|
||
# weight_scale is not used at runtime for BMM layers; remove it
|
||
# to avoid confusing other code paths.
|
||
for attr in ("weight_scale", "weight_scale_2", "input_scale"):
|
||
if hasattr(mod, attr):
|
||
delattr(mod, attr)
|
||
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
|
||
mod.quant_method = UnquantizedLinearMethod()
|
||
|
||
@staticmethod
|
||
def _ue8m0_to_float32(sf: torch.Tensor) -> torch.Tensor:
|
||
"""Convert NVFP4 block scales (float8_e4m3fn / UE4M3) to float32.
|
||
|
||
Checkpoint stores float8_e4m3fn (standard NVFP4 spec, NOT UE8M0).
|
||
Simple .to(float32) is correct — shift-by-23 was wrong (Bug #7 fix).
|
||
"""
|
||
return sf.to(torch.float32)
|
||
|
||
def _unpack_nvfp4_to_bf16(self, w_uint8, e2m1_lut, device):
|
||
"""Unpack NVFP4 uint8 packed weights to bf16 using E2M1 format."""
|
||
# Extract 4-bit FP4 values (0-15, bit 3 = sign)
|
||
even_raw = (w_uint8 & 0x0F).int()
|
||
odd_raw = ((w_uint8 >> 4) & 0x0F).int()
|
||
# Sign: 0-7 = positive, 8-15 = negative
|
||
even_sign = torch.where(even_raw >= 8, -1.0, 1.0).to(torch.bfloat16)
|
||
odd_sign = torch.where(odd_raw >= 8, -1.0, 1.0).to(torch.bfloat16)
|
||
# Magnitude index: lower 3 bits (0-7)
|
||
even_vals = even_sign * e2m1_lut.to(device)[even_raw & 0x07]
|
||
odd_vals = odd_sign * e2m1_lut.to(device)[odd_raw & 0x07]
|
||
# Interleave and flatten
|
||
w_bf16 = torch.stack([even_vals, odd_vals], dim=-1)
|
||
w_bf16 = w_bf16.reshape(w_uint8.shape[0], -1).to(torch.bfloat16)
|
||
return w_bf16
|
||
@torch.compile(backend=current_platform.simple_compile_backend)
|
||
def hc_head(
|
||
hidden_states: torch.Tensor,
|
||
hc_fn: torch.Tensor,
|
||
hc_scale: torch.Tensor,
|
||
hc_base: torch.Tensor,
|
||
rms_norm_eps: float,
|
||
hc_eps: float,
|
||
) -> torch.Tensor:
|
||
hc_mult, hidden_size = hidden_states.shape[-2:]
|
||
outer_shape = hidden_states.shape[:-2]
|
||
hs_flat = hidden_states.view(-1, hc_mult, hidden_size)
|
||
num_tokens = hs_flat.shape[0]
|
||
out = torch.empty(
|
||
num_tokens, hidden_size, dtype=torch.bfloat16, device=hidden_states.device
|
||
)
|
||
torch.ops.vllm.hc_head_fused_kernel(
|
||
hs_flat,
|
||
hc_fn,
|
||
hc_scale,
|
||
hc_base,
|
||
out,
|
||
hidden_size,
|
||
rms_norm_eps,
|
||
hc_eps,
|
||
hc_mult,
|
||
)
|
||
return out.view(*outer_shape, hidden_size)
|
||
|
||
|
||
def _make_deepseek_v4_weights_mapper(expert_dtype: str) -> WeightsMapper:
|
||
if expert_dtype == "fp4":
|
||
# MXFP4 experts use Mxfp4MoEMethod, which registers scales as
|
||
# ``w{1,2,3}_weight_scale`` (no _inv suffix). FP8 linear and
|
||
# shared experts use Fp8LinearMethod's block scales, which
|
||
# register as ``weight_scale_inv``.
|
||
scale_regex = {
|
||
re.compile(r"(\.experts\.\d+\.w[123])\.scale$"): r"\1.weight_scale",
|
||
re.compile(r"\.scale$"): ".weight_scale_inv",
|
||
}
|
||
else:
|
||
# FP8 experts use Fp8MoEMethod (block_quant=True), which registers
|
||
# scales as ``w{13,2}_weight_scale_inv``. Map all ``.scale`` keys
|
||
# there.
|
||
scale_regex = {
|
||
re.compile(r"\.scale$"): ".weight_scale_inv",
|
||
}
|
||
|
||
# ── ModelOpt NVFP4 export patches ────────────────────────────────
|
||
# modelopt exports with different naming than the original HF ckpt:
|
||
# - Expert projections: gate_proj/up_proj/down_proj → w1/w3/w2
|
||
# - Shared expert projections: gate_proj/up_proj → w1/w3 (stacking)
|
||
# - Compressor: kv_proj → wkv, gate_proj → wgate (stacking)
|
||
# - Attention: self_attn prefix, kv_proj → wkv (stacking)
|
||
# - modelopt uses mlp, vllm uses ffn
|
||
# Order matters for regex: skip patterns MUST come before renames.
|
||
|
||
# Skip NVFP4 scales for compressor+attention fused params.
|
||
# After substr renaming, these map to stacked params (fused_wkv_wgate,
|
||
# fused_wqa_wkv, gate_up_proj) which don't register NVFP4 scale params
|
||
# because ModelOptNvFp4Config only handles Linear, not
|
||
# MergedColumnParallelLinear. We unpack weights as bf16 and let
|
||
# process_weights_after_loading re-quantize them.
|
||
# Must match ORIGINAL checkpoint key names (before substr renaming).
|
||
fused_skip_regex = {
|
||
# Compressor: SKIP ALL tensors. The compressor uses quant_config=None,
|
||
# so MergedColumnParallelLinear creates bf16 weight params. NVFP4 uint8
|
||
# checkpoint data can't be loaded into these params (shape mismatch:
|
||
# uint8 (head_dim, hidden_size//2) vs bf16 (head_dim, hidden_size)).
|
||
# The stacking weight_loader silently skips the sub-weights, leaving
|
||
# random bf16 initialization. We reconstruct the compressor weights
|
||
# manually in post-load conversion by reading from the checkpoint.
|
||
re.compile(r"\.compressor\.kv_proj\.weight$"): None,
|
||
re.compile(r"\.compressor\.gate_proj\.weight$"): None,
|
||
re.compile(r"\.compressor\.kv_proj\.weight_scale$"): None,
|
||
re.compile(r"\.compressor\.gate_proj\.weight_scale$"): None,
|
||
re.compile(r"\.compressor\.kv_proj\.weight_scale_2$"): None,
|
||
re.compile(r"\.compressor\.gate_proj\.weight_scale_2$"): None,
|
||
re.compile(r"\.compressor\.kv_proj\.input_scale$"): None,
|
||
re.compile(r"\.compressor\.gate_proj\.input_scale$"): None,
|
||
# Note: attention and shared expert scale tensors are NO LONGER
|
||
# skipped. After fixing substr mappings, they correctly map to the
|
||
# model's NVFP4 scale parameters (fused_wqa_wkv, wq_b, wo_a,
|
||
# wo_b, gate_up_proj). They load via the stacking logic.
|
||
}
|
||
# Routed expert projections: gate_proj→w1, up_proj→w3, down_proj→w2
|
||
# Regex (not substr) to match ONLY .experts.N. — not .shared_experts.
|
||
expert_rename_regex = {
|
||
re.compile(r"(\.experts\.\d+\.)gate_proj\."): r"\1w1.",
|
||
re.compile(r"(\.experts\.\d+\.)up_proj\."): r"\1w3.",
|
||
re.compile(r"(\.experts\.\d+\.)down_proj\."): r"\1w2.",
|
||
}
|
||
# Merge: skip patterns first, then renames, then original scale_regex
|
||
merged_regex = {}
|
||
merged_regex.update(fused_skip_regex)
|
||
merged_regex.update(expert_rename_regex)
|
||
merged_regex.update(scale_regex)
|
||
|
||
return WeightsMapper(
|
||
orig_to_new_prefix={
|
||
"layers.": "model.layers.",
|
||
"embed.": "model.embed.",
|
||
"norm.": "model.norm.",
|
||
"hc_head": "model.hc_head",
|
||
"mtp.": "model.mtp.",
|
||
},
|
||
orig_to_new_regex=merged_regex,
|
||
orig_to_new_suffix={
|
||
"embed.weight": "embed_tokens.weight",
|
||
".ffn.gate.bias": ".ffn.gate.e_score_correction_bias",
|
||
},
|
||
orig_to_new_substr={
|
||
".attn.compressor.": ".attn.mla_attn.compressor.",
|
||
".shared_experts.w2": ".shared_experts.down_proj",
|
||
# ── ModelOpt NVFP4 substr patches ──
|
||
# Attention: self_attn → attn (projections at attn level, not mla_attn)
|
||
".self_attn.q_a_proj.": ".attn.wq_a.",
|
||
".self_attn.q_b_proj.": ".attn.wq_b.",
|
||
".self_attn.q_a_norm.": ".attn.q_norm.",
|
||
".self_attn.o_a_proj.": ".attn.wo_a.",
|
||
".self_attn.o_b_proj.": ".attn.wo_b.",
|
||
".self_attn.sinks": ".attn.attn_sink",
|
||
# kv_proj → wkv (for stacking into fused_wqa_wkv)
|
||
".self_attn.kv_proj.": ".attn.wkv.",
|
||
".self_attn.kv_norm.": ".attn.kv_norm.",
|
||
# kv_norm is at attention level, not compressor/mla_attn level in vllm
|
||
# Must come before the general compressor mapping
|
||
".self_attn.compressor.kv_norm.": ".attn.kv_norm.",
|
||
# Compressor: self_attn.compressor → attn.mla_attn.compressor
|
||
".self_attn.compressor.": ".attn.mla_attn.compressor.",
|
||
# Compressor projections for stacking (fused_wkv_wgate)
|
||
".compressor.kv_proj.": ".compressor.wkv.",
|
||
".compressor.gate_proj.": ".compressor.wgate.",
|
||
# Shared expert projections (stacking into gate_up_proj)
|
||
".shared_experts.gate_proj.": ".shared_experts.w1.",
|
||
".shared_experts.up_proj.": ".shared_experts.w3.",
|
||
# modelopt uses mlp, vllm uses ffn internally
|
||
".mlp.": ".ffn.",
|
||
},
|
||
)
|
||
|
||
|
||
class DeepseekV4ForCausalLM(nn.Module):
|
||
model_cls = DeepseekV4Model
|
||
|
||
# Default mapper assumes the original FP4-expert checkpoint layout.
|
||
# Overridden per-instance in __init__ when expert_dtype != "fp4".
|
||
hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper("fp4")
|
||
|
||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||
super().__init__()
|
||
|
||
config = vllm_config.model_config.hf_config
|
||
self.config = config
|
||
expert_dtype = getattr(config, "expert_dtype", "fp4")
|
||
if expert_dtype != "fp4":
|
||
self.hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper(expert_dtype)
|
||
|
||
self.model = self.model_cls(
|
||
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
|
||
)
|
||
self.lm_head = ParallelLMHead(
|
||
config.vocab_size,
|
||
config.hidden_size,
|
||
prefix=maybe_prefix(prefix, "lm_head"),
|
||
)
|
||
self.logits_processor = LogitsProcessor(config.vocab_size)
|
||
|
||
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||
return self.model.embed_input_ids(input_ids)
|
||
|
||
def compute_logits(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
) -> torch.Tensor | None:
|
||
logits = self.logits_processor(self.lm_head, hidden_states)
|
||
return logits
|
||
|
||
def forward(
|
||
self,
|
||
input_ids: torch.Tensor,
|
||
positions: torch.Tensor,
|
||
intermediate_tensors: IntermediateTensors | None = None,
|
||
inputs_embeds: torch.Tensor | None = None,
|
||
) -> torch.Tensor | IntermediateTensors:
|
||
hidden_states = self.model(
|
||
input_ids, positions, intermediate_tensors, inputs_embeds
|
||
)
|
||
return hidden_states
|
||
|
||
def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
|
||
"""Pre-hc_head residual stream buffer (max_num_batched_tokens,
|
||
hc_mult * hidden_size) for the MTP draft model. Populated by
|
||
forward(); valid after each target step."""
|
||
return getattr(self.model, "_mtp_hidden_buffer", None)
|
||
|
||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||
loader = AutoWeightsLoader(self, skip_substrs=["mtp."])
|
||
loaded_params = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
|
||
self.model.finalize_mega_moe_weights()
|
||
self.model._convert_nvfp4_post_load()
|
||
if os.environ.get('NVFP4_DEBUG_SYNC', '') == '1':
|
||
torch.cuda.synchronize()
|
||
print("[NVFP4] post-load conversion done, CUDA OK")
|
||
return loaded_params
|
||
|
||
def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
|
||
return self.model.get_expert_mapping()
|