Skip to content

ignore= is honored at calibration but NOT at save_pretrained_wrapper — modules excluded from quantization still get RTN-packed at save #712

Description

@pasta-paul

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:

  1. 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.

  2. 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#46127DeepseekV4NextNPredictor class so MTP loads at all

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions