Summary
compressed_tensors (or llmcompressor's save_pretrained_wrapper that wraps it) treats the recipe's ignore= list inconsistently between calibration phase and save phase:
-
At calibration time: ignore= correctly excludes matched modules from the GPTQModifier / QuantizationMixin pipeline. The matched modules have no Hessian built, no quantize_weight call, no observer attached. This is verified — under our recipe, the MTP block's compress_module_list subgraph was empty (0 modules processed for MTP) during calibration.
-
At save time: the saved artifact has matched modules in W4A16 packed form anyway. They were re-quantized at save via what looks like an RTN-style pass that consults targets= but NOT ignore=. The Compressing model: 0/N progress bar in save_pretrained_wrapper (llmcompressor/transformers/compression/compressed_tensors_utils.py:79) compresses any module matched by targets=, regardless of whether it's also in ignore=.
Reproduction
Recipe used in our DSv4-Flash W4A16+BF16-MTP run:
GPTQModifier(
config_groups={
"experts": QuantizationScheme(
targets=[r"re:.*mlp\.experts\.\d+\.(gate_proj|up_proj|down_proj)$"],
**W4A16,
),
"attention": QuantizationScheme(
targets=[r"re:.*self_attn\.(q_a_proj|q_b_proj|kv_proj|o_a_proj|o_b_proj)$"],
**FP8_BLOCK,
),
},
ignore=[
"lm_head",
r"re:.*mtp\..*", # ← INTENDED: exclude MTP draft head from quantization
],
)
targets= matches both model.layers.X.mlp.experts.* (intended) and model.mtp.0.mlp.experts.* (unintended — the .* prefix doesn't anchor). ignore=r"re:.*mtp\..*" is supposed to exclude all MTP paths.
Result at calibration: subgraph for MTP block had 0 modules — ignore worked.
Result at save:
import json
with open("model.safetensors.index.json") as f:
wm = json.load(f)["weight_map"]
print([k for k in wm if "mtp" in k and "weight_packed" in k][:3])
# → ['model.mtp.0.mlp.experts.0.down_proj.weight_packed',
# 'model.mtp.0.mlp.experts.0.gate_proj.weight_packed',
# 'model.mtp.0.mlp.experts.0.up_proj.weight_packed']
768 MTP expert linears in weight_packed (int4) format. The MTP block was not supposed to be quantized.
Workaround
Anchor targets= patterns more specifically so MTP paths don't match in the first place:
targets=[r"re:^model\.layers\.\d+\.mlp\.experts\.\d+\.(gate_proj|up_proj|down_proj)$"]
With anchoring, save-time RTN doesn't match MTP paths → MTP modules ship in their loaded BF16 form. We confirmed this fixes the artifact on a subsequent smoke run.
Proposed fix
Either:
-
Make save-time compression consult ignore= — the cleaner fix. save_pretrained_wrapper should filter the compression target set through the recipe's ignore= list the same way GPTQModifier does at calibration. This restores symmetric semantics between calibration and save.
-
Document the asymmetry loudly — if (1) is intentional, the docs should say so. The current behavior is surprising: users naturally assume ignore= means "ignore everywhere," not "ignore at GPTQ but quantize at save."
(1) is what most users would expect. Mixed-precision recipes (e.g., DSv4 with BF16 MTP draft head + W4A16 main experts — see our model card draft for the design rationale) only work cleanly if ignore= is symmetric.
Why this matters
Speculative-decoding architectures (DSv4 MTP, Eagle, Medusa, etc.) need the draft head at higher precision than the main model — quantization noise in the draft directly kills acceptance rate. The natural way to express this in a recipe is:
targets=[main-model-MoE-pattern],
ignore=["draft-head-pattern"],
Without ignore= being honored at save, this pattern silently fails — the draft head gets quantized despite the user's intent, and the speedup the architecture is supposed to provide is silently degraded. The failure is invisible without inspecting weight_map of the saved artifact.
Related
vllm-project/llm-compressor#2735 — DSv4 example drops MTP layer (the root issue this PR's fix enables)
vllm-project/llm-compressor#2739 — extend ARCH_TO_2D_MAPPINGS for MTP paths (makes MTP visible to the pipeline so ignore= becomes meaningful)
huggingface/transformers#46127 — DeepseekV4NextNPredictor class so MTP loads at all
Summary
compressed_tensors(orllmcompressor'ssave_pretrained_wrapperthat wraps it) treats the recipe'signore=list inconsistently between calibration phase and save phase:At calibration time:
ignore=correctly excludes matched modules from the GPTQModifier / QuantizationMixin pipeline. The matched modules have no Hessian built, no quantize_weight call, no observer attached. This is verified — under our recipe, the MTP block'scompress_module_listsubgraph was empty (0 modules processed for MTP) during calibration.At save time: the saved artifact has matched modules in W4A16 packed form anyway. They were re-quantized at save via what looks like an RTN-style pass that consults
targets=but NOTignore=. TheCompressing model: 0/Nprogress bar insave_pretrained_wrapper(llmcompressor/transformers/compression/compressed_tensors_utils.py:79) compresses any module matched bytargets=, regardless of whether it's also inignore=.Reproduction
Recipe used in our DSv4-Flash W4A16+BF16-MTP run:
targets=matches bothmodel.layers.X.mlp.experts.*(intended) andmodel.mtp.0.mlp.experts.*(unintended — the.*prefix doesn't anchor).ignore=r"re:.*mtp\..*"is supposed to exclude all MTP paths.Result at calibration: subgraph for MTP block had 0 modules — ignore worked.
Result at save:
768 MTP expert linears in
weight_packed(int4) format. The MTP block was not supposed to be quantized.Workaround
Anchor
targets=patterns more specifically so MTP paths don't match in the first place:With anchoring, save-time RTN doesn't match MTP paths → MTP modules ship in their loaded BF16 form. We confirmed this fixes the artifact on a subsequent smoke run.
Proposed fix
Either:
Make save-time compression consult
ignore=— the cleaner fix.save_pretrained_wrappershould filter the compression target set through the recipe'signore=list the same way GPTQModifier does at calibration. This restores symmetric semantics between calibration and save.Document the asymmetry loudly — if (1) is intentional, the docs should say so. The current behavior is surprising: users naturally assume
ignore=means "ignore everywhere," not "ignore at GPTQ but quantize at save."(1) is what most users would expect. Mixed-precision recipes (e.g., DSv4 with BF16 MTP draft head + W4A16 main experts — see our model card draft for the design rationale) only work cleanly if
ignore=is symmetric.Why this matters
Speculative-decoding architectures (DSv4 MTP, Eagle, Medusa, etc.) need the draft head at higher precision than the main model — quantization noise in the draft directly kills acceptance rate. The natural way to express this in a recipe is:
Without
ignore=being honored at save, this pattern silently fails — the draft head gets quantized despite the user's intent, and the speedup the architecture is supposed to provide is silently degraded. The failure is invisible without inspectingweight_mapof the saved artifact.Related
vllm-project/llm-compressor#2735— DSv4 example drops MTP layer (the root issue this PR's fix enables)vllm-project/llm-compressor#2739— extendARCH_TO_2D_MAPPINGSfor MTP paths (makes MTP visible to the pipeline soignore=becomes meaningful)huggingface/transformers#46127—DeepseekV4NextNPredictorclass so MTP loads at all