Skip to content

[OMNIML-6370] [3/5] Finalize AutoQuantize selections with sequential layerwise calibration - #3152

Draft
meenchen wants to merge 3 commits into
weimingc/autoquant-format-parallelfrom
weimingc/autoquant-finalize
Draft

meenchen wants to merge 3 commits into
weimingc/autoquant-format-parallelfrom
weimingc/autoquant-finalize

Conversation

@meenchen

@meenchen meenchen commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: New feature

[3/5] of the sub-4-bit AutoQuantize series (after #3151).

Series (merge in order): #3154 (merged) → #3151 → #3152 → #3153 → #3155

Sequential (layerwise) GPTQ makes each layer's result depend on the formats chosen for the layers before it, which a per-layer search cannot know. The search therefore scores candidates with their own local algorithms, and the frozen per-layer formats are calibrated afterwards on the source weights.

  • mtq.get_auto_quantize_config(state, algorithm=...) returns the frozen config with the given calibration algorithm instead of always max.
  • AutoQuantize recipes take auto_quantize.finalize_algorithm. hf_ptq.py then stops after saving the search checkpoint; rerunning the same command finds the scores, re-solves for the recipe's budget, and quantizes the freshly loaded source model with the frozen formats, the finalize algorithm and the recipe's KV-cache config before exporting.
  • hf_ptq.py --auto_quantize_format_shard I/N and --auto_quantize_merge_shards run the format shards and merge them.

Usage

for i in 0 1 2 3 4; do  # one GPU each
  CUDA_VISIBLE_DEVICES=$i python hf_ptq.py --recipe <aq-recipe> ... \
    --auto_quantize_checkpoint ck/shard$i.pth --auto_quantize_format_shard $i/5 &
done; wait
python hf_ptq.py --recipe <aq-recipe> ... --auto_quantize_checkpoint ck/merged.pth \
  --auto_quantize_merge_shards ck/shard0.pth,...,ck/shard4.pth --export_path out

Testing

  • End to end on GB300: Qwen3.6-35B-A3B, 3 shards → merge → sequential-GPTQ finalize (40 layers) → export.
  • Unit: KL forward_step handles the early-stopped forward of layerwise candidate calibration (fails without the fix); recipe and hf_ptq helper suites pass (test_hf_ptq_args.py, test_autoquant_utils.py, test_example_utils.py: 191 tests).
  • _run_auto_quantize_recipe reads the shard flags with defaults: the KV-cache AutoQuantize stage tests build args without them and failed with AttributeError (5 tests) before this.
  • Finalizing memory-maps the merged checkpoint on the CPU and reads only its scores, instead of loading every searched format's calibration onto the GPU it was saved from.
  • Measured effect of the finalize stage (held-out KL vs BF16, 128 docs): uniform IQ2_XXS / XS / S 0.150→0.118, 0.111→0.089, 0.081→0.064; AutoQuant 2.75 / 3.0 / 3.25 bits 0.130→0.101, 0.095→0.076, 0.078→0.065.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅
  • New PIP dependency: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅

🤖 Generated with Claude Code

@copy-pr-bot

copy-pr-bot Bot commented Oct 30, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

@meenchen meenchen changed the title [OMNIML-6370] [4/5] Finalize AutoQuantize selections with sequential layerwise calibration [OMNIML-6370] [3/5] Finalize AutoQuantize selections with sequential layerwise calibration Oct 30, 2026
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch from 86b5475 to 70670ad Compare November 2, 2026 23:24
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch 2 times, most recently from 0f7188c to 1a4a8c3 Compare November 3, 2026 20:10
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch 2 times, most recently from 1691197 to a1bd87a Compare November 3, 2026 22:33
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch from 1a4a8c3 to 9b37689 Compare November 3, 2026 22:33
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch from a1bd87a to e23cc89 Compare November 4, 2026 05:22
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch from 9b37689 to 8c5691a Compare November 4, 2026 05:22
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch from e23cc89 to dbf9e33 Compare November 4, 2026 07:06
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch from 8c5691a to 8d96d20 Compare November 4, 2026 07:06
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch from dbf9e33 to c8c8ae5 Compare November 4, 2026 16:32
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch from 8d96d20 to 2792a02 Compare November 4, 2026 16:32
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch from c8c8ae5 to fc9558b Compare November 4, 2026 18:52
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch 2 times, most recently from e7e3533 to e9e1401 Compare November 4, 2026 20:20
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch 2 times, most recently from 45e81d8 to eb5da0d Compare November 4, 2026 21:50
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch 2 times, most recently from c1b62a6 to 6e451f1 Compare November 5, 2026 17:49
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch 2 times, most recently from 60303de to f8c7b37 Compare November 5, 2026 22:36
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch from 6e451f1 to e170688 Compare November 5, 2026 22:36
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch from f8c7b37 to 7275f38 Compare November 5, 2026 23:57
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch from e170688 to 0be766b Compare November 5, 2026 23:57
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch from 7275f38 to fe62ae5 Compare November 6, 2026 00:09
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch 2 times, most recently from 8176ab3 to ab91a8e Compare November 6, 2026 00:24
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch 2 times, most recently from 07e2459 to d8d6753 Compare November 6, 2026 00:58
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch from ab91a8e to 9c31102 Compare November 6, 2026 00:58
meenchen added a commit that referenced this pull request Nov 6, 2026
…from the source weights (#3154)

### What does this PR do?

Type of change: Bug fix / new feature

[1/5] of the sub-4-bit AutoQuantize series (merge in order).

Series (merge in order): #3154 → #3151 → #3152 → #3153 → #3155

GPTQ and imatrix overwrite the weights they quantize. `auto_quantize`
rejected both, and accepting them as-is would let one candidate's
calibration change the weights every later candidate — and the
unquantized scoring reference — sees.

AutoQuantize now snapshots (to CPU) only the weights a GPTQ/imatrix
candidate's calibration rewrites: the parameters its enabled weight
quantizers cover, and for imatrix alone only those imatrix encodes (not
Q8_0; a fused projection when any of its experts is). After such a
candidate calibrates, its GGML quantizers replay the codes it pinned
while the source weights are restored for the next candidate; the
snapshot is freed before sensitivity scoring. After the solve, the
selected candidates' weights are left as their codes decode, as a plain
GPTQ run leaves them, and no candidate is left replaying. GGML fake
quant now returns exactly the reconstruction export decodes, so scoring
and the exported model see the same weights (BF16 included).

Refused before the model is converted: any candidate with an enabled
layerwise `export_dir`, empty included, whether its options are a dict
or a `LayerwiseConfig`, whose export would leave the model in export
form before scoring (`hf_ptq.py` already refused it for AutoQuantize
recipes); and, in a search with a weight-rewriting candidate, any
candidate whose layerwise `checkpoint_dir` resume restores the layers'
weights into new Parameters the source-weight restore cannot reach
(`calib_mutates_weights=False` resumes are accepted). Refused before any
calibration: a weight-rewriting candidate whose weight quantizer is not
a single GGML quantizer or rotates the weight, or whose algorithm list
runs anything besides GPTQ, imatrix and max (AWQ or SmoothQuant would
transform the weight GPTQ encodes); and meta or DTensor weights it would
rewrite, weights stored packed or scaled (a native FP8 checkpoint's), or
ones whose storage another parameter shares (tied weights, or views of
them). These checks need the converted quantizers to know which weights
a candidate rewrites, so `auto_quantize` hands back the unconverted
modules, and the configs that conversion changed (including nested
sub-configs), when the search fails before calibrating any candidate,
and the same model can be searched again. Only a search with a
weight-rewriting candidate saves the modules for this, and it drops them
once these checks pass, so calibration and scoring never hold the
weights conversion replaces (HF Conv1D's transposed copies); other
searches save nothing. A failure during calibration restores the source
weights and drops the modes that calibration added. `auto_quantize`
hands back the pre-quant-scale folding policy on every exit, including a
failure during scoring. Weights a calibration leaves alone (imatrix on
Q8_0) simply quantize the source weight. NVFP4/FP8-only searches take no
snapshot and never slice fused-expert weights after the solve.
`auto_quantize` now accepts `gptq`, `imatrix` and algorithm lists such
as `[gptq, imatrix]`.

#### Why snapshot and replay

Every candidate after a GPTQ/imatrix calibration, and the unquantized
scoring reference, must see the source weights. The alternatives cost
more:
- **Copy the model, or its state dict:** this holds every weight, about
70 GB for Qwen3.6-35B-A3B in BF16. The snapshot holds only the weights a
rewriting candidate quantizes, on the host, and is freed before scoring.
- **Make GPTQ/imatrix leave weights alone:** this would change
calibrators that `mtq.quantize` shares, and the chosen codes would still
have to be kept somewhere.

The GGML quantizer already pins those codes for export, so replaying the
pin makes the scored, selected and exported weights the same codes.

#### Reading order

1. `_AutoQuantizeBaseSearcher.before_search`: snapshot
(`_snapshot_source_weights`; its checks are `_rewritten_weight` and
`_check_restorable`), then for each candidate calibrate, replay its pins
and restore the source weights.
2. `_write_selected_codes_to_weights` (after the solve), and
`GGMLFormat._pinned_rows` / `full_pin` (the replay).
3. `auto_quantize`: the window in which a failure hands back the
unconverted modules, and the folding policy it hands back on every exit.

### Usage

```python
iq2_gptq = {
    "quant_cfg": [{"quantizer_name": "*", "enable": False},
                  {"quantizer_name": "*weight_quantizer",
                   "cfg": {"num_bits": "iq2_s", "block_sizes": {-1: 256}, "backend": "ggml", "effective_bits": 2.5625}}],
    "algorithm": [{"method": "gptq", "block_size": 256, "perc_damp": 0.3, "importance_weighted": True}, {"method": "imatrix"}],
}
model, state = mtq.auto_quantize(model, constraints={"effective_bits": 3.0},
                                 quantization_formats=[(iq2_gptq, "IQ2_S_GPTQ"), mtq.NVFP4_DEFAULT_CFG], ...)
```

### Testing

- New tests in `tests/unit/torch/quantization/test_ggml_gptq.py`,
`test_autoquant.py`, `plugins/test_fused_experts.py` and
`tests/gpu/torch/quantization/test_ggml_gptq_cuda.py`. I reverted each
fix in place and checked that its test fails. They cover:
  - other candidates' scores are unaffected by GPTQ;
- mixed selection: a GPTQ layer next to a BF16 layer, a GGML entry
replacing an earlier INT4 entry, and imatrix on Q8_0;
- the snapshot holds only rewritten weights (a fused `down_proj` that is
disabled or Q8_0), takes a whole fused `gate_up_proj` with one Q8_0
expert, and a BF16 selection hands back the source weights;
- refusals (non-GGML, tied, an aliased view, a rotated GPTQ or imatrix
target, AWQ then GPTQ, a layerwise `checkpoint_dir`, native FP8 weights
(GPU), and fused MoE whose root, nested and experts' configs conversion
changes) hand back the unconverted model and its configs, which searches
again;
- calibration failures (GPTQ rewrite then failure, `[gptq, layerwise
imatrix]` failing midway) restore the weights, the folding policy and
the mode list;
  - the folding policy comes back after a scoring failure;
  - exact BF16 replay;
  - no fused-expert indexing after a search without pins;
  - an `algorithm: []` candidate keeps the saved state restorable.
- Layerwise directories (`test_huggingface.py`), on a tiny Llama with
discoverable decoder layers: an FP8 candidate with an `export_dir`, with
an empty `export_dir`, and with a `checkpoint_dir` beside a Q8_0 GPTQ
candidate, each given as a dict and as a `LayerwiseConfig`, is refused
before conversion, leaving the modules, the weights and the working
directory untouched, and the same model searches again. Without the
refusals, the layers are exported and scoring fails on the packed
weights, or the max candidate's checkpoint is written.
- Conv1D weight lifetime (`test_huggingface.py`): an INT8 search saves
no module state, and a Q8_0 GPTQ search drops it before calibration, so
the weights conversion replaces are dead in every calibration and
scoring forward.
- Regression check against the base commit (RTX 6000 Ada, BF16, FP8 +
INT4 searches): a 24-layer GPT-2 (Conv1D), a 16-layer Llama and a
fused-MoE Qwen3, with gradient scoring and with KL for Llama, give the
same scores, selections and output logits, the same search time (within
4%) and the same memory. On GPT-2 the weights held after conversion are
0 MiB, against 576 MiB before 5acdcc4.
- Unit suites `tests/unit/torch/quantization` + `tests/unit/recipe` on
CPU (16 xdist workers): 2151 passed. The rest fail the same way on
`main` at this branch's base on the test host:
  - diffusers lacks `q_bmm_quantizer` (4 failures);
  - `test_diffusers_wan_conv3d` fails to collect (16 errors);
- one to four `test_nemotron_3_5_super_vl_autoquant_recipe` cases fail
under xdist and pass serially.
- `pre-commit run mypy --all-files` passes; ruff check and ruff format
pass on the touched files.
- Rollback cost, paid only by searches with a GPTQ/imatrix candidate:
saving the module attributes takes 2 MiB and 0.16 s on Qwen3.6-35B-A3B
(837 modules), and 69 MiB on a 24.6k-module non-fused MoE.
- GPU (RTX 6000 Ada): `tests/gpu/torch/quantization -k "auto_quant or
autoquant or ggml or gptq or imatrix"` 57 passed, including
`test_fsdp2_autoquant.py` and the native FP8 refusal. The deepspeed and
onnx packages on the host stop 3 files from collecting.
- Stack restacked on this; the same unit suites at [5/5]: 2148 passed,
with the same failures.
- Qwen3.6-35B-A3B on GB300: searches over IQ2_{XXS,XS,S} (imatrix and
local GPTQ), NVFP4, FP8, BF16; see PR 5 for results.

### Before your PR is "*Ready for review*"

- Is this change backward compatible?: ✅
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: ✅
- Did you update Changelog?: ✅
- Did you get Claude approval on this PR?: review comments addressed
(007a287, 83df05c, 848bd95, 357fb38, 59ea14d, 609ed1c,
9f76d8d, 7a21227, 7e028ac, 5acdcc4, f75da89, a9f070f,
1808e13)

### Additional Information

Supersedes #3082 … #3057 and #3113 … #3120 with a smaller design (~720
source lines across the series).

Synced with main by a merge (99f82c8), which keeps the commits the
review threads cite. #3197 made the IQ formats re-encode a weight under
its pinned scales and importance ahead of the pinned-rows lookup that
honours `PIN_OVERRIDES_WEIGHT`, so during a search a GPTQ or imatrix
candidate would have quantized the source weight under its scales
instead of replaying its codes; b7be410 puts the override first there
too (caught by
`test_a_replayed_pin_fake_quantizes_to_exactly_its_decoding_in_bf16`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
  * AutoQuantize supports GPTQ and iMatrix candidates with GGML formats.
* Selected candidates retain their calibrated quantized results in the
returned model.
* **Bug Fixes**
* Candidate scores are no longer affected by weight changes made while
evaluating other candidates.
* Source weights and quantization settings are restored when candidate
evaluation fails; unsupported weight-rewrite configurations are
rejected.
* Failed conversions before calibration begins restore the model’s prior
state.
* Fake quantization better preserves reconstructed values in narrow data
types.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch from 9c31102 to ed6b499 Compare November 6, 2026 06:13
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch from d8d6753 to 80c3352 Compare November 6, 2026 06:13
@meenchen
meenchen force-pushed the weimingc/autoquant-format-parallel branch from ed6b499 to 81bfb30 Compare November 6, 2026 21:37
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch 3 times, most recently from 41c495f to bf155a4 Compare November 7, 2026 02:15
meenchen and others added 3 commits November 6, 2026 19:54
A layerwise algorithm such as sequential GPTQ makes each layer's result
depend on the formats chosen for the layers before it, which a per-layer
search cannot know. The search therefore scores candidates with their own
(local) algorithms, and the selected per-layer formats are calibrated
afterwards on the source weights.

- mtq.get_auto_quantize_config takes the algorithm of the config it
  returns instead of always returning max.
- AutoQuantize recipes take auto_quantize.finalize_algorithm. hf_ptq then
  stops after saving the search checkpoint; rerunning the same command
  finds the scores, re-solves for the recipe's budget, and quantizes the
  freshly loaded source model with the frozen formats, the finalize
  algorithm and the recipe's KV-cache config before exporting.
- hf_ptq takes --auto_quantize_format_shard INDEX/COUNT and
  --auto_quantize_merge_shards to run the format shards in parallel and
  merge them into --auto_quantize_checkpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com>
Finalizing needs the score table, but loading the checkpoint brought every
searched format's calibration into memory first, on the GPUs it was saved from.
Memory-map it on the CPU so the calibration stays on disk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com>
_run_auto_quantize_recipe read --auto_quantize_merge_shards and
--auto_quantize_format_shard as attributes, so callers that build args without
them, as the KV-cache AutoQuantize stage tests do, failed with AttributeError,
before the recipe refusals those tests check. Default them to off, as
autoquant_utils does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com>
@meenchen
meenchen force-pushed the weimingc/autoquant-finalize branch from bf155a4 to 8653170 Compare November 7, 2026 05:32

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant