Repository navigation
Conversation
|
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. |
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
|
shengliangxu
left a comment
There was a problem hiding this comment.
Bot review (gpt-6.1-sol) — DM the bot to share feedback.
Changes requested: checkpoint-only finalization drops the KL context and uses the wrong solver.
Needs action:
- Persist and restore KL context for offline re-solving in
algorithms.py; extendget_auto_quantize_configand HF finalization coverage (see inline). - Reject checkpoint reuse and shard merges with incompatible scoring contexts in
algorithms.py, with mismatch tests (see inline). - Explain in the PR body why context-KL is preferred over the existing Aumann–Shapley path scorer in
_auto_quantize_shapley.py.
No action needed:
- Existing test edits are justified: default cases remain covered, context calibration is shared, and KL now accepts
contextoptions.
| We use binary search to minimize the max(per-layer score) while meeting the constraint. | ||
| A scoring context adds the same base damage to every candidate of every group, so a | ||
| threshold on raw scores is meaningless; those scores are ranked by their sum instead. | ||
| """ |
There was a problem hiding this comment.
Bot comment.
[P1] Preserve the context for checkpoint-only re-solves
This dispatch depends on self.config['context'], but context is absent from default_state_dict, so neither the returned search state nor saved checkpoints record it. _resolve_best_recipe() constructs a KL searcher with default_search_config, making mtq.get_auto_quantize_config(state, constraints=...) use the threshold solver instead of this LP branch.
This also breaks the advertised finalization workflow: examples/hf_ptq/hf_ptq.py::_run_auto_quantize_recipe() calls that re-solve whenever finalize_algorithm resumes, even for the original budget. Rerunning the same command can therefore change the context-aware assignment before sequential GPTQ runs.
Please persist the scoring context and restore it into the offline searcher's config, retaining None semantics for legacy KL states. Extend the existing re-solve/finalization coverage to compare against the original LP assignment for both contexts, including merged checkpoints.
| """A searcher for AutoQuantize algorithm that uses KL-Divergence loss based score estimation.""" | ||
|
|
||
| method_name = "kl_div" | ||
| method_config_keys = frozenset({"context"}) |
There was a problem hiding this comment.
Bot comment.
[P2] Validate the scoring context before reusing scores
Context changes what every raw score means, but _AutoQuantizeBaseSearcher.before_search() only checks the method, costs, formats and resolved search setup before skipping scoring for a populated checkpoint. Reusing a default-KL checkpoint with context='lowest', or changing 'lowest' to 1, silently applies the new context's solver to old-context measurements.
Likewise, _SHARD_INVARIANT_STATE does not include context. Because searched-format scores are disjoint and only shard 0 measures BF16 in context, merging shards scored under different contexts can succeed without detecting incompatible measurements.
Please record and validate a context/scoring signature on resume and in shard invariants; the existing Aumann–Shapley scoring-signature mechanism provides a precedent. Add changed-context resume and mixed-context merge cases.
86b5475 to
70670ad
Compare
1fd2215 to
1658a6f
Compare
1691197 to
a1bd87a
Compare
0986299 to
bfecf53
Compare
e23cc89 to
dbf9e33
Compare
bfecf53 to
117544b
Compare
dbf9e33 to
c8c8ae5
Compare
117544b to
24168a1
Compare
c8c8ae5 to
fc9558b
Compare
24168a1 to
4b2ee1b
Compare
fc9558b to
45e81d8
Compare
4b2ee1b to
1cdfb9e
Compare
45e81d8 to
eb5da0d
Compare
52ad620 to
92d35d5
Compare
60303de to
f8c7b37
Compare
4097d58 to
fbf446b
Compare
7275f38 to
fe62ae5
Compare
177fad8 to
2c167dc
Compare
fe62ae5 to
07e2459
Compare
2c167dc to
e78d3cc
Compare
07e2459 to
d8d6753
Compare
…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>
d8d6753 to
80c3352
Compare
5e21a27 to
a8d97a1
Compare
67cd622 to
41c495f
Compare
a8d97a1 to
15b44f5
Compare
41c495f to
bf155a4
Compare
15b44f5 to
92431b3
Compare
KL-divergence AutoQuantize scores each (group, format) with every other
group unquantized. At sub-4-bit budgets that misjudges which layers
matter: on Qwen3.6-35B-A3B it starves late layers whose damage only
shows once every layer is quantized, and the selection loses to uniform
IQ2 at matched bits.
method={'method': 'kl_div', 'context': 'lowest'} scores each candidate,
BF16 included, with every other searched group at its lowest-bit
format. All scores of a group then share the context's base damage, so
the linear program over their sum ranks assignments by marginal gain at
that operating point; the max-score threshold search is only used
without a context. Format shards all calibrate the context format, and
shard 0 alone scores BF16 so the merge stays exact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com>
get_auto_quantize_config(state, constraints) rebuilt the searcher with default options, so a context-scored KL state was re-solved with the max-score threshold solver instead of the LP the scores need. Save the options that change what a score means in the search state, check them on resume and shard merge, and apply them on re-solve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com>
92431b3 to
c543ea0
Compare
bf155a4 to
8653170
Compare
What does this PR do?
Type of change: New feature
[4/5] of the sub-4-bit AutoQuantize series (after #3152).
Series (merge in order): #3154 → #3151 → #3152 → #3153 → #3155
At sub-4-bit budgets, scoring each (group, format) with every other group unquantized misjudges which layers matter. On Qwen3.6-35B-A3B both the gradient and KL scorers rate the first 10 layers ~30× more sensitive than layers 20–39, starve the late layers with IQ2_XXS, and lose to uniform IQ2 at every budget.
method={"method": "kl_div", "context": "lowest" | k}scores every candidate, BF16 included, with every other searched group at its lowest-bit (k-th lowest) format. All scores of a group then share the context's base damage, so the linear program over their sum ranks assignments by marginal gain at that operating point (the max-score threshold search is kept for the no-context default). Format shards all calibrate the context format; shard 0 alone scores BF16 so the merge stays exact. The recipe schema acceptsmethod_options: {context: ...}forkl_div.The search state records the scoring context (
scoring_config). Resume and shard merge refuse a checkpoint scored with a different context, andget_auto_quantize_config(state, constraints)re-solves with it. Before this, a re-solve rebuilt the searcher with default options, so a context-scored state went through the threshold solver instead of the LP.Testing
context="lowest"andcontext=1; asserts BF16 is measured in context and that the budget is spent (fails with the threshold solver).get_auto_quantize_config(state, {"effective_bits": 6.0})and asserts it matches the unsharded search (fails without thescoring_configfix). AutoQuant + Shapley unit suites pass.Before your PR is "Ready for review"
🤖 Generated with Claude Code