Skip to content

Report KL divergence from the unquantized model in hf_ptq - #2662

Draft
cjluo-nv wants to merge 2 commits into
mainfrom
chenjiel/hf-ptq-kl-divergence
Draft

cjluo-nv wants to merge 2 commits into
mainfrom
chenjiel/hf-ptq-kl-divergence

Conversation

@cjluo-nv

@cjluo-nv cjluo-nv commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: new feature

Adds --kl_divergence to examples/hf_ptq/hf_ptq.py, plus a way to keep and build its references offline. Right after quantization, before export, it reports how far the quantized model has drifted from the unquantized one:

  • the mean KL divergence from the unquantized model's next-token distribution;
  • both perplexities;
  • how often the most likely next token is unchanged.

Scoring copies llama.cpp's llama-perplexity --kl-divergence: consecutive 512-token chunks, 100 by default, with only the second half of each chunk scored. BOS replaces the first token only if the tokenizer prepends one. With the default wikitext2 data (reproduced byte for byte as llama.cpp's wiki.test.raw), the numbers are directly comparable with llama.cpp's for GGUF quants of the same model.

Why: we're adding GGML IQ formats, and next imatrix and block-GPTQ for them. Those need a quality signal that doesn't require exporting a GGUF and running llama.cpp.

Two ways to get the reference.

  • In-run (default): the unquantized log-probabilities come from the same model before it is quantized, so it is loaded once. Disabling quantizers afterwards wouldn't work as a substitute, because GPTQ-style algorithms modify weights in place. The reference is held in host memory as float16, chunks × seq_len / 2 × vocabulary values: about 12.7 GB for 100 × 512 tokens of a 248k vocabulary.

  • Saved or offline:

    • --kl_divergence_reference ref.pt scores against ref.pt and skips the unquantized pass when the file exists; otherwise it writes ref.pt from this run.
    • python examples/hf_ptq/kl_divergence.py --pyt_ckpt_path <model> --output ref.pt --data <data> builds one without quantizing, using hf_ptq's own model loader.

    References are written with torch.save and read with torch.load(weights_only=True, mmap=True). Scoring against a reference built for another vocabulary raises an error.

Data. --kl_divergence_data takes wikitext2 (default), a UTF-8 text file, or a ModelOpt dataset name such as nemotron-post-training-dataset-v2. Datasets are chat-formatted when both the dataset and the tokenizer support it, so instruct models can be scored on chat-formatted text as well as prose.

Not supported: --use_fsdp2, --low_memory_mode, --specdec_offline_dataset and layerwise.export_dir. The last leaves the model in export form, so it can't be scored.

Usage

# Score in the same run (one model load)
python examples/hf_ptq/hf_ptq.py --pyt_ckpt_path Qwen/Qwen3.8-27B --recipe general/ptq/iq1_s \
  --export_path /tmp/iq1_s --kl_divergence

# Build a reference offline on other data, then reuse it across PTQ runs
python examples/hf_ptq/kl_divergence.py --pyt_ckpt_path Qwen/Qwen3.8-27B --output ref.pt \
  --data nemotron-post-training-dataset-v2
python examples/hf_ptq/hf_ptq.py --pyt_ckpt_path Qwen/Qwen3.8-27B --recipe general/ptq/iq1_s \
  --export_path /tmp/iq1_s --kl_divergence_reference ref.pt
KL divergence vs. the unquantized model over 25500 scored tokens:
  Mean PPL(Q)    :  11.389264 ± 0.182879
  Mean PPL(base) :   6.746027 ± 0.103227
  Mean KLD       :   0.612284 ± 0.005046
  Same top p     :     67.373 ± 0.294 %

Testing

Cross-check against llama.cpp on Qwen3.8-27B. I loaded the BF16 model in transformers and collected the reference with this module: 297,193 tokens, 100 × 255 scored positions, 64 s on one GPU. I then overwrote every quantized weight with the exact dequantized values of two llama.cpp GGUF quants and scored them with kl_divergence. The GGUF-to-HF tensor placement, including the converter's row and column reorders, was recovered by exact BF16 fingerprints against the BF16 GGUF. llama.cpp's numbers are llama-perplexity --kl-divergence on the same files and text:

This PR (PyTorch) llama.cpp
BF16 perplexity 6.7460 6.7411
BF16 vs itself: KL / same top 0.000001 / 100 % —
UD-IQ1_S-layout quant: KL / perplexity / same top 0.6123 ± 0.0050 / 11.389 / 67.37 % 0.6150 ± 0.0050 / 11.390 / 67.07 %
Block-GPTQ IQ quant: KL / perplexity / same top 0.4544 ± 0.0044 / 9.848 / 71.49 % 0.4569 ± 0.0044 / 9.867 / 71.33 %

KL agrees to about 0.4%, within one standard error, and perplexity to 0.2%. The remaining differences are consistent with compute precision: BF16 PyTorch versus llama.cpp's CUDA kernels.

Unit tests. tests/examples/hf_ptq/test_kl_divergence.py (new, CPU, tiny Qwen3) covers:

  • chunking and the scored half;
  • BOS substitution;
  • zero divergence for an unchanged model;
  • equality with a direct KL and perplexity computation on a perturbed model;
  • the vocabulary-mismatch error and the reference file round trip;
  • text-file tokenization without special tokens;
  • drawing dataset samples until there are enough tokens.

test_hf_ptq_args.py gains a test that --kl_divergence_reference implies --kl_divergence. 49 tests pass across both files.

Other checks.

  • The Hugging Face Salesforce/wikitext wikitext-2-raw-v1 test split, with blank rows written as " \n", equals llama.cpp's wiki.test.raw byte for byte (1,288,556 characters).
  • pre-commit is clean on all changed files.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ (new opt-in flags)
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A (the scoring follows llama.cpp's documented method; no code copied)
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅
  • Did you get Claude approval on this PR?: ❌ (draft)

Additional Information

Overlaps with #2661, which scores BF16-generated continuations with a second model copy. The two could be consolidated, for example by adding #2661's generated-continuation data as another --kl_divergence_data option.

🤖 Generated with Claude Code

Add --kl_divergence to examples/hf_ptq/hf_ptq.py. It scores the unquantized
model on wikitext-2 test before quantization and the quantized model after,
the way llama.cpp's llama-perplexity --kl-divergence does: consecutive
512-token chunks, with the second half of each chunk scored. It prints the
mean KL divergence, both perplexities and the same-top-token rate, so the
numbers can be compared directly with llama.cpp's for GGUF quants.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Oct 5, 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 5, 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
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

--kl_divergence_reference PATH scores against a saved reference and skips
the unquantized pass, or writes PATH from this run's unquantized model when
it does not exist yet. kl_divergence.py doubles as a script that builds a
reference from an unquantized model without quantizing it.
--kl_divergence_data replaces --kl_divergence_text_file and also accepts a
ModelOpt dataset name, chat-formatted where supported, so references can be
built on data other than wikitext-2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
cjluo-nv added a commit that referenced this pull request Oct 6, 2026
### What does this PR do?

Type of change: new feature

Makes GPTQ (`algorithm: {method: gptq}`) work for the GGML formats:
`iq1_s`, `iq1_m`, `iq2_xxs`, `iq2_xs`, `iq2_s` and `q8_0`.

GPTQ's update rounds one column at a time. The GGML formats can't do
that: they encode each block of consecutive weights jointly (256 for the
IQ formats, 32 for `q8_0`). Calling the encoder once per column would
also mean up to 17k full-matrix encodes per tensor.

This PR adds a GPTQ helper for the `ggml` backend, registered through
the existing `register_gptq_helper` hook. It works one GGML block at a
time:

1. encode the block's columns with the format's own encoder;
2. fold its error `E` into the later columns through the inverse-Hessian
block, `E @ inv(U_gg) @ U_g,rest`;
3. update the rest of the GPTQ `block_size` window immediately and later
columns lazily, as standard GPTQ does.

The update is `gptq_group_update`. With a group of one column it is
exactly the existing column-wise update, which a test checks.

**Export writes the codes GPTQ chose.** After GPTQ the weight equals the
decoding of its codes, and the GGML encoders do not return those codes
when they encode that decoding again: 0% identical blocks on Qwen3.8-27B
MLP weights for every IQ format, which is a second round of quantization
error. GPTQ therefore pins its payload as a buffer on the weight
quantizer (`pin_packed_weight`).
- Fake quant and `pack()` use the pinned payload whenever the weight is
exactly its decoding, and encode afresh otherwise.
- Being a buffer, the pin survives accelerate offload,
`mto.save`/`restore` and layerwise checkpoint resume. Under Accelerate
*buffer* offload, a buffer first registered inside the weight-access
context is now added to the hook's `weights_map`
(`plugins/accelerate.py`).
- The pin is keyed by its format (`_ggml_pinned_<format>`), so it is
never handed to another format's decoder.
- The Megatron exporter packs through the module's weight quantizer too.
Its gate/up, expert, row-split and QKV head-gather slices get the
matching pinned rows: the lookup also accepts a row subset of the pinned
weight, exactly checked.
- The existing identity cache stays in front as the per-forward fast
path.

`GPTQHelper` also no longer queries GPU memory for modules that aren't
on a CUDA device; that made GPTQ fail for CPU-resident models on a
machine with a GPU. `block_size` must be a multiple of the format's
block size; anything else raises.

**The built-in IQ recipes now use it.** `general/ptq/iq1_s`, `iq1_m`,
`iq2_xxs`, `iq2_xs` and `iq2_s`, and their presets, change in two ways:
- they quantize only the MLP/MoE weights (`*mlp*` and
`*block_sparse_moe*` weight quantizers), keeping attention and the other
linears in BF16;
- they calibrate with layerwise GPTQ, `block_size: 256` and `perc_damp:
0.3`.

So these recipes now need calibration data. Fused-MoE experts
(`_QuantFusedExperts`) are still quantized, but without the GPTQ update:
`gptq()` only visits 2-D quantized linears.

This is the first PR in the IQ GPTQ + imatrix plan. A prototype around
llama.cpp's own IQ quantizers showed block-GPTQ is the largest single
quality lever for the IQ formats; weighted encoders and an imatrix
follow in separate PRs.

### Usage

```bash
python examples/hf_ptq/hf_ptq.py --pyt_ckpt_path <model> --recipe general/ptq/iq2_xxs --export_path <dir>
```

The preset behind it, for a custom recipe:

```yaml
quantize:
  algorithm:
    method: gptq
    block_size: 256     # a multiple of the format's block size
    perc_damp: 0.3      # heavier than GPTQ's usual 0.01: it helped markedly at IQ bit widths in the prototype
    layerwise:
      enable: true
  quant_cfg:
    - $import: base_disable_all
    - quantizer_name: '*mlp*weight_quantizer'
      cfg:
        $import: iq2_xxs
    - quantizer_name: '*block_sparse_moe*weight_quantizer'
      cfg:
        $import: iq2_xxs
    - $import: default_disabled_quantizers
```

### Testing

**Qwen3.8-27B, IQ2_XXS.** All runs used `examples/hf_ptq/hf_ptq.py` with
its default calibration data (`cnn_nemotron_v2_mix`, 1024 × 512, batch
size 4). They are scored by the KL divergence from the BF16 model on
wikitext-2 test, 100 × 512-token chunks, scored as llama.cpp's
`llama-perplexity --kl-divergence` does, in memory before export. The
scoring code is #2662, run locally.

| IQ2_XXS on | GPTQ | Mean KL divergence | Perplexity (BF16: 6.746) |
Same top token |
|---|---|---|---|---|
| every eligible linear (the recipe before this PR) | no | 1.151 ± 0.008
| 19.61 | 57.4 % |
| every eligible linear | yes | 0.426 ± 0.004 | 9.91 | 74.1 % |
| MLP only | no | 0.450 ± 0.005 | 10.12 | 73.8 % |
| **MLP only (the new `general/ptq/iq2_xxs`)** | **yes** | **0.298 ±
0.004** | **8.48** | **78.0 %** |

GPTQ took 12–17 s per layer, about 22 minutes for the 64 layers on one
GPU.

Note on that run: on a shared GPU, the model left GPU 6 77% full, and
`GPTQHelper` then keeps Hessians on CPU (the existing 65% threshold).
That made Hessian accumulation about 300 s per layer. For this
measurement I raised the threshold locally, which changes only where the
Hessians live, not the result. This PR doesn't change that behaviour.

**Unit tests** (`tests/unit/torch/quantization/test_ggml_gptq.py`, CPU;
they pass with and without a GPU visible):
- with one-column groups, `gptq_group_update` equals the existing
`gptq_blockwise_update`;
- with 4-column groups, it equals a direct, non-lazy computation of `E @
inv(U_gg) @ U_g,rest`;
- windows that would split a group are rejected;
- `{"method": "gptq"}` runs end to end through `mtq.quantize`. That
covers the helper's registration, the payload layout, the forward,
`pack()`, and a lower calibration-weighted error than plain IQ1_S;
- the pin outlives the weight tensor (same values in a new tensor),
survives `mto.save`/`restore`, and is dropped once the weight changes;
- the pin survives a real `AlignDevicesHook(offload=True,
offload_buffers=True)` GPTQ run;
- the pin is ignored once the quantizer switches format, both live and
after restore;
- Megatron export writes the pinned rows from the real
`_gated_mlp_slicing`, `_grouped_mlp_slicing`, `_qkv_slicing` and
`_gated_delta_net_slicing`, given a GPTQ-pinned quantizer.

`tests/gpu_megatron/torch/export/test_unified_export_megatron.py -k iq`
passes in the CI image (`nvcr.io/nvidia/nemo:26.08`, megatron-core
0.19.1): 45 passed.

**GPU tests** (`tests/gpu/torch/quantization/test_ggml_gptq_cuda.py`)
run GPTQ through `mtq.quantize` with the CUDA IQ1_S encoder. They check
that:
- the weight equals the decoded pinned payload;
- forward and `pack()` both use that payload;
- the calibration-weighted error is below plain IQ1_S;
- a `block_size` of 128 raises.

**Recipes:**
- `tests/unit/recipe/test_presets.py` now checks the IQ recipes' new
contract: IQ packing on the MLP/MoE weight quantizers, and GPTQ with a
`block_size` that is a whole number of IQ blocks. All 473 recipe and
GGML GPTQ unit tests pass.
- The five `general/ptq/iq*` hf_ptq end-to-end tests
(`test_llm_ptq.py::test_ptq_llama`, TinyLlama) pass with GPTQ, in 2 min
41 s together.
- The exported checkpoint packs only `mlp.{gate,up,down}_proj`, and
attention is listed in `exclude_modules`.

**Existing suites still pass:** `test_ggml_backend.py`, `test_gptq.py`
(unit and GPU), `test_iq_formats.py` and `test_iq_formats_cuda.py`.

pre-commit is clean.

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

- Is this change backward compatible?: ✅ for every released recipe and
API.
- The `general/ptq/iq*` recipes change behaviour: they now quantize only
MLP/MoE weights and need calibration data. They are new in the
unreleased 0.48 cycle (CHANGELOG, 0.48 New Features), so no released
behaviour changes; the CHANGELOG entry states the new contract.
  - Non-GPTQ GGML caching and export are unchanged.
- 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](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
✅
- Did you get Claude approval on this PR?: ❌

### Additional Information

Related: #2662 (KL-divergence scoring in hf_ptq, used for the numbers
above).

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

---------

Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cjluo-nv added a commit that referenced this pull request Oct 9, 2026
…#2686)

### What does this PR do?

Type of change: new feature

This is the second step of the IQ GPTQ + imatrix plan, after #2665: the
IQ2 search now weights each input column by how much the layer's outputs
depend on it.

**Weighted encoders.** `quantize_iq2_xxs`, `quantize_iq2_xs` and
`quantize_iq2_s` take an optional per-input-column `importance` `w`.
They then choose codes that minimise `Σ w·(|x| − s·q)²` instead of the
plain squared error, which is how llama.cpp uses an imatrix.
- The weighting covers the local-scale choice, the grid entry and the
even-parity sign flip. The super-block scale predictor stays unweighted.
- The CUDA kernel gets the weighting through a `kWeighted` template
flag. The unweighted kernel compiles as before.
- With uniform importance, the payload is identical to the unweighted
one.
- `importance` must be finite and non-negative, with one entry per input
column; anything else raises.
- The new `GGMLFormat.weighted` field marks the formats that accept it.

**GPTQ feeds them, when the recipe asks.** A new GPTQ option,
`importance_weighted` (default `false`), turns it on. `GGMLGPTQHelper`
then gives each GGML block of a weighted format its slice of
`sqrt(diag(H))`. Here `diag(H) = E[x_j²]` is llama.cpp's imatrix
statistic, floored at 1e-4 of its mean. GPTQ already accumulates `H`, so
this needs no extra calibration pass. The built-in `general/ptq/iq2_*`
presets set the option, so their output is the weighted one. Formats
without a weighted search (IQ1_*, NVFP4, INT4) ignore it, so one recipe
can set it for mixed formats.

**Why the switch is on the algorithm, not the format.** The right weight
depends on what supplies the statistic. Under GPTQ, later columns absorb
part of each block's error, and the square root measured best. A future
imatrix-only calibration, with no error compensation, would want the raw
`E[x²]` and would be its own `method`. The encoder's `importance=`
argument and `GGMLFormat.weighted` are shared by both.

**Why the square root.** The raw diagonal is heavy-tailed, so a few
outlier columns dominate the search. It made KL divergence worse than no
weighting on IQ2_XXS and IQ2_XS (table below). The square root keeps the
column ranking with half the log-range, and it beats both on all three
formats.

IQ1_S and IQ1_M stay unweighted for now. Their encoders are the next
step.

### Usage

The built-in IQ2 recipes turn it on:

```bash
python examples/hf_ptq/hf_ptq.py --pyt_ckpt_path <model> --recipe general/ptq/iq2_xxs --export_path <dir>
```

In a custom recipe:

```yaml
algorithm:
  method: gptq
  block_size: 256
  perc_damp: 0.3
  importance_weighted: true   # weight the IQ2 codebook search by sqrt(diag(H)); other formats ignore it
  layerwise:
    enable: true
```

Calling the encoder directly, for example with an imatrix from
elsewhere:

```python
from modelopt.torch.quantization.ggml.iq2_xxs import quantize_iq2_xxs

packed, shape = quantize_iq2_xxs(weight, importance=imatrix)  # imatrix: [in_features], finite, >= 0
```

### Testing

**Qwen3.8-27B, MLP-only IQ2 with GPTQ**, using the default
`general/ptq/iq2_*` recipes.
- Calibration: `hf_ptq.py` defaults (`cnn_nemotron_v2_mix`, 1024 × 512).
- Scoring: KL divergence from BF16 on wikitext-2 test, 100 × 512-token
chunks, scored as llama.cpp's `llama-perplexity --kl-divergence` does.
This is the same setup as #2665, with the #2662 code run locally.
- The only difference between rows is the importance passed to the
search.

Mean KL divergence for each format and each choice of importance:

| Format | Unweighted (main) | `diag(H)` | **`sqrt(diag(H))` (this PR)**
| sqrt − unweighted, paired (chunks where sqrt is lower) |
|---|---|---|---|---|
| IQ2_XXS | 0.307 | 0.319 | **0.287** | −0.0197 ± 0.0025 (80/100) |
| IQ2_XS | 0.214 | 0.226 | **0.203** | −0.0106 ± 0.0020 (71/100) |
| IQ2_S | 0.159 | 0.152 | **0.139** | −0.0195 ± 0.0015 (91/100) |

| Format | Perplexity (BF16: 6.746): unweighted / `diag(H)` / sqrt |
Same top token: unweighted / `diag(H)` / sqrt |
|---|---|---|
| IQ2_XXS | 8.57 / 8.51 / **8.32** | 77.2 / 75.8 / 77.1 % |
| IQ2_XS | 7.90 / 7.87 / **7.84** | 81.3 / 80.4 / 81.3 % |
| IQ2_S | 7.74 / 7.56 / **7.50** | 83.8 / 83.8 / **84.6** % |

- **The square root beats the raw diagonal on every format.** KL
divergence is lower by 0.031, 0.023 and 0.012, and lower in 91, 88 and
82 of the 100 chunks. The raw diagonal is worse than no weighting on
IQ2_XXS and IQ2_XS.
- **The exponent is fixed rather than exposed** because the best one is
the same on all three formats.
- **Paired differences** compare both arms on the same tokens. The
standard error is over the 100 chunks.
- **Same-top-token agreement** is unchanged on IQ2_XXS and IQ2_XS and
improves on IQ2_S.
- **Every unweighted arm ran with uniform importance.** That gives
byte-identical codes: the tests check it, and so did a run on
Qwen3.8-27B's layer-10 MLP weights.
- **Runs are deterministic.** Rerunning an arm reproduces its numbers
exactly.
- **Earlier baseline:** an earlier version of this description compared
IQ2_XXS against 0.298. That baseline came from a pre-merge #2665 commit;
every row here now runs on current main.

GPTQ's per-layer relative error (the `Relative MSE error` it logs),
weighted ÷ unweighted:

| Format | Importance | `gate_proj` | `up_proj` | `down_proj` |
|---|---|---|---|---|
| IQ2_XXS | `diag(H)` | 1.00 (38/64 layers lower) | 1.01 (20/64) | 0.91
(46/64) |
| IQ2_XXS | `sqrt(diag(H))` | 0.97 (58/64) | 0.98 (58/64) | 0.90 (64/64)
|
| IQ2_XS | `diag(H)` | 1.00 (36/64) | 1.01 (32/64) | 0.94 (44/64) |
| IQ2_XS | `sqrt(diag(H))` | 0.97 (59/64) | 0.97 (60/64) | 0.90 (64/64)
|
| IQ2_S | `diag(H)` | 1.00 (38/64) | 1.01 (13/64) | 0.95 (38/64) |
| IQ2_S | `sqrt(diag(H))` | 0.97 (54/64) | 0.98 (53/64) | 0.93 (63/64) |

These are geometric means over the 64 layers. With layerwise
calibration, each run's later layers see different inputs, so read these
as indicative.

The raw diagonal barely lowers GPTQ's own error on `gate_proj` and
`up_proj`, while the square root lowers it on every projection. So the
raw diagonal is a worse stand-in for GPTQ's per-block objective, not a
case of over-fitting the calibration data.

The plan came from a prototype that used llama.cpp's own quantizers on
the UD-IQ1_S layout. In it, Hessian-diagonal weighting stacked with
block-GPTQ (wikitext-2 KL divergence):

| | Unweighted | Weighted by our `diag(H)` |
|---|---|---|
| no GPTQ | 2.360 | 0.619 |
| GPTQ | 0.584 | 0.482 |

**Unit tests**, on CPU. They pass with and without a GPU visible, 122
passed.
- `tests/unit/torch/quantization/test_iq_formats.py`, for each weighted
format:
  - uniform importance reproduces the unweighted payload exactly;
- non-uniform importance lowers the importance-weighted error below 0.75
× that of the unweighted search;
- invalid importance raises: wrong length, integer dtype, negative, or
NaN.
- `tests/unit/torch/quantization/test_ggml_gptq.py`:
  - GPTQ hands an IQ2 format `sqrt(diag(H))` column-sliced per block;
  - an unweighted format (`iq1_s`) gets nothing.

**GPU tests**, 92 passed:
`tests/gpu/torch/quantization/test_iq_formats_cuda.py`,
`test_ggml_gptq_cuda.py`, `test_gptq.py`, `test_iq1_s_cuda.py` and
`test_iq2_xs_cuda.py`.
- The weighted CUDA packer matches the PyTorch weighted encoder. At most
1 block in 1,000 differs, which is float tie-breaking, and the weighted
errors are equal.
- With uniform importance, the CUDA packer reproduces the unweighted
payload.
- The CUDA packer rejects an importance that doesn't tile the blocks.

`pre-commit` passes.

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

Make sure you read and follow [Contributor
guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md)
and your commits are signed (`git commit -s -S`).

Make sure you read and follow the [Security Best
Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors)
(e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(...,
weights_only=False)`, `pickle`, etc.).

- Is this change backward compatible?: ✅ The API is compatible, since
`importance` is optional. The codes GPTQ picks for the IQ2 recipes
change, which is the point of this PR.
- 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](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
✅
- Did you get Claude approval on this PR?: ❌ Not yet.

### Additional Information

Follows #2665 (block-GPTQ for the GGML formats). Next: weighted IQ1_S /
IQ1_M encoders.

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


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

* **New Features**
* IQ2_S, IQ2_XS, and IQ2_XXS quantization now support optional
per-column importance weights to prioritize accuracy for more important
values.
* GPTQ can derive weights from the square root of the Hessian diagonal.
Built-in IQ2 recipes enable this option by default; quantization without
weights remains supported.
* **Bug Fixes**
* Invalid importance weights, including unsupported types, shapes,
negative values, or NaNs, are rejected.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

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