Fix NVFP4 weights mapper: add prefix mappings, fix substr order

- Add orig_to_new_prefix mappings (layers→model.layers, embed_tokens→model.embed_tokens, etc.)
  AutoWeightsLoader strips the model. prefix before the mapper runs, so these are required
- Move .self_attn.compressor. → .attn.mla_attn.compressor. before .self_attn. → .attn.
  in substr_renames so compressor keys get the mla_attn prefix before the general rename
- Remove suffix renames (head.weight→lm_head.weight, embed.weight→embed_tokens.weight)
  that were causing double-mapping since the NVFP4 checkpoint already uses lm_head/embed_tokens
- Add unit test: tests/test_nvfp4_mapper.py (39 cases, no vLLM/CUDA needed)
This commit is contained in:
2026-05-18 23:03:34 +00:00
parent b039123207
commit 17496b2615
2 changed files with 182 additions and 4 deletions

173
tests/test_nvfp4_mapper.py Normal file
View File

@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""Unit test for the NVFP4 weights mapper.
Validates that checkpoint key names from our ModelOpt-quantized
DeepSeek-V4-Pro checkpoint are correctly mapped to vLLM model
parameter names.
This can run WITHOUT vLLM or CUDA — it only tests the mapper logic.
"""
import re
import sys
from typing import Optional
class WeightsMapper:
"""Simplified WeightsMapper for testing."""
def __init__(
self,
orig_to_new_prefix: Optional[dict] = None,
orig_to_new_regex: Optional[dict] = None,
orig_to_new_suffix: Optional[dict] = None,
orig_to_new_substr: Optional[dict] = None,
):
self.prefix_map = orig_to_new_prefix or {}
self.regex_map = orig_to_new_regex or {}
self.suffix_map = orig_to_new_suffix or {}
self.substr_map = orig_to_new_substr or {}
def map_name(self, name: str) -> str:
# 1. Prefix
for old, new in self.prefix_map.items():
if name.startswith(old):
name = new + name[len(old):]
break
# 2. Regex
for pattern, replacement in self.regex_map.items():
name = pattern.sub(replacement, name)
# 3. Suffix
for old, new in self.suffix_map.items():
if name.endswith(old):
name = name[: -len(old)] + new
break
# 4. Substr (ordered dict — specific before general)
for old, new in self.substr_map.items():
if old in name:
name = name.replace(old, new, 1)
return name
def _make_deepseek_v4_nvfp4_weights_mapper() -> WeightsMapper:
"""Exact copy of the mapper from deepseek_v4.py."""
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.",
}
suffix_renames = {}
substr_renames = {
".self_attn.compressor.": ".attn.mla_attn.compressor.",
".self_attn.q_a_proj.": ".attn.wq_a.",
".self_attn.kv_proj.": ".attn.wkv.",
".self_attn.q_b_proj.": ".attn.wq_b.",
".self_attn.o_a_proj.": ".attn.wo_a.",
".self_attn.o_b_proj.": ".attn.wo_b.",
".self_attn.q_a_norm.": ".attn.q_a_norm.",
".self_attn.kv_norm.": ".attn.kv_norm.",
".self_attn.sinks": ".attn.sinks",
".mlp.shared_experts.gate_proj.": ".ffn.shared_experts.w1.",
".mlp.shared_experts.up_proj.": ".ffn.shared_experts.w3.",
".mlp.shared_experts.down_proj.": ".ffn.shared_experts.down_proj.",
".mlp.": ".ffn.",
".self_attn.": ".attn.",
}
return WeightsMapper(
orig_to_new_prefix={
"layers.": "model.layers.",
"embed_tokens.": "model.embed_tokens.",
"norm.": "model.norm.",
"hc_head": "model.hc_head",
"mtp.": "model.mtp.",
},
orig_to_new_regex=expert_rename_regex,
orig_to_new_suffix=suffix_renames,
orig_to_new_substr=substr_renames,
)
TEST_CASES = [
# Embedding & top-level
("embed_tokens.weight", "model.embed_tokens.weight"),
("norm.weight", "model.norm.weight"),
("hc_head.hc_fn", "model.hc_head.hc_fn"),
("hc_head.hc_base", "model.hc_head.hc_base"),
("hc_head.hc_scale", "model.hc_head.hc_scale"),
("lm_head.weight", "lm_head.weight"),
# Attention — self_attn → attn
("layers.0.self_attn.q_a_proj.weight", "model.layers.0.attn.wq_a.weight"),
("layers.0.self_attn.q_a_proj.input_scale", "model.layers.0.attn.wq_a.input_scale"),
("layers.0.self_attn.kv_proj.weight", "model.layers.0.attn.wkv.weight"),
("layers.0.self_attn.kv_proj.input_scale", "model.layers.0.attn.wkv.input_scale"),
("layers.0.self_attn.q_b_proj.weight", "model.layers.0.attn.wq_b.weight"),
("layers.0.self_attn.o_a_proj.weight", "model.layers.0.attn.wo_a.weight"),
("layers.0.self_attn.o_b_proj.weight", "model.layers.0.attn.wo_b.weight"),
("layers.0.self_attn.o_b_proj.input_scale", "model.layers.0.attn.wo_b.input_scale"),
("layers.0.self_attn.q_a_norm.weight", "model.layers.0.attn.q_a_norm.weight"),
("layers.0.self_attn.kv_norm.weight", "model.layers.0.attn.kv_norm.weight"),
("layers.0.self_attn.sinks", "model.layers.0.attn.sinks"),
# Attention — compressor (inside self_attn)
("layers.0.self_attn.compressor.kv_proj.weight", "model.layers.0.attn.mla_attn.compressor.kv_proj.weight"),
("layers.0.self_attn.compressor.kv_proj.input_scale", "model.layers.0.attn.mla_attn.compressor.kv_proj.input_scale"),
("layers.0.self_attn.compressor.gate_proj.weight", "model.layers.0.attn.mla_attn.compressor.gate_proj.weight"),
("layers.0.self_attn.compressor.gate_proj.input_scale", "model.layers.0.attn.mla_attn.compressor.gate_proj.input_scale"),
("layers.0.self_attn.compressor.kv_norm.weight", "model.layers.0.attn.mla_attn.compressor.kv_norm.weight"),
("layers.0.self_attn.compressor.position_bias", "model.layers.0.attn.mla_attn.compressor.position_bias"),
# MoE gate
("layers.0.mlp.gate.tid2eid", "model.layers.0.ffn.gate.tid2eid"),
("layers.0.mlp.gate.weight", "model.layers.0.ffn.gate.weight"),
# Expert weights — gate_proj → w1, up_proj → w3, down_proj → w2
("layers.0.mlp.experts.0.gate_proj.weight", "model.layers.0.ffn.experts.0.w1.weight"),
("layers.0.mlp.experts.0.up_proj.weight", "model.layers.0.ffn.experts.0.w3.weight"),
("layers.0.mlp.experts.0.down_proj.weight", "model.layers.0.ffn.experts.0.w2.weight"),
("layers.0.mlp.experts.0.gate_proj.input_scale", "model.layers.0.ffn.experts.0.w1.input_scale"),
("layers.0.mlp.experts.0.gate_proj.weight_scale", "model.layers.0.ffn.experts.0.w1.weight_scale"),
("layers.0.mlp.experts.0.gate_proj.weight_scale_2", "model.layers.0.ffn.experts.0.w1.weight_scale_2"),
("layers.0.mlp.experts.255.down_proj.weight", "model.layers.0.ffn.experts.255.w2.weight"),
# Shared experts — gate_proj → w1, up_proj → w3, down_proj stays
("layers.0.mlp.shared_experts.gate_proj.weight", "model.layers.0.ffn.shared_experts.w1.weight"),
("layers.0.mlp.shared_experts.up_proj.weight", "model.layers.0.ffn.shared_experts.w3.weight"),
("layers.0.mlp.shared_experts.down_proj.weight", "model.layers.0.ffn.shared_experts.down_proj.weight"),
("layers.0.mlp.shared_experts.gate_proj.input_scale", "model.layers.0.ffn.shared_experts.w1.input_scale"),
("layers.0.mlp.shared_experts.down_proj.weight_scale", "model.layers.0.ffn.shared_experts.down_proj.weight_scale"),
("layers.0.mlp.shared_experts.down_proj.weight_scale_2", "model.layers.0.ffn.shared_experts.down_proj.weight_scale_2"),
# Layer norm
("layers.0.post_attention_layernorm.weight", "model.layers.0.post_attention_layernorm.weight"),
]
def main():
mapper = _make_deepseek_v4_nvfp4_weights_mapper()
passed = 0
failed = 0
for ckpt_key, expected in TEST_CASES:
result = mapper.map_name(ckpt_key)
if result == expected:
passed += 1
else:
failed += 1
print(f"FAIL: {ckpt_key}")
print(f" expected: {expected}")
print(f" got: {result}")
print(f"\n{passed} passed, {failed} failed")
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1648,12 +1648,14 @@ def _make_deepseek_v4_nvfp4_weights_mapper() -> WeightsMapper:
}
suffix_renames = {
"head.weight": "lm_head.weight",
"embed.weight": "embed_tokens.weight",
# The NVFP4 checkpoint already uses lm_head / embed_tokens directly,
# no suffix renames needed (unlike the MXFP4 upstream format).
}
# NOTE: specific renames MUST come before general ones (applied in order)
substr_renames = {
# Attention compressor (MUST come before .self_attn. → .attn.)
".self_attn.compressor.": ".attn.mla_attn.compressor.",
# Attention projections (specific before .self_attn. → .attn.)
".self_attn.q_a_proj.": ".attn.wq_a.",
".self_attn.kv_proj.": ".attn.wkv.",
@@ -1663,7 +1665,6 @@ def _make_deepseek_v4_nvfp4_weights_mapper() -> WeightsMapper:
".self_attn.q_a_norm.": ".attn.q_a_norm.",
".self_attn.kv_norm.": ".attn.kv_norm.",
".self_attn.sinks": ".attn.sinks",
".attn.compressor.": ".attn.mla_attn.compressor.",
# Shared expert projections (specific before .mlp. → .ffn.)
".mlp.shared_experts.gate_proj.": ".ffn.shared_experts.w1.",
".mlp.shared_experts.up_proj.": ".ffn.shared_experts.w3.",
@@ -1675,7 +1676,11 @@ def _make_deepseek_v4_nvfp4_weights_mapper() -> WeightsMapper:
return WeightsMapper(
orig_to_new_prefix={
# Keep model. prefix — model params use it (e.g. model.embed_tokens.weight)
"layers.": "model.layers.",
"embed_tokens.": "model.embed_tokens.",
"norm.": "model.norm.",
"hc_head": "model.hc_head",
"mtp.": "model.mtp.",
},
orig_to_new_regex=expert_rename_regex,
orig_to_new_suffix=suffix_renames,