Repository navigation
Description
System / environment
- Images (both reproduce, empirically verified 2026-07-09):
nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc15.post1and1.3.0rc20(NVIDIA Release 26.02, PyTorch 2.11.0a0). Note the DeepSeek-V4 model file was refactored between the two: inrc15.post1the loader lives in_torch/models/modeling_deepseekv4.py; inrc20it was folded into_torch/models/modeling_deepseekv3.py(there is nomodeling_deepseekv4.pyin rc20). The bug survives the refactor — see line refs for both below. - Hardware: single node, 8× B300. Failure is during weight loading (per-rank, at
Loading weights 0%), so it is hardware/parallelism independent — reproduces with TP4 and reproduces identically under both monolithictrtllm-serve serveand PD-disaggregated serving. - Checkpoint:
nvidia/DeepSeek-V4-Pro-NVFP4— the ModelOpt experts-only NVFP4 re-quantization (hf_quant_config.json→producer.version: "dsv4-nvfp4-experts").
Reproduce
trtllm-serve serve /path/to/DeepSeek-V4-Pro-NVFP4 \
--tp_size 4 --host 0.0.0.0 --port 8001
# crashes before serving, during model weight loading — no tokenizer/parser flags needed
# to reproduce. (In rc20 the `--tool_parser deepseek_v4` value was also removed; the valid
# set is deepseek_v3/deepseek_v31/deepseek_v32 — orthogonal to this bug.)Actual behavior
File ".../tensorrt_llm/_torch/models/modeling_deepseekv4.py", line 1030, in load_weights
_copy_deepseek_v4_fused_a_weight_scale(module, fused_a, fused_a_scale)
File ".../tensorrt_llm/_torch/models/modeling_deepseekv4.py", line 240, in _copy_deepseek_v4_fused_a_weight_scale
if tuple(module.weight_scale.shape) == tuple(fused_a_scale.shape):
File ".../torch/nn/modules/module.py", line 1967, in __getattr__
raise AttributeError(...)
AttributeError: 'Linear' object has no attribute 'weight_scale'
Root cause (traced through the code)
This checkpoint is MIXED_PRECISION: only the routed MoE experts are NVFP4; the attention projections are stored as native DeepSeek block-scale FP8 — every layers.N.attn.{wq_a,wkv,wo_a,wo_b,wq_b} leaf carries both .weight (fp8) and .scale. Its hf_quant_config.json:
{
"producer": {"name": "modelopt", "version": "dsv4-nvfp4-experts"},
"quantization": {
"quant_algo": "MIXED_PRECISION",
"quantized_layers": { "layers.0.ffn.experts": {"quant_algo": "NVFP4", "group_size": 16}, "...": "... (61 layers)" },
"exclude_modules": ["*.attn.*", "*.ffn.shared_experts.*", "head", "mtp.*"]
}
}- The attention Linears (
q_a_proj/kv_a_proj_with_mqa, and MTPe_proj/h_proj) are built withmodel_config.get_quant_config()called with noname(e.g.modeling_deepseekv4.py:2177/2185/2197/2208).ModelConfig.get_quant_config(name=None)returns the globalQuantConfig(model_config.py:249-250). - The per-layer table (
quantized_layers→per_layer_quant_configs) is consulted only when anameis passed, and raisesValueErroron a miss (model_config.py:252-254). In practice it is used only for the MoE experts (which are wired through a separateoverride_quant_config,modeling_deepseekv4.py:1467/1504). So nohf_quant_config.json/quant_cfg.jsonentry can reach the attention modules. - The global algo is
MIXED_PRECISION+exclude_modules: "*.attn.*"(matched viafnmatch,quantization/quantize.py:51) → the attn Linear is built unquantized (BF16), with noweight_scale. - In
load_weights,nvfp4_fused_aisFalse(attn weights are FP8, not FP4), so the code takes the else branch (~modeling_deepseekv4.py:1015-1032). That branch findskv_a_proj_with_mqa.weight_scale_invin the checkpoint and calls_copy_deepseek_v4_fused_a_weight_scale(module, fused_a, fused_a_scale), which dereferencesmodule.weight_scale→AttributeError.
Note the same branch also does module.weight.data.copy_(fused_a) where fused_a is the FP8 weight and module.weight is BF16 — i.e. even if the weight_scale access were guarded, the FP8 weight would be loaded into a BF16 module without applying the block scale, silently producing wrong values. The branch implicitly assumes the fused_a module is FP8_BLOCK_SCALES.
Expectation / suggested fix
When the fused_a target module is unquantized (excluded → BF16) but the checkpoint stores it as block-scale FP8, the else branch should dequantize (weight_dequant, already present at modeling_deepseekv4.py:97-147) the FP8 fused_a with its fused_a_scale and copy the resulting BF16 into module.weight, instead of copying the scale onto a non-existent module.weight_scale. (Equivalently: guard on hasattr(module, "weight_scale") and branch to dequant.)
What we ruled out
- Image version — reproduced on both
1.3.0rc15.post1and1.3.0rc20(empirically). rc20 refactored the V4 loader intomodeling_deepseekv3.pyand reworkedmodel_config.py, but still setsexclude_modulesfrom the HF quant config and crashes identically (rc20 crash sitemodeling_deepseekv3.py:~557). - PD-disaggregated vs monolithic — identical crash (both workers run
trtllm-serve serve; loading is the same code path). - Removing
*.attn.*fromexclude_modules— still crashes; under aMIXED_PRECISIONglobal there is no per-module algo for the now-un-excluded attn, so it is not built as FP8. - Missing
quant_cfg.json— neither the checkpoint nor the ModelOpt extended file exists in the repo; adding per-layer entries there cannot help, since attn queriesget_quant_config()without a name (point 2 above).
Additional note
The native DeepSeek checkpoint deepseek-ai/DeepSeek-V4-Pro (FP8 + MXFP4 routed experts; config-level quantization_config: {quant_method: fp8, weight_block_size: [128,128]}, no exclude_modules) loads and serves fine — verified end-to-end on rc15.post1, TP4 on 4× B300: weights load to 100%, Application startup complete, and chat completions generate correctly. Because the global algo is FP8_BLOCK_SCALES (not MIXED_PRECISION) and attn is not excluded, every attn Linear is built as FP8_BLOCK_SCALES and has a weight_scale. So TensorRT-LLM can serve DeepSeek-V4-Pro on this hardware; the crash is specific to the ModelOpt experts-only NVFP4 packaging (nvidia/DeepSeek-V4-Pro-NVFP4), where attn is exclude_modules'd (→ built BF16) yet physically stored as FP8 with a scale.
Activity
waynehacking8 commentedon Jul 11, 2026
I tried to reproduce this on current main (8bcec87) with a tiny synthetic replica of the checkpoint (same hf_quant_config.json, tensor names, fp8-e4m3 weights + ue8m0 block scales, experts as ModelOpt NVFP4 quadruples) on an RTX PRO 6000, and main now fails earlier than your trace: DeepseekV4DecoderLayer hits the "MIXED_PRECISION is ambiguous" assert (modeling_deepseekv4.py:1762, added with #15414) during model construction, before load_weights ever runs. So on main this packaging needs layer-level MIXED_PRECISION resolution at construction first (the FIXME right above that assert acknowledges the gap), and then your fused_a fix -- the unguarded weight_scale deref plus raw-fp8 copy you diagnosed is still there in the else branch (modeling_deepseekv4.py:1032, same pattern at modeling_deepseekv3.py:557 for the V3/V32 arches). I have the guard+weight_dequant half ready on a branch (https://github.com/waynehacking8/TensorRT-LLM/tree/wayne/fix-16196-fused-a-dequant) if maintainers want it as a follow-on; caveats on my repro: main python over the 1.3.0rc20 wheel binaries, synthetic tiny checkpoint, not the real weights.
d3nb commentedon Jul 11, 2026
Thanks @waynehacking8 — I tested your fused-A dequant fix against the real
nvidia/DeepSeek-V4-Pro-NVFP4weights (851 GB, 64 shards) on 4× B300, TP4, and it clears the loading crash.Setup: since the real checkpoint's
deepseek_v4model type isn't recognized by the transformers in therc20wheel (ValueError: ... does not recognize this architectureatconfig_utils.py:569, before load), I ran on1.3.0rc15.post1(which loadsdeepseek_v4fine — the officialdeepseek-ai/DeepSeek-V4-ProFP8+MXFP4 checkpoint loads + serves + generates end-to-end on the exact same setup, as a control). I ported your guard torc15.post1'smodeling_deepseekv4.pyelse-branch (line 1030), i.e. the samehasattr(module, "weight_scale")→weight_dequant(...)change.Result: the
'Linear' object has no attribute 'weight_scale'crash is gone — weights load to 100%. So the fused-A guard+dequant is confirmed on real weights, not just the synthetic replica. 🎉But a second, downstream blocker then surfaces during executor init (MLA attention warmup), and I believe it's the same root cause showing up at runtime:
_q_branchtakes the CuTe-DSL path becauseq_b_proj.weight.dtype == torch.bfloat16(attn isexclude_modules'd → built unquantized BF16), while_is_fused_q_fp8_quant_enabled()is stillTrue— exactly the "future config drift" the assert's comment warns about. So dequantizing fused-A to BF16 makes the model load, but leaves the attention modules BF16 while the rest of the MLA path still assumes FP8 q-quant.This lines up with your point that the real fix is the construction-time, layer-level MIXED_PRECISION resolution (the
MIXED_PRECISION is ambiguousassert + the FIXME above it): if the excluded-but-FP8 attn modules were built asFP8_BLOCK_SCALESat construction, they'd carry aweight_scale(so the fused-A else-branch never triggers) and the runtime_q_branchFP8-q-quant contract would hold. The guard+dequant is a good defensive fallback, but for this ModelOpt experts-only NVFP4 packaging it isn't sufficient on its own — the attn modules really need to end up FP8, not BF16.Happy to test a construction-time resolution patch on the real weights + B300 if you or the maintainers put one up. For reference, my env:
rc15.post1image, realnvidia/DeepSeek-V4-Pro-NVFP4, TP4 on 4× B300,moe_config.backend=TRTLLM.waynehacking8 commentedon Jul 11, 2026
Thanks for running the real thing -- that closes the loading half on actual weights, and your downstream _q_branch assert points at the same place my construction repro does. I think the underlying semantic bug is that this packaging's exclude_modules means "left at the base model's precision", and DeepSeek-V4-Pro's base attn IS block-scale FP8 (the repack's embedded quantization_config still says quant_method=fp8 with weight_block_size [128,128] alongside the NVFP4 moe_quant_algo), while TRT-LLM's exclude handling builds excluded modules as unquantized BF16 -- so the construction-time resolution should build the excluded attn Linears as FP8_BLOCK_SCALES, which gives them a weight_scale (the fused-A else branch then never fires) and keeps the MLA fused-q-quant contract intact, with the layer-level MIXED_PRECISION assert (modeling_deepseekv4.py:1762 on main) resolved the same way. I'll draft that construction-time patch on my branch and will gladly take you up on the B300 offer once it's up.
[None][fix] Resolve MIXED_PRECISION construction for experts-only NVF…
waynehacking8 commentedon Jul 11, 2026
@d3nb the construction-time resolution is up on the same branch (https://github.com/waynehacking8/TensorRT-LLM/tree/wayne/fix-16196-fused-a-dequant, commit 3972f5e, on top of the loader guard). It does three things: derives an FP8_BLOCK_SCALES config from the checkpoint's embedded base quantization_config and builds the excluded attention / shared experts / MTP e_proj+h_proj with it (matching the native FP8 flow), resolves the decoder-layer algo from the per-layer quantized_layers table under MIXED_PRECISION (so the ambiguity assert goes away and the MoE stays NVFP4), and accepts the checkpoint-native layers.N.ffn.experts key form that these repacks emit. On my tiny replica: construction passes, weights load to 100%, the fused_a fp8 payload is bit-exact vs the checkpoint, and q_b_proj / shared experts end up FP8 with weight_scale present -- which should also satisfy the _q_branch fused-q-quant contract you hit. Not covered: runtime on real weights (your B300 loop), and MTP spec decoding (the MTP routed experts are stored at the base model's MXFP4 -- worth leaving spec-dec off for the first run).
d3nb commentedon Jul 11, 2026
Update from the B300 real-weights side, @waynehacking8 — the construction-time resolution works end-to-end on the real
nvidia/DeepSeek-V4-Pro-NVFP4, and the MTP path you flagged is now working too.Construction-time resolution — validated on real weights + B300
I implemented the same idea you described (independently, on
1.3.0rc15.post1inmodel_config.pyrather thanmodeling_deepseekv4.py, since that's the wheel that loadsdeepseek_v4configs for us): whenquant_algo == MIXED_PRECISIONand the checkpoint's basequantization_configisquant_method=fp8 / weight_block_size=[128,128], resolve the global toFP8_BLOCK_SCALES(reusing the native-fp8 path) so the excluded attention / shared-expert projections build as FP8 with aweight_scale, keep the per-layer NVFP4 experts, and keep the genuinely-unquantized modules (head,mtp) excluded.Result on 4× B300, TP4,
moe_config.backend=TRTLLM:Application startup complete, chat completions generate correctly ("我是DeepSeek…");q_b_proj/ shared experts end up FP8 withweight_scalepresent — so the runtime_q_branchfused-FP8-q-quant assert I hit earlier is gone, exactly as you predicted.So both halves (fused-A dequant guard, and construction-time FP8 resolution) are now confirmed on the actual 851 GB checkpoint, not just synthetic replicas.
The MTP piece: dense experts are NVFP4, MTP experts are MXFP4 (same checkpoint)
You were right that MTP needed separate handling — the reason turned out to be a per-region layout split inside one checkpoint:
layers.0..60.ffn.experts→ dtypeU8= NVFP4 (ModelOpt re-quantized these);mtp.0.ffn.experts→ dtypeI8= MXFP4 (left at the base model's precision).The routed-MoE layout resolver detects a single layout (from
layers.0.ffn.experts.0.w1.weight= NVFP4) and applies it to all MoE layers including the MTP layer, so the MTP experts get NVFP4 and die infused_moe/quantization.pyload_quant_scales. Detecting the MTP expert dtype separately (mtp.0.ffn.experts.0.w1.weight→I8→W4A8_MXFP4_*) and covering the MTP layer indices with that config fixes it.With that, on 4× B300:
num_nextn_predict_layers): loads + serves + generates. Log:MTP routed experts layout=mxfp4 over 1 layer(s)/dense routed experts NVFP4.model.layers.{num_hidden+i}are covered too. (One wrinkle:spec_configisNoneinfrom_pretrainedin our path, so_get_mtp_num_layersreturns 0 there — I currently cover a small generous range of MTP indices as a stopgap; the clean version would thread the model-sidenum_nextn_predict_layersin, or extend coverage where the MTP layers are duplicated.)Our
model_config.pydiff (rc15.post1) is small — happy to share it or fold the MTP-layout piece into your branch, whichever you prefer. We can re-run any revision of your branch against the real weights + B300 (both MTP off and MTP=1/3). For reference, env:1.3.0rc15.post1,nvidia/DeepSeek-V4-Pro-NVFP4, TP4 on 4× B300,moe_config.backend=TRTLLM.d3nb commentedon Jul 11, 2026
@waynehacking8 I put the MTP-layout piece up as a PR against your branch so it stays consolidated: waynehacking8#1 (single change to
_set_deepseek_v4_routed_moe_quant_config, +38/-6, on top of your 3972f5e). It detects the MTP routed-expert dtype separately (mtp.0.ffn.experts.0.w1.weight→ I8/MXFP4) so the MTP layers stop inheriting the dense NVFP4 config. Validated on the realnvidia/DeepSeek-V4-Pro-NVFP4+ 4× B300 (MTP=1 and MTP=3, accept_len ≈ 2.86). Feel free to squash/reword or fold it in however you like.[None][fix] Dequantize DeepSeek-V4 fused A when module is built unqua…
[None][fix] Resolve MIXED_PRECISION construction for experts-only NVF…
[None][fix] Dequantize DeepSeek-V4 fused A when module is built unqua…
[NVIDIA#16196][fix] Describe the MTP layout case without its history