Skip to content

quantize: add option to automatically choose optimal quant types to reach a file/bpw target size at lowest error - #15550

Open
EAddario wants to merge 335 commits into
ggml-org:masterfrom
EAddario:quantize
Open

EAddario wants to merge 335 commits into
ggml-org:masterfrom
EAddario:quantize

Conversation

@EAddario

@EAddario EAddario commented Aug 24, 2025 •

Copy link
Copy Markdown
Contributor

This PR adds target_bpw_type(), a function to determine an optimal per-tensor quantization mix to achieve a user-specified total file size (i.e., --target-size 1.5g) or a global bits-per-weight (bpw) target (i.e., --target-bpw 4.5678).

An evaluation of 34 models across 238 tests shows that target_bpw_type() optimization matched or exceeded the quality of standard quantization in 93% of cases, as indicated by lower KLD metrics.

The function solves a constrained optimization problem to minimize quantization error, subject to a global size budget. It estimates per-tensor error for each layer, and dynamically allocates the bit budget where is needed the most.

High level flow:

  1. Error estimation via Monte Carlo sampling: For every applicable tensor, the function computes the quantization error, dequantizing a subset of rows weighted by an importance matrix (imatrix).
  2. Pareto Frontier analysis: For each tensor, it identifies the Pareto frontier discarding types that increase size without sufficiently decreasing error.
  3. Lagrangian optimization: It uses Lagrangian relaxation to find an optimal distribution of bits across the entire model. Higher bit-rates are dynamically allocated to tensors where they provide the highest reduction in error.
  4. Resumable state: When --state-file is set, target computations are saved to a file. If the quantization is interrupted (e.g., Ctrl+C), it can resume where it left off in the next run.

Advantages

  1. Target arbitrary size models

    • The algorithm will produce a model (nearly) exactly of the requested file/bpw size, which is very useful for maximizing VRAM usage. In a system with 24GB VRAM and a 70B model, standard quants might produce a 16.8GB file (too small, quality left on table) or a 24.1GB file (won't fit). --target-size 23.85g will generate a 23.85 GiB file to utilize the hardware fully.
  2. Data-driven mixed precision often can improve quality at fixed size

    • Instead of using hardcoded heuristics, that may be sub‑optimal for a given architecture or size, the mix is determined by the actual error sensitivity of the specific model's weights. This often yields a better quality/size trade-off, especially in aggressive quantization scenarios (1.5 to 3.5 bpw), or for unusual architectures.
  3. Allows better like-for-like comparisons between models and families

    • Standard quantization uses hardcoded rules like: "use Q4_K_M, except bump some tensors up/down, except fall back if incompatible, except keep some tensors unquantized..." and consquently, two different models quantized at the same Q4_K_M level can end up with very different bpw (e.g. 4.75 and 4.30).

    • Model performance generally scales with size; larger models typically outperform smaller ones. A model quantized with more bits will usually perform better (exhibiting lower perplexity and better evaluation scores) than a smaller version, even when the same underlying quantization method is used. This makes performance comparisons between models not a controlled experiment, as the models being compared have different effective compression ratios.

    • --target-bpw helps to normalize experiments by forcing models to be quantized to a roughly equal overall byte budget. This standardization allows performance variations between models to be more accurately linked to underlying factors such as architectural or training differences, the effect of quantization error at the same compression level, or the decisions made by the optimizer regarding allocation.

Disadvantages

  1. Quantization process is significantly slower than standard

    • This approach can take 5x-10x longer as it quantizes a sample of most tensors into 15 different formats, dequantizes them back, computes error diffs, and selects the best size/error option that fits the target file size or global bpw budget.

    • However, the --state-file option will save the above-mentioned computations to disk so that future quantizations can be generated at normal speed. It also allows to interrupt the computation process and resume it at a later time.

  2. The optimization target is only a proxy for the model's performance quality

    • The process minimizes a per-tensor estimated error computed from sampled rows, not actual perplexity or divergence of output distributions. Since errors interact nonlinearly across layers, there are no guarantees it will select the best possible mix subject to the file/bpw size constraint.
  3. An imatrix with activations data is required for best results

    • The algorithm requires an imatrix generated by llama-imatrix commit 0504396 or newer
    • Activation and statistics data are needed to compute the error. If the imatrix file does not contain the necessary data, --target-bpw and --target-file will refuse to run.

Design considerations

The target_bpw_type() function is implemented as a container for several lambdas providing all the logic for serialization, multithreading, math/stats, and optimization. Although there are clear downsides to this approach (i.e. cognitive load, testability and maintainability), given that all structs and helper lambdas are highly specific to this exact algorithm and have no reuse value elsewhere in the library, a self-contained "God function" seemed the best option to keep modifications to existing code to a minimum.

Test results

Target BPW test results
`KLD results over the full quant range:`
model IQ1_M Q2_K Q3_K Q4_K Q5_K Q6_K Q8_0
ARWKV-R1-1B5 WIN WIN WIN WIN WIN WIN TIE
ARWKV-R1-7B WIN WIN WIN LOSS WIN WIN TIE
ERNIE-4.5-0.3B-PT WIN WIN WIN WIN WIN WIN TIE
ERNIE-4.5-21B-A3B-PT WIN WIN WIN WIN WIN WIN WIN
Falcon-H1-1.5B-Instruct WIN WIN WIN WIN WIN LOSS WIN
Falcon-H1-7B-Instruct LOSS LOSS LOSS WIN WIN LOSS TIE
gemma-3-4b-it WIN WIN WIN WIN WIN WIN TIE
gemma-3-12b-it WIN WIN WIN WIN WIN WIN TIE
gemma-4-E2B-it WIN WIN WIN WIN WIN WIN WIN
gemma-4-E4B-it WIN WIN WIN WIN WIN WIN WIN
granite-3.1-1b-a400m-instruct WIN WIN WIN WIN WIN WIN WIN
granite-3.1-3b-a800m-instruct WIN WIN WIN WIN WIN WIN WIN
granite-4.0-h-tiny WIN WIN WIN WIN WIN TIE TIE
granite-4.0-micro WIN WIN WIN WIN WIN WIN WIN
granite-4.1-3b WIN WIN WIN WIN WIN WIN WIN
granite-4.1-8b WIN WIN WIN WIN WIN WIN TIE
Huihui-MoE-1.2B-A0.6B WIN WIN WIN WIN WIN WIN TIE
Huihui-MoE-5B-A1.7B-abliterated WIN WIN WIN WIN WIN WIN WIN
Llama-3.1-8B WIN WIN WIN WIN WIN WIN TIE
Llama-3.2-1B WIN WIN WIN WIN WIN WIN TIE
mamba-1.4b-hf WIN WIN WIN WIN WIN WIN TIE
mamba-2.8b-hf LOSS LOSS LOSS LOSS LOSS LOSS LOSS
MiniCPM5-1B WIN WIN WIN WIN WIN WIN TIE
MiniCPM5-2B WIN WIN WIN WIN WIN WIN TIE
Ministral-3-3B-Instruct-2512 WIN WIN WIN WIN WIN TIE TIE
Ministral-3-8B-Instruct-2512 WIN WIN WIN WIN WIN WIN TIE
NVIDIA-Nemotron-Nano-9B-v2 WIN WIN WIN WIN WIN LOSS WIN
NVIDIA-Nemotron-Nano-12B WIN WIN WIN WIN WIN WIN WIN
Phi-4-mini-reasoning NA LOSS WIN WIN WIN WIN TIE
Phi-4-reasoning WIN WIN WIN WIN WIN WIN TIE
Qwen3-8B WIN WIN WIN WIN WIN WIN TIE
Qwen3-14B WIN WIN WIN WIN TIE WIN TIE
Qwen3.5-4B WIN WIN WIN WIN WIN TIE TIE
Qwen3.5-9B WIN WIN WIN WIN WIN LOSS TIE

238 tests: 195 (82%) WIN, 26 (11%) TIE, 16 (7%) LOSS, 1 NA

AI usage disclosure

AI was used to validate the mathematical approach and calculations, and to optimize and debug the code.

Special thanks to @AesSedai, @am17an, @compilade, @ddh0 and @ngxson for their contributions during the development of this PR.

@netrunnereve

netrunnereve commented Aug 25, 2025 •

Copy link
Copy Markdown
Contributor

This is a very interesting idea and makes me think of video compression. In video we can use a variable bitrate algorithm that allocates more bits to scenes with lots of detail and less bits for say a still image, all while targeting a preset birate.

I'm just thinking here but maybe in the future we can consider performance as well and automatically juggle error and speed with some sort of slider like what they have for video.

screenshot

@EAddario EAddario changed the title quantize: add option to automatically choose optimal quant types to reach a bpw target at lowest MSE error possible quantize: add option to automatically choose optimal quant types to reach a bpw target at lowest MSE error Aug 25, 2025
@EAddario

Copy link
Copy Markdown
Contributor Author

Sharing some like-for-like test results showing that this approach produces, in the majority of the cases, better quality models compared to naive quantisation (i.e. simply running standard llama-quantize with no further optimisations).

To reduce the duration of the tests, I have chosen two small but representative models: Llama-3.2-1B ("classic" transformer architecture) and Huihui-MoE-1.2B-A0.6B (typical Mixture or Experts).

The test protocol for each is:

  1. Generate Q8_0, Q6_K, Q5_K_M, Q5_K_S, Q4_K_M, Q4_K_S, Q3_K_L, Q3_K_M, Q3_K_S, IQ4_NL, IQ3_M, and IQ3_S naive quantisations (e.g. llama-quantize --imatrix imatrix-with-activations.gguf LLM-Model-F16.gguf Naive-Quantized-<TYPE>.gguf <type>)
  2. Determine each model bits per weight (bpw). This can be easily done by using python llama.cpp/gguf-py/gguf/scripts/gguf_dump.py --markdown BPW-Quantized-<TYPE>.gguf type
  3. Generate equivalent quant types by setting --target-bpw to the corresponding bpw values (e.g. llama-quantize --imatrix imatrix-with-activations.gguf --target-bpw <naive bpw> LLM-Model-F16.gguf BPW-Quantized-<TYPE>.gguf <type>)
  4. Calculate quality scores via llama-perplexity -m <Naive|BPW>-Quantized-<TYPE>.gguf -f calibration_dataset.txt --kl-divergence-base LLM-Model-F16.logits --kl-divergence

Llama-3.2-1B results:

Model Naive BPW Target BPW Naive PPL PPL Naive 𝜌PPL 𝜌PPL Naive KLD KLD
IQ3_M 4.2042 4.2058 11.21441 13.08066 97.46% 94.62% 0.14661 0.29047
IQ3_S 4.1177 4.1191 11.41846 14.10772 97.08% 93.22% 0.16744 0.36883
IQ4_NL 4.9535 4.9542 10.10609 9.98096 99.19% 99.41% 0.04641 0.03356
Q3_K_L 4.6913 4.6894 10.74840 10.30599 98.10% 98.83% 0.10510 0.06738
Q3_K_M 4.4215 4.4184 10.97909 10.42277 97.71% 98.65% 0.12602 0.07920
Q3_K_S 4.1033 4.1037 14.19578 12.11165 92.80% 95.92% 0.37986 0.22400
Q4_K_M 5.1779 5.1792 10.01618 9.88781 99.34% 99.54% 0.03732 0.02654
Q4_K_S 4.9704 4.9762 10.06778 9.97105 99.27% 99.42% 0.04243 0.03350
Q5_K_M 5.8499 5.8521 9.75894 9.79049 99.80% 99.73% 0.01128 0.01620
Q5_K_S 5.7273 5.7291 9.76039 9.79663 99.80% 99.70% 0.01135 0.01757
Q6_K 6.5639 6.5646 9.68812 9.68277 99.91% 99.94% 0.00495 0.00354
Q8_0 8.5013 8.486 9.65172 9.64781 99.99% 99.99% 0.00050 0.00048

Huihui-MoE-1.2B-A0.6B results:

Model Naive BPW Target BPW Naive PPL PPL Naive 𝜌PPL 𝜌PPL Naive KLD KLD
IQ3_M 3.9173 3.9204 27.44670 30.17950 92.93% 91.42% 0.53704 0.58776
IQ3_S 3.8207 3.8239 29.30734 32.94412 92.20% 90.00% 0.52148 0.70061
IQ4_NL 4.5043 4.5092 19.55229 19.71237 98.62% 98.09% 0.08709 0.13948
Q3_K_L 4.3883 4.3923 21.48216 20.62434 96.80% 98.20% 0.20565 0.12301
Q3_K_M 4.1221 5.0412 21.94908 18.87276 96.43% 99.20% 0.23232 0.04863
Q3_K_S 3.8207 3.8519 26.05622 23.81005 93.87% 95.60% 0.41128 0.30752
Q4_K_M 4.9904 5.0412 18.91957 18.87276 99.02% 99.20% 0.05888 0.04863
Q4_K_S 4.7793 4.7826 19.12118 19.25212 98.89% 99.02% 0.06898 0.06238
Q5_K_M 5.7541 5.7950 18.28129 18.31989 99.66% 99.70% 0.01778 0.01531
Q5_K_S 5.6323 5.6342 18.38359 18.37216 99.63% 99.65% 0.02013 0.01884
Q6_K 6.5655 6.5693 18.20380 18.19202 99.80% 99.81% 0.00776 0.00725
Q8_0 8.5028 8.5071 18.09292 18.08959 99.90% 99.90% 0.00094 0.00090

PPL: the smaller the better; 𝜌PPL: the higher the better; KLD: the smaller the better; In bold: best quality

Note

Although these are very encouraging results, more testing with different model architectures and sizes will be required before categorically concluding this functionality consistently yields higher quality models.

Comments, feedback and, in particular, bug reports are very much welcome

@ThiloteE

ThiloteE commented Aug 30, 2025 •

Copy link
Copy Markdown
Contributor

This has an effect on the file size of the quants too, right?

@EAddario

Copy link
Copy Markdown
Contributor Author

That's correct @ThiloteE, file size is directly and proportionally influenced by the chosen bpw

Comment thread src/llama-quant.cpp Resolved
Comment thread src/llama-quant.cpp Resolved
Comment thread src/llama-quant.cpp Resolved
Comment thread src/llama-quant.cpp Resolved
@EAddario

Copy link
Copy Markdown
Contributor Author

Latest version introduces a new algorithm to select candidates resulting in models that are much closer to the bpw target and, in the large majority of the models tested, with better quality metrics than the equivalent naive version or very close:

ERNIE-4.5-21B-A3B-PT
Model BPW PPL 𝜌PPL KLD Same Top P
ERNIE-4.5-21B-A3B-PT-Q4_K_M-naive 4.8543 6.358778 99.48% 0.020945 93.72%
ERNIE-4.5-21B-A3B-PT-Q4_K_M-mse 4.8600 6.358717 99.57% 0.017668 94.04%
ERNIE-4.5-21B-A3B-PT-Q4_K_M-fast 4.8600 6.361663 99.57% 0.017631 94.05%
ERNIE-4.5-21B-A3B-PT-Q4_K_M-precise 4.8600 6.361663 99.57% 0.017631 94.05%
ERNIE-4.5-21B-A3B-PT-IQ4_NL-naive 4.5325 6.373562 99.40% 0.025309 93.14%
ERNIE-4.5-21B-A3B-PT-IQ4_NL-mse 4.5382 6.350721 99.44% 0.023833 93.18%
ERNIE-4.5-21B-A3B-PT-IQ4_NL-fast 4.5382 6.360253 99.45% 0.023487 93.29%
ERNIE-4.5-21B-A3B-PT-IQ4_NL-precise 4.5382 6.360253 99.45% 0.023487 93.29%
Huihui-MoE-1.2B-A0.6B
Model BPW PPL 𝜌PPL KLD Same Top P
Huihui-MoE-1.2B-A0.6B-Q4_K_M-naive 4.9904 18.919566 99.02% 0.058880 87.35%
Huihui-MoE-1.2B-A0.6B-Q4_K_M-mse 4.9934 18.790209 99.19% 0.047507 88.43%
Huihui-MoE-1.2B-A0.6B-Q4_K_M-fast 4.9934 19.042245 99.11% 0.056029 87.12%
Huihui-MoE-1.2B-A0.6B-Q4_K_M-precise 4.9928 18.850755 99.20% 0.047156 88.47%
Huihui-MoE-1.2B-A0.6B-IQ4_NL-naive 4.7539 19.547184 98.62% 0.087197 84.89%
Huihui-MoE-1.2B-A0.6B-IQ4_NL-mse 4.7568 19.164889 98.99% 0.064329 86.78%
Huihui-MoE-1.2B-A0.6B-IQ4_NL-fast 4.7563 19.123570 98.88% 0.069254 86.30%
Huihui-MoE-1.2B-A0.6B-IQ4_NL-precise 4.7568 19.203988 98.87% 0.070200 86.04%
Huihui-MoE-5B-A1.7B-abliterated
Model BPW PPL 𝜌PPL KLD Same Top P
Huihui-MoE-5B-A1.7B-abliterated-Q4_K_M-naive 5.1797 14.744046 99.21% 0.031757 92.07%
Huihui-MoE-5B-A1.7B-abliterated-Q4_K_M-mse 5.1815 14.833509 99.20% 0.032450 92.01%
Huihui-MoE-5B-A1.7B-abliterated-Q4_K_M-fast 5.1812 14.879478 99.19% 0.032689 91.96%
Huihui-MoE-5B-A1.7B-abliterated-Q4_K_M-precise 5.1813 14.851407 99.20% 0.032571 92.00%
Huihui-MoE-5B-A1.7B-abliterated-IQ4_NL-naive 4.6252 14.911541 98.58% 0.065290 89.03%
Huihui-MoE-5B-A1.7B-abliterated-IQ4_NL-mse 4.6270 14.917422 98.86% 0.050793 90.09%
Huihui-MoE-5B-A1.7B-abliterated-IQ4_NL-fast 4.6270 14.787866 98.80% 0.051434 90.12%
Huihui-MoE-5B-A1.7B-abliterated-IQ4_NL-precise 4.6270 14.825255 98.79% 0.051980 90.09%
Llama-3.1-8B
Model BPW PPL 𝜌PPL KLD Same Top P
Llama-3.1-8B-Q4_K_M-naive 5.1777 6.291397 99.47% 0.024019 93.18%
Llama-3.1-8B-Q4_K_M-mse 5.1781 6.243892 99.64% 0.016981 93.88%
Llama-3.1-8B-Q4_K_M-fast 5.1781 6.243892 99.64% 0.016981 93.88%
Llama-3.1-8B-Q4_K_M-precise 5.1781 6.243892 99.64% 0.016981 93.88%
Llama-3.1-8B-IQ4_NL-naive 4.6526 6.321343 99.33% 0.030539 92.22%
Llama-3.1-8B-IQ4_NL-mse 4.6534 6.308061 99.40% 0.027789 92.35%
Llama-3.1-8B-IQ4_NL-fast 4.6534 6.308061 99.40% 0.027789 92.35%
Llama-3.1-8B-IQ4_NL-precise 4.6534 6.308061 99.40% 0.027789 92.35%
Llama-3.2-1B
Model BPW PPL 𝜌PPL KLD Same Top P
Llama-3.2-1B-Q4_K_M-naive 5.1779 10.022728 99.34% 0.037496 90.20%
Llama-3.2-1B-Q4_K_M-mse 5.1790 9.875390 99.54% 0.026770 91.43%
Llama-3.2-1B-Q4_K_M-fast 5.1790 9.864875 99.55% 0.026093 91.55%
Llama-3.2-1B-Q4_K_M-precise 5.1790 9.864875 99.55% 0.026093 91.55%
Llama-3.2-1B-IQ4_NL-naive 4.9535 10.094483 99.18% 0.046429 88.96%
Llama-3.2-1B-IQ4_NL-mse 4.9542 9.967031 99.41% 0.033563 90.52%
Llama-3.2-1B-IQ4_NL-fast 4.9542 9.961903 99.42% 0.033207 90.54%
Llama-3.2-1B-IQ4_NL-precise 4.9542 9.961903 99.42% 0.033207 90.54%
NVIDIA-Nemotron-Nano-9B-v2
Model BPW PPL 𝜌PPL KLD Same Top P
NVIDIA-Nemotron-Nano-9B-v2-Q4_K_M-naive 5.8664 7.812578 99.83% 0.008216 95.49%
NVIDIA-Nemotron-Nano-9B-v2-Q4_K_M-mse 5.8722 7.828592 99.79% 0.011882 93.62%
NVIDIA-Nemotron-Nano-9B-v2-Q4_K_M-fast 5.8722 7.830588 99.79% 0.011959 93.62%
NVIDIA-Nemotron-Nano-9B-v2-Q4_K_M-precise 5.8722 7.829907 99.79% 0.012017 93.62%
NVIDIA-Nemotron-Nano-9B-v2-IQ4_NL-naive 4.7711 7.884677 99.68% 0.016280 93.65%
NVIDIA-Nemotron-Nano-9B-v2-IQ4_NL-mse 4.7778 7.904368 99.61% 0.021356 91.92%
NVIDIA-Nemotron-Nano-9B-v2-IQ4_NL-fast 4.7772 7.905187 99.61% 0.020880 92.06%
NVIDIA-Nemotron-Nano-9B-v2-IQ4_NL-precise 4.7771 7.903981 99.61% 0.021088 92.00%
NVIDIA-Nemotron-Nano-12B-v2
Model BPW PPL 𝜌PPL KLD Same Top P
NVIDIA-Nemotron-Nano-12B-v2-Q4_K_M-naive 4.8654 6.514963 99.70% 0.012792 94.79%
NVIDIA-Nemotron-Nano-12B-v2-Q4_K_M-mse 4.8702 6.542849 99.64% 0.016636 93.85%
NVIDIA-Nemotron-Nano-12B-v2-Q4_K_M-fast 4.8705 6.536551 99.67% 0.014806 94.11%
NVIDIA-Nemotron-Nano-12B-v2-Q4_K_M-precise 4.8705 6.535902 99.68% 0.014691 94.15%
NVIDIA-Nemotron-Nano-12B-v2-IQ4_NL-naive 4.6177 6.544873 99.60% 0.018025 93.87%
NVIDIA-Nemotron-Nano-12B-v2-IQ4_NL-mse 4.7778 7.904368 99.60% 0.021356 91.92%
NVIDIA-Nemotron-Nano-12B-v2-IQ4_NL-fast 4.6228 6.520408 99.65% 0.015356 94.29%
NVIDIA-Nemotron-Nano-12B-v2-IQ4_NL-precise 4.6228 6.520723 99.65% 0.015515 94.23%
Qwen3-14B
Model BPW PPL 𝜌PPL KLD Same Top P
Qwen3-14B-Q4_K_M-naive 4.8730 8.451932 99.47% 0.016716 94.68%
Qwen3-14B-Q4_K_M-mse 4.8735 8.432478 99.52% 0.016641 94.40%
Qwen3-14B-Q4_K_M-fast 4.8735 8.432038 99.53% 0.016474 94.39%
Qwen3-14B-Q4_K_M-precise 4.8735 8.432038 99.53% 0.016474 94.39%
Qwen3-14B-IQ4_NL-naive 4.6236 8.381749 99.35% 0.022481 93.73%
Qwen3-14B-IQ4_NL-mse 4.6243 8.371418 99.42% 0.021300 93.71%
Qwen3-14B-IQ4_NL-fast 4.6243 8.364448 99.43% 0.020910 93.77%
Qwen3-14B-IQ4_NL-precise 4.6243 8.364448 99.43% 0.020910 93.77%

@EAddario EAddario changed the title quantize: add option to automatically choose optimal quant types to reach a bpw target at lowest MSE error quantize: add option to automatically choose optimal quant types to reach a bpw target at lowest error Sep 16, 2025
@pwilkin

pwilkin commented Oct 22, 2026

Copy link
Copy Markdown
Member

/bot review

@ggml-gh-bot

ggml-gh-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Automated code review

Review of PR #15550: --target-bpw / --target-size automatic quant type selection

Impressive algorithmic work with thorough empirical validation in the description. The findings below are mostly robustness and parsing issues; the core optimization math looks sound.

Blocking

(point 1) Unvalidated ggml_type from the state file leads to OOB reads - src/llama-quant.cpp:1084 (load_state). cd.type = (ggml_type)t is read from disk with no range check. A corrupt or hand-edited state file can push an out-of-range type through quant_mix into metadata[i].target_type, where ggml_blck_size() / ggml_get_type_traits() only assert() bounds (ggml/src/ggml.c:1339), so release builds index type_traits[] out of bounds. Reject t outside [0, GGML_TYPE_COUNT) (ideally restrict to the quant_types pool) and drop the entry or the whole file otherwise.

(point 2) Element-type confusion on imatrix.stats_schema - common/imatrix-loader.cpp:131. The schema key is used via gguf_get_arr_str() without first checking the KV is an array of strings, unlike the datasets key two blocks above. A crafted/hostile imatrix GGUF with a non-string array under this key trips GGML_ASSERT (process abort) or, with asserts disabled, reads data_string[i] on an empty vector. Add the same gguf_get_kv_type() == GGUF_TYPE_ARRAY && gguf_get_arr_type() == GGUF_TYPE_STRING guard used for datasets_key.

(point 3) Expert down-projection cap never pins the gate tensors - src/llama-quant.cpp:2875. mean_bpw(expert_ffn_gate, &siblings) fills siblings with the gate member indices, then mean_bpw(expert_ffn_up, &siblings) overwrites the vector with only the up indices (the following std::sort does not help). Consequently ffn_gate_exps tensors are never matched by the std::binary_search(siblings...) pin at line 2892 and go back into the re-auction unconstrained, so the cap does not hold for gate and the freed budget can bump it above sibling_bpw. Merge both member lists into siblings before sorting (e.g. collect into a temp and append).

(point 4) README examples for the new flags parse incorrectly - tools/quantize/README.md:143,148,153 all use input-model-f32.gguf 8 (input, then thread count). Since try_parse_ftype("8") fails, the 8 is consumed as the output filename (quantize.cpp:~800), so the model is written to a file literally named 8 and nthreads is silently ignored. The argc == arg_idx append path only helps when the second positional is a real output path. Either detect a purely-numeric trailing positional as nthreads, or fix the README invocations to <input> <output> [nthreads]. Also fix the ---state-file typo at line 153.

(point 5) "Ignoring" an invalid --state-file is not actually ignored - src/llama-quant.cpp:979. The warning says the invalid state file is ignored, but the very next line sets checkpoint_file = filename, so save_state will still try to write to it. Either keep the default checkpoint name (matching the message) or reword the message; the current behavior contradicts itself.

(point 6) Non-ASCII characters in code - src/llama-quant.cpp:2505 ("⬆︎" in a log string), :1404 ("Centered‑Normalized" uses U+2011), :1872 (∞), :2065 (±). AGENTS.md requires ASCII-only in code and comments; replace with -/->/"+/-" equivalents. The boost indicator could be a plain * or text.

Will slow the review

(point 7) Public API struct layout - include/llama.h:454. The activations/statistics pointers are inserted mid-struct, shifting the offsets of kv_overrides, tt_overrides, prune_layers, max_buf_size for any consumer built against the old layout. Append all new fields at the end instead. Also document the sentinels (target_bpw == -1.0f, target_size == -1 mean "unset") in the header comments - right now a library user has no way to know these magic values exist.

(point 8) Bundled behavior change: unused-KV tensors converted to Q4_0 in all modes - src/llama-quant.cpp:3224-3227. Tensors matching blk.N.attn_[kv].weight in shared-KV models (gemma4 family) are now unconditionally converted to Q4_0 during every quantization, not just target-bpw mode. This changes existing quant outputs (smaller, since the tensors are unused at runtime, but still a semantic change to standard q4_k_m etc.) and is a separate feature from the PR's stated scope. Please split it out or get explicit maintainer sign-off for it in this PR, with a size/quality datapoint for one affected model.

(point 9) No tests - the diff adds ~2000 lines of new selection, serialization, and optimization logic with zero test coverage, while tests/test-quant-type-selection.cpp and tests/test-quantize-fns.cpp exist as natural homes. At minimum: state-file save/load roundtrip (including corrupt input), --target-exclude filtering, and the budget clamp paths can be unit-tested without a model; the two GGUF-parsing fixes above are exactly the kind of bugs a test would have caught.

(point 10) README table regression - tools/quantize/README.md:196-197: Q4_K_S row is duplicated and Q3_K_M (present in the old table, 3.9960 bpw) was dropped in the reformat.

(point 11) Design / structure - the PR description justifies the single target_bpw_type() "God function" as avoiding global-scope pollution, but static free functions in a .cpp (or a separate llama-quant-bpw.cpp TU) do not pollute anything and would make the hand-rolled Jacobi/QR/SVD, state serialization, and auction independently readable and testable. This has been discussed with maintainers already (ngxson suggested a standalone tool), so I raise it as a design point rather than a blocker, but a middle step (splitting serialization and the SVD estimator into static helpers) would materially reduce review cost.

(point 12) Library code installs process-wide signal handlers - src/llama-quant.cpp signal_scope_guard. llama-quant.cpp is part of libllama; silently replacing SIGINT/SIGTERM handlers inside llama_model_quantize() is intrusive for embedders (restoration is correct, but only if the function exits normally). Consider gating this on an explicit opt-in or documenting the side effect in llama.h next to state_file.

(point 13) State file not keyed to the imatrix - load_state validates only model_id (hash of model metadata). Resuming --target-bpw with a different imatrix silently reuses stale per-tensor errors. Store a hash of the imatrix data (or its datasets/chunk metadata) alongside model_id and warn or refuse on mismatch.

(point 14) parse_target_size overflow - tools/quantize/quantize.cpp:~520: (int64_t)(val * mul) with val * mul > INT64_MAX is UB; clamp or reject. Also 0 is accepted although the help text says "must be a positive number" (it just clamps to minimum, so either accept the wording or reject 0).

Nits

(point 15) quantize.cpp:826: the const_cast<char *> on get_ftype(...) and "F16" is unnecessary - tmp_argv is a std::vector<const char *>.

(point 16) common/imatrix-loader.h: stats_schema is populated by the loader but never consumed anywhere (quantize.cpp hardcodes the canonical indices); either use it or drop it.

(point 17) In target mode, --token-embedding-type and --output-tensor-type are silently ignored (only --pure gets a warning at llama-quant.cpp:3294); either honor them as pinned tensors or warn like --pure does.

(point 18) The --state-file writability probe opens-for-append then removes the target file (llama-quant.cpp:~968); if std::remove fails, an empty file is left behind. std::ofstream open alone (without the app + remove dance) already answers the question.

(point 19) --target-size accounting uses gguf_get_meta_size() only; per-tensor alignment padding in the output GGUF is not counted. Tiny relative error, but worth a one-line "approximate" note in the README since the PR sells near-exact sizes.

This review was generated automatically by pi coding agent using zai-org/GLM-5.3. It may contain mistakes. Maintainers make the final call.

@EAddario

EAddario commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Mostly valid points but a few missed the mark (e.g. point 3 is plainly wrong but I'll add a comment to clarify what's going on; on 15, I added the cast because CI failed somewhere, windows if I recall correctly; etc.).

@EAddario
EAddario marked this pull request as draft October 25, 2026 22:35
@github-actions github-actions Bot added the testing Everything test related label Nov 1, 2026
@EAddario
EAddario marked this pull request as ready for review November 1, 2026 09:12
@EAddario
EAddario requested a review from a team as a code owner November 1, 2026 09:12
@EAddario

EAddario commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

@pwilkin, issues flagged by the review bot should be addressed now. New test-quantize-target added.

@pwilkin

pwilkin commented Nov 1, 2026

Copy link
Copy Markdown
Member

ubuntu-webgpu has compile errors:

FAILED: [code=1] src/CMakeFiles/llama.dir/llama-quant.cpp.obj 
ccache /opt/hostedtoolcache/wasi-sdk/bin/clang++ --target=wasm32-wasip3 -DGGML_USE_CPU -DGGML_USE_WEBGPU -I/home/runner/work/llama.cpp/llama.cpp/src/. -I/home/runner/work/llama.cpp/llama.cpp/build-wasi/src -I/home/runner/work/llama.cpp/llama.cpp/src/../include -I/home/runner/work/llama.cpp/llama.cpp/ggml/src/../include -msimd128 -D_WASI_EMULATED_SIGNAL -fwasm-exceptions -mllvm -wasm-use-legacy-eh=false -O3 -DNDEBUG -Xclang -fno-pch-timestamp -Wmissing-declarations -Wmissing-noreturn -Wall -Wextra -Wpedantic -Wcast-qual -Wno-unused-function -Wunreachable-code-break -Wunreachable-code-return -Wmissing-prototypes -Wextra-semi -MD -MT src/CMakeFiles/llama.dir/llama-quant.cpp.obj -MF src/CMakeFiles/llama.dir/llama-quant.cpp.obj.d -o src/CMakeFiles/llama.dir/llama-quant.cpp.obj -c /home/runner/work/llama.cpp/llama.cpp/src/llama-quant.cpp
/home/runner/work/llama.cpp/llama.cpp/src/llama-quant.cpp:51:62: warning: implicit conversion from 'unsigned long long' to 'const size_t' (aka 'const unsigned long') changes value from 8589934592 to 0 [-Wconstant-conversion]
   51 | static const size_t LLAMA_QUANT_MAX_BUF_SIZE = 8ull*1024*1024*1024;
      |                     ~~~~~~~~~~~~~~~~~~~~~~~~   ~~~~~~~~~~~~~~^~~~~
/home/runner/work/llama.cpp/llama.cpp/src/llama-quant.cpp:2380:28: error: variable has incomplete type 'std::exception_ptr'
 2380 |         std::exception_ptr w_exception;
      |                            ^
/opt/hostedtoolcache/wasi-sdk/bin/../share/wasi-sysroot/include/wasm32-wasip3/eh/c++/v1/__exception/operations.h:38:33: note: forward declaration of 'std::exception_ptr'
   38 | class _LIBCPP_EXPORTED_FROM_ABI exception_ptr;
      |                                 ^
/home/runner/work/llama.cpp/llama.cpp/src/llama-quant.cpp:2410:39: error: calling 'current_exception' with incomplete return type 'exception_ptr'
 2410 |                         w_exception = std::current_exception();
      |                                       ^~~~~~~~~~~~~~~~~~~~~~~~
/opt/hostedtoolcache/wasi-sdk/bin/../share/wasi-sysroot/include/wasm32-wasip3/eh/c++/v1/__exception/operations.h:40:59: note: 'current_exception' declared here
   40 | [[__nodiscard__]] _LIBCPP_EXPORTED_FROM_ABI exception_ptr current_exception() _NOEXCEPT;
      |                                                           ^
/opt/hostedtoolcache/wasi-sdk/bin/../share/wasi-sysroot/include/wasm32-wasip3/eh/c++/v1/__exception/operations.h:38:33: note: forward declaration of 'std::exception_ptr'
   38 | class _LIBCPP_EXPORTED_FROM_ABI exception_ptr;
      |                                 ^
1 warning and 2 errors generated.
[75/81] Building CXX object src/CMakeFiles/llama.dir/llama-model-loader.cpp.obj
/home/runner/work/llama.cpp/llama.cpp/src/llama-model-loader.cpp:1107:57: warning: implicit conversion from 'unsigned long long' to 'const size_t' (aka 'const unsigned long') changes value from 4294967296 to 0 [-Wconstant-conversion]
 1107 |     constexpr size_t auto_min_size = 4ull * 1024 * 1024 * 1024;
      |                      ~~~~~~~~~~~~~   ~~~~~~~~~~~~~~~~~~~^~~~~~
1 warning generated.
[76/81] Building CXX object src/CMakeFiles/llama.dir/llama-sampler.cpp.obj

@RedOxer

RedOxer commented Nov 2, 2026

Copy link
Copy Markdown

Thanks for this work. I compared per-tensor cost estimates against a degrade-one ground truth (each block matrix alone in Q2_K, ΔKL vs BF16, everything else Q8_0) on Qwen3-1.7B and SmolLM2-1.7B.

The per-tensor error estimated by this branch, read from its log, reaches a Spearman correlation of 0.45 / 0.44 with the measured ΔKL. An output-side cost (KL at the model output, Fisher approximation) reaches 0.93 / 0.95. With the same imatrix and the same file size, maps built from that cost have lower KL than --target-size on Llama 3.1 8B and Qwen3-8B: small gaps at Q2–Q4 (2–10 %), larger ones at Q5 (14–25 %).

Details, reproduction commands and the code: #31549

If it's useful, I'd be happy to help see whether an output-side cost could fit into --target-bpw, or to provide it as a tensor→type mapping consumed by llama-quantize, as ngxson suggested above.

@EAddario

EAddario commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Hi @RedOxer, I like the imatrix angle! it's an interesting approach which I think is useful, but in a different direction to --target-size. I'd group it in the same category as @bartowski1182 and @Thireus' where the best tensor/quant recipe is derived by "running the model" (forward pass) to measure the effects of changes on the resulting KLD/PPL. You can and will get the best possible quality (high correlation) in exchange of high computational cost to measure multiple permutations.

This PR takes an alternative angle in that the lowest error recipe is estimated through measuring quant/dequant error in relation to each tensor compared to itself but in the previous layer. The upside is it doesn't take much CPU but the downside of course is that the resulting quality is not the same as an exhaustive search.

IMO, producing a --tensor-type-file would be the way to go.

If you haven't yet, recommend to check out Thireus' GGUF-Tool-Suite and its companion website (it may be down momentarily) and Per-tensor layout maps for GGUF quantization

@RedOxer

RedOxer commented Nov 6, 2026

Copy link
Copy Markdown

Hello @EAddario !
Thanks for taking a look, and for the pointers! Agreed on --tensor-type-file: that's exactly what llamap outputs, so the result goes through stock llama-quantize.

One clarification on cost, since it's the main point of the approach: llamap doesn't search permutations or evaluate the model once per variant. It estimates every tensor × type cost from a single forward/backward pass on 512 calibration tokens (Fisher approximation, 128 samples), plus the in-memory quantization of each candidate. On an 8B that's ~15 min for 11 types, mostly spent quantizing candidates, then ~1 min per extra budget. For comparison, --target-size takes ~3 min, and degrade-one takes one llama-perplexity run per tensor and type. I used degrade-one only as ground truth, to validate the estimates. So it sits in between: it needs a GPU and the HF weights, but no exhaustive search.

I'll look at Thireus' GGUF-Tool-Suite and add it to the related work. Thanks again!

@EAddario

EAddario commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@pwilkin, anything else that's needed?

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

documentation Improvements or additions to documentation examples ggml changes relating to the ggml tensor library for machine learning testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants