[Model] Support DeepSeek-V4.1-Flash (#56214)

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Signed-off-by: Jee Jee Li <jeejeelee@inferact.ai>
Signed-off-by: andyluo7 <andy.luo@amd.com>
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Co-authored-by: Jee Jee Li <jeejeelee@inferact.ai>
Co-authored-by: andyluo7 <andy.luo@amd.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: zjy0516 <riverclouds.zhu@qq.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yongye Zhu
2026-09-11 02:11:19 -07:00
committed by GitHub
co-authored by Jee Jee Li andyluo7 OpenAI Codex zjy0516 Claude Opus 5
parent e6821ceac9
commit e77daef89e
49 changed files with 3996 additions and 138 deletions
+378
View File
@@ -37,6 +37,7 @@ from vllm.model_executor.layers.quantization.fp8 import (
Fp8MoEMethod,
)
from vllm.model_executor.layers.quantization.kv_cache import BaseKVCacheMethod
from vllm.model_executor.layers.quantization.modelopt import ModelOptLinearMethod
from vllm.model_executor.layers.quantization.online.fp8 import (
Fp8PerTensorOnlineLinearMethod,
)
@@ -63,6 +64,383 @@ MODELS = [
]
@pytest.mark.skipif(not current_platform.is_cuda(), reason="DSV4.1 requires CUDA")
@pytest.mark.parametrize("tp_rank", [0, 1])
@pytest.mark.parametrize("scale_dtype", [torch.uint8, torch.float8_e8m0fnu])
def test_deepseek_v41_mxfp8_scale_loading(
dist_init, default_vllm_config, monkeypatch, tp_rank, scale_dtype
):
"""Expand linear scales before TP slicing, including padded shared experts."""
from vllm.model_executor.layers import linear as linear_module
from vllm.model_executor.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
RowParallelLinear,
)
from vllm.models.deepseek_v4_1 import quant_config as quant_module
from vllm.models.deepseek_v4_1.nvidia import model as model_module
default_vllm_config.model_config = SimpleNamespace(dtype=torch.bfloat16)
quant_config = quant_module.DeepseekV4FP8Config(
is_checkpoint_fp8_serialized=True, weight_block_size=[32, 32]
)
quant_config._resolved_expert_dtype = "fp4"
for module in (model_module, linear_module):
monkeypatch.setattr(module, "get_tensor_model_parallel_world_size", lambda: 2)
monkeypatch.setattr(module, "get_tensor_model_parallel_rank", lambda: tp_rank)
tp_args = dict(quant_config=quant_config, bias=False)
block = torch.nn.Module()
block.attn = torch.nn.Module()
block.attn.wq_b = ColumnParallelLinear(128, 128, **tp_args)
block.attn.fused_wqa_wkv = MergedColumnParallelLinear(128, [128, 64], **tp_args)
block.ffn = torch.nn.Module()
block.ffn.shared_experts = torch.nn.Module()
block.ffn.shared_experts.gate_up_proj = MergedColumnParallelLinear(
128, [128, 128], **tp_args
)
block.ffn.shared_experts.down_proj = RowParallelLinear(128, 128, **tp_args)
block.ffn.experts = torch.nn.Module()
block.ffn.experts.register_parameter(
"weight_scale", torch.nn.Parameter(torch.empty(3, 4, dtype=torch.uint8), False)
)
block.engram = torch.nn.Module()
block.engram.register_parameter(
"weight_scale_inv",
torch.nn.Parameter(torch.empty(3, 4, dtype=scale_dtype), False),
)
def load_expert_scale(param, weight, *args, **kwargs):
param.data.copy_(weight)
return True
block.ffn.experts.weight_scale.weight_loader = load_expert_scale
model = model_module.DeepseekV4Model.__new__(model_module.DeepseekV4Model)
torch.nn.Module.__init__(model)
model.layers = torch.nn.ModuleList([block])
model.config = SimpleNamespace(num_attention_heads=2)
model.quant_config = quant_config
model.use_sequence_parallel = False
model.get_expert_mapping = lambda: [
("experts.weight_scale", "experts.0.w1.weight_scale", 0, "w1")
]
checkpoints = [
("attn.wq_b", "attn.wq_b", 128, 128, 0, None),
("attn.wq_a", "attn.fused_wqa_wkv", 128, 128, 0, slice(0, 64)),
("attn.wkv", "attn.fused_wqa_wkv", 64, 128, 0, slice(64, 96)),
(
"ffn.shared_experts.w1",
"ffn.shared_experts.gate_up_proj",
96,
128,
0,
slice(0, 64),
),
(
"ffn.shared_experts.w3",
"ffn.shared_experts.gate_up_proj",
96,
128,
0,
slice(64, 128),
),
(
"ffn.shared_experts.down_proj",
"ffn.shared_experts.down_proj",
128,
96,
1,
None,
),
]
weights = []
expected = []
for source, target, n, k, axis, shard in checkpoints:
weight = torch.randint(-4, 5, (n, k)).to(torch.float8_e4m3fn)
scale_bytes = torch.randint(124, 131, (n // 32, k // 32), dtype=torch.uint8)
weights.extend(
[
(f"layers.0.{source}.weight", weight),
(f"layers.0.{source}.scale", scale_bytes.view(scale_dtype)),
]
)
dequant = (
weight.float().reshape(n // 32, 32, k // 32, 32)
* torch.exp2(scale_bytes.float() - 127)[:, None, :, None]
).reshape(n, k)
if "shared_experts" in source:
padded = torch.zeros(128, 128)
padded[:n, :k] = dequant
dequant = padded
expected.append((target, shard, dequant.chunk(2, dim=axis)[tp_rank]))
expert_scale = torch.full((3, 4), 125, dtype=torch.uint8)
weights.append(
("layers.0.ffn.experts.0.w1.weight_scale", expert_scale.view(scale_dtype))
)
weights.append(("layers.0.engram.weight_scale_inv", expert_scale.view(scale_dtype)))
mapper = model_module._make_deepseek_v4_weights_mapper("fp4", "weight_scale")
loaded = model.load_weights(
(name.removeprefix("model."), weight) for name, weight in mapper.apply(weights)
)
assert "layers.0.ffn.experts.weight_scale" in loaded
assert torch.equal(block.ffn.experts.weight_scale, expert_scale)
assert torch.equal(block.engram.weight_scale_inv.view(torch.uint8), expert_scale)
for target, shard, reference in expected:
linear = block.get_submodule(target)
assert isinstance(linear.quant_method, ModelOptLinearMethod)
assert f"layers.0.{target}.weight_scale" in loaded
weight = linear.weight if shard is None else linear.weight[shard]
scale = linear.weight_scale if shard is None else linear.weight_scale[shard]
actual = (
weight.float().unflatten(-1, (-1, 32))
* torch.exp2(scale.float() - 127).unsqueeze(-1)
).flatten(-2)
torch.testing.assert_close(actual, reference, rtol=0, atol=0)
@pytest.mark.skipif(not current_platform.is_cuda(), reason="DSV4.1 requires CUDA")
@pytest.mark.parametrize(
"weight_block_size,expert_dtype,scale_name",
[
([32, 32], "fp4", "weight_scale"),
([128, 128], "fp4", "weight_scale_inv"),
([32, 32], "fp8", "weight_scale_inv"),
],
)
def test_deepseek_v41_vl_mapper_routes_linear_scales(
weight_block_size, expert_dtype, scale_name
):
"""The VL wrapper must map ``.scale`` keys to the parameter the linear
quant method registers, as the text model does. A hardcoded
``weight_scale_inv`` raised KeyError for native MXFP8 checkpoints."""
from vllm.models.deepseek_v4_1.nvidia import model as model_module
from vllm.models.deepseek_v4_1.nvidia import vl_model as vl_module
vllm_config = SimpleNamespace(
quant_config=SimpleNamespace(weight_block_size=weight_block_size)
)
resolved = model_module._linear_scale_param_name(vllm_config, expert_dtype)
assert resolved == scale_name
mapper = vl_module._make_deepseek_v4_vl_weights_mapper(expert_dtype, resolved)
weight = torch.empty(0)
mapped = [
name
for name, _ in mapper.apply(
[("layers.0.attn.wq_a.scale", weight), ("layers.0.attn.wkv.weight", weight)]
)
]
assert mapped == [
f"language_model.model.layers.0.attn.wq_a.{scale_name}",
"language_model.model.layers.0.attn.wkv.weight",
]
@pytest.mark.skipif(not current_platform.is_cuda(), reason="DeepGEMM requires CUDA")
@pytest.mark.parametrize("scale_dtype", [torch.uint8, torch.float8_e8m0fnu])
@pytest.mark.parametrize(
"weight_shape,is_bmm",
[
((129, 160), False),
((387, 160), True),
((3, 129, 160), False),
((3, 128, 512), False),
],
)
def test_deepgemm_mxfp8_preserves_weight_and_scale_values(
scale_dtype, weight_shape, is_bmm
):
"""DeepGEMM layout conversion preserves native weights and E8M0 scales."""
from vllm.model_executor.layers.quantization.utils import fp8_utils
from vllm.utils.deep_gemm import is_deep_gemm_supported
if not is_deep_gemm_supported() or not current_platform.is_device_capability_family(
100
):
pytest.skip("DeepGEMM MXFP8 requires Blackwell")
weight = torch.randn(weight_shape, device="cuda").to(torch.float8_e4m3fn)
scales = (
torch.randint(
1,
255,
(weight.numel() // 32 + 1,),
device="cuda",
dtype=torch.uint8,
)[1:]
.view(*weight_shape[:-1], weight_shape[-1] // 32)
.view(scale_dtype)
)
original_weight = weight.view(torch.uint8).clone()
original_scales = scales.view(torch.uint8).clone()
processed_weight, packed = fp8_utils.deepgemm_post_process_fp8_weight_block(
weight,
scales,
quant_block_shape=(1, 32),
use_e8m0=True,
is_bmm=is_bmm,
bmm_batch_size=3 if is_bmm else 0,
)
assert packed.dtype == torch.int32
unpacked = torch.stack(
[(packed >> (8 * i)) & 0xFF for i in range(4)], dim=-1
).flatten(-2)
unpacked = unpacked[..., : scales.shape[-1]].to(torch.uint8)
torch.testing.assert_close(
unpacked.reshape_as(original_scales), original_scales, rtol=0, atol=0
)
torch.testing.assert_close(
processed_weight.view(torch.uint8).reshape_as(original_weight),
original_weight,
rtol=0,
atol=0,
)
@pytest.mark.skipif(not current_platform.is_cuda(), reason="DeepGEMM requires CUDA")
@pytest.mark.parametrize("prequantized", [False, True])
@pytest.mark.parametrize("config_source", ["deepseek", "mxfp8"])
@pytest.mark.parametrize("num_tokens", [1, 7, 128])
def test_mxfp8_bmm_loads_and_projects_grouped_weights(
dist_init, default_vllm_config, prequantized, config_source, num_tokens
):
"""BMM metadata set after construction selects grouped weight processing."""
from vllm.model_executor.kernels.linear.mxfp8.deep_gemm import (
DeepGemmMxfp8BmmLinearKernel,
)
from vllm.model_executor.layers.linear import ColumnParallelLinear
from vllm.model_executor.layers.quantization import get_quantization_config
from vllm.models.deepseek_v4.common.ops.fused_inv_rope_fp8_quant import (
fused_inv_rope_fp8_quant,
)
from vllm.models.deepseek_v4.nvidia.ops.o_proj import (
compute_fp8_einsum_recipe,
deep_gemm_fp8_o_proj,
)
from vllm.models.deepseek_v4_1.nvidia.model import DeepseekV4Model
from vllm.models.deepseek_v4_1.quant_config import DeepseekV4FP8Config
from vllm.utils.deep_gemm import is_deep_gemm_supported
if not is_deep_gemm_supported() or not current_platform.is_device_capability_family(
100
):
pytest.skip("DeepGEMM MXFP8 BMM requires Blackwell")
default_vllm_config.model_config = SimpleNamespace(dtype=torch.bfloat16)
quant_config = DeepseekV4FP8Config(
is_checkpoint_fp8_serialized=True, weight_block_size=[32, 32]
)
quant_config._resolved_expert_dtype = "fp4"
if config_source == "mxfp8":
quant_config = get_quantization_config("mxfp8").from_config(
{"quant_method": "mxfp8"}
)
with torch.device("cuda"):
linear = ColumnParallelLinear(
512,
256,
bias=False,
quant_config=quant_config,
return_bias=False,
)
linear.is_bmm = True
linear.bmm_batch_size = 2
weight = torch.randn(256, 512, device="cuda").to(torch.float8_e4m3fn)
scales = torch.randint(124, 131, (8, 16), device="cuda", dtype=torch.uint8)
checkpoint_scales = (
scales.view(torch.float8_e8m0fnu)
if config_source == "deepseek"
else scales.repeat_interleave(32, dim=0)
)
model = DeepseekV4Model.__new__(DeepseekV4Model)
torch.nn.Module.__init__(model)
block = torch.nn.Module()
block.attn = torch.nn.Module()
block.attn.wo_a = linear
model.layers = torch.nn.ModuleList([block])
model.config = SimpleNamespace(num_attention_heads=2)
model.quant_config = quant_config
model.use_sequence_parallel = False
model.get_expert_mapping = lambda: []
model.load_weights(
[
("layers.0.attn.wo_a.weight", weight),
("layers.0.attn.wo_a.weight_scale", checkpoint_scales),
]
)
reference_weight = (
weight.float().reshape(8, 32, 16, 32)
* torch.exp2(scales.float() - 127)[:, None, :, None]
).reshape(2, 128, 512)
linear.quant_method.process_weights_after_loading(linear)
linear.quant_method.process_weights_after_loading(linear)
assert isinstance(linear.quant_method.kernel, DeepGemmMxfp8BmmLinearKernel)
assert linear.weight.shape == (2, 128, 512)
torch.testing.assert_close(
linear.weight.view(torch.uint8).flatten(),
weight.view(torch.uint8).flatten(),
rtol=0,
atol=0,
)
unpacked_scales = torch.stack(
[(linear.weight_scale >> (8 * i)) & 0xFF for i in range(4)], dim=-1
).flatten(-2)
torch.testing.assert_close(
unpacked_scales.to(torch.uint8).reshape(256, 16),
scales.repeat_interleave(32, dim=0),
rtol=0,
atol=0,
)
x = torch.randn(2, num_tokens, 512, device="cuda", dtype=torch.bfloat16)
reference = torch.einsum("gmk,gnk->mgn", x.float(), reference_weight)
inputs = x.transpose(0, 1)
if prequantized:
cache = torch.cat(
(
torch.ones(num_tokens, 32, device="cuda"),
torch.zeros(num_tokens, 32, device="cuda"),
),
dim=1,
)
inputs = fused_inv_rope_fp8_quant(
x.permute(1, 0, 2),
torch.arange(num_tokens, device="cuda"),
cache,
n_groups=2,
heads_per_group=1,
nope_dim=448,
rope_dim=64,
quant_group_size=32,
tma_aligned_scales=current_platform.has_device_capability(100),
)
with torch.no_grad():
output = linear(inputs)
assert output.shape == reference.shape
assert (output.float() - reference).norm() / reference.norm() < 0.06
if prequantized:
recipe, tma_aligned_scales = compute_fp8_einsum_recipe(block_size=32)
projected = deep_gemm_fp8_o_proj(
x.permute(1, 0, 2),
torch.arange(num_tokens, device="cuda"),
cache,
linear,
torch.nn.Identity(),
n_groups=2,
heads_per_group=1,
nope_dim=448,
rope_dim=64,
o_lora_rank=128,
einsum_recipe=recipe,
tma_aligned_scales=tma_aligned_scales,
)
torch.testing.assert_close(projected, output.flatten(1), rtol=0, atol=0)
compiled = torch.compile(linear, backend="eager", fullgraph=True)
with torch.no_grad():
torch.testing.assert_close(compiled(inputs), output, rtol=0, atol=0)
def test_prepare_gated_trtllm_fp8_moe_weights_pads_each_projection(monkeypatch):
monkeypatch.setattr(
flashinfer_utils,
+17
View File
@@ -127,6 +127,23 @@ def test_modelopt_nvfp4_quantizes_parallel_lm_head():
assert method.spec.activation is kNvfp4Dynamic
def test_modelopt_mxfp8_preserves_per_row_checkpoint_scales(dist_init, monkeypatch):
"""Standard MXFP8 checkpoints already have one scale row per weight row."""
from vllm.model_executor.layers.linear import ReplicatedLinear
kernel = Mock()
kernel.input_quant_key.return_value = None
monkeypatch.setattr(
"vllm.model_executor.layers.quantization.modelopt.init_mxfp8_linear_kernel",
lambda **kwargs: kernel,
)
config = ModelOptMxFp8Config.from_config({"quant_method": "mxfp8"})
linear = ReplicatedLinear(64, 64, bias=False, quant_config=config)
scales = torch.arange(128, dtype=torch.uint8).reshape(64, 2)
linear.weight_scale.weight_loader(linear.weight_scale, scales)
assert torch.equal(linear.weight_scale, scales)
def test_modelopt_fp8_updates_weight_dims_after_transpose():
"""Humming reads weight.input_dim/output_dim. Swapping the
ModelWeightParameter for a plain Parameter drops them, so the per-tensor