Repository navigation
Conversation
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>
|
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. |
Contributor
|
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
Comment |
--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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Type of change: new feature
Adds
--kl_divergencetoexamples/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: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 defaultwikitext2data (reproduced byte for byte as llama.cpp'swiki.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.ptscores againstref.ptand skips the unquantized pass when the file exists; otherwise it writesref.ptfrom 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.saveand read withtorch.load(weights_only=True, mmap=True). Scoring against a reference built for another vocabulary raises an error.Data.
--kl_divergence_datatakeswikitext2(default), a UTF-8 text file, or a ModelOpt dataset name such asnemotron-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_datasetandlayerwise.export_dir. The last leaves the model in export form, so it can't be scored.Usage
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 arellama-perplexity --kl-divergenceon the same files and text: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:test_hf_ptq_args.pygains a test that--kl_divergence_referenceimplies--kl_divergence. 49 tests pass across both files.Other checks.
Salesforce/wikitextwikitext-2-raw-v1test split, with blank rows written as" \n", equals llama.cpp'swiki.test.rawbyte for byte (1,288,556 characters).Before your PR is "Ready for review"
CONTRIBUTING.md: N/A (the scoring follows llama.cpp's documented method; no code copied)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_dataoption.🤖 Generated with Claude Code