Recurrent Residual Quantization (RRQ) for LLMs - #2308
Conversation
…+2+2) Implement RRQ (Recurrent Residual Quantization) algorithm for LLM quantization. Each layer is quantized into 4 planes of INT2 via iterative RTN: - Base plane (plane 0): standard INT2 AutoRound export (auto_round format) - Residual planes (1-3): packed INT2, stored in auto_round:rrq format Key components: - RRQConfig: algorithm config (bits=2, data_type=int, act_bits=16, 4 planes) - RRQRTNQuantizer: iterative RTN quantizer producing packed INT2 planes - RRQFormat: output format backend for residual model export - RRQLinear: inference module with dynamic precision (2/4/6/8-bit) - load_rrq_model: loader combining base + residual into RRQ-enabled model - save_quantized_rrq / save_rrq_base_model: export helpers Fixes: - qlinear_torch.py: self.device -> device param in asym pack path - SUPPORTED_FORMATS: added auto_round:rrq - ModelFreeCompressor: accept auto_round:rrq format - GGUF/MLX export: reject RRQ residual models (fail fast) Validation (Qwen3-0.6B, group_size=128, asym, XPU): - 23/23 unit tests pass - HellaSwag accuracy: 26.5%(2b) -> 35.5%(4b) -> 43.5%(6b) vs fp32 43.5%
… base) Add generate_rrq_residual(base_model_dir, raw_model, output_dir) to generate the 3 RTN INT2 residual planes from an existing INT2 base model + original FP weights, without re-quantizing the base. Supports local dirs and HF model names; validates bits/group_size/sym against the base config; exposed via lazy import from auto_round. Adds 5 unit tests (output structure, residual norm monotonic decrease, config fail-fast, top-level export) and the Phase 1 PR description. All 28 RRQ tests pass.
Add RRQConfig tuning fields and RRQSignRoundQuantizer with four sequential AutoRound sign-SGD rounds. Each round optimizes value_k/min_scale_k/max_scale_k through the STE path while freezing the completed prefix, then exports the existing packed INT2 ABI. Route RRQ OPT configurations through the calibrated compressor, preserve RTN behavior for iters=0, and add Phase 3 tests and Qwen3-0.6B validation documentation. Verified with 31 RRQ tests and a two-iteration Qwen3-0.6B tuning/export/load run.
Fix Phase 3 prefix state so each round freezes the cumulative sum of all previous planes instead of only the immediately preceding plane. Add a reconstruction regression test and expose iters/lr/calibration controls in the Qwen3 RRQ test script. Validated with 32 RRQ tests and corrected OPT-50 versus RTN HellaSwag evaluations.
for more information, see https://pre-commit.ci
…cision
- base plane routed through imatrix-weighted opt-RTN (bit-exact with standard
W2A16 base); residual planes seed scale search via search_optimized_init_scale
- collect per-layer imatrix on both RTN and SignRound paths; need_calib always
- config defaults to SignRound (iters=200); iters=0 selects RTN-only; surface
standard AutoRound knobs; num_residual_planes in {1,3}; disable_opt_rtn kept
as a routing guard
- force RRQ down the regular compressor so all residual planes are retained;
explicit export format overrides a previously resolved format
- add set_rrq_random_residual + load_rrq_model(residual_fraction=...) for
seeded per-layer mixed precision; new unit test
Announce RRQ (progressive multi-precision: one INT2 checkpoint serving 2/4/6/8-bit + per-layer mixed precision) in What's New, with paper link (arxiv 2608.04048), in both EN and CN READMEs.
for more information, see https://pre-commit.ci
Qwen3-8B RRQ Accuracy ResultsExperiment Setup
Calibration configuration (aligned with paper
File Size Analysis
Notes:
Mean Accuracy: bestsym (symmetric, aligned with paper)All values are percentages. Differences are percentage points (pp).
The 6≈8 phenomenon is reproduced: 6-bit and 8-bit achieve nearly identical accuracy (gap ≤0.13 pp). W4A16 Baseline (bestsym config)Standard AutoRound W4A16 achieved 67.85% mean accuracy (pre-fix asymmetric eval, raw
Note: The W4A16 baseline was evaluated with the pre-fix (raw Task-Level Accuracy: bestsymAll values are percentages.
Paper Reference (Qwen3-8B, Section 5)
Mixed-Precision Definitions
Key Findings
The random mixed-precision CLI options are implemented in |
…n add_groups RTNConfig registers two options that share the same dest (disable_opt_rtn): --disable_opt_rtn (const=True) and --enable_opt_rtn (const=False). Adding RRQConfig, which inherits those via super().register_args(), made add_groups attempt a cross-group merge where the existing_index lookup matched the incoming --enable_opt_rtn against the first --disable_opt_rtn via the shared dest, causing _merge_parameter to raise "incompatible shared CLI argument 'disable_opt_rtn'". Prefer matching on the shared option string first, and only fall back to matching by dest (for aliased options) when the compatibility keys are equal.
…tmul Previously, RRQLinear.forward called each plane's QuantLinear.forward separately (4 dequant+GEMM ops for 4 planes). This is slow on CPU since there is no fused kernel yet. Now: - Extract _dequantize() from QuantLinear.forward (both symmetric and asymmetric variants) so the unpack+scale logic can be reused. - RRQLinear._get_packed_weight() dequantizes each active plane once, accumulates into a single weight tensor (cached by plane count), then runs one matmul + bias. This reduces 4 GEMMs to 1 GEMM per layer per forward call. The per-plane dequant is a one-time cost (cached), so the steady-state forward is a single GEMM on the accumulated weight.
for more information, see https://pre-commit.ci
RRQConfig.__init__ took tunable fields (iters/lr/minmax_lr/momentum/etc.) via **kwargs.pop instead of named parameters, so the registered CLI fields were not accepted by the config constructor. This broke the main test test_registered_cli_fields_are_accepted_by_config_constructors when the RRQ entry was added to the registry. Declare the fields as keyword-only parameters (matching SignRoundConfig), with identical defaults, so the CLI field-acceptance contract holds without changing behavior.
|
implementation of #2300 |
…convert For MLLM models (e.g. Qwen3-VL), _collect_modules_to_not_convert() previously excluded layers that belonged to blocks from the 'not convert' list, even when those layers were not in layer_config (i.e. not quantized). This caused vision layers (e.g. model.visual.merger.linear_fc2) to be missing from modules_to_not_convert in the exported quantization_config.json, breaking inference with vLLM/AWQ backends that rely on this field to skip unquantized modules. Fix: remove the redundant 'not in layers_in_blocks' condition from the full-model scan. Any supported layer not in layer_config by definition was not quantized and must be excluded from AWQ conversion.
Per the 'don't alter shared code' principle, revert common-path changes that were not strictly required by RRQ, keeping the original paths identical to main: - compressors/base.py: restore the 'only set formats when None' logic (was unconditionally overriding, changing all compressor behavior). - compressors/model_free.py: restore output_tensors cleanup before clear_memory(), the original cross-shard log message, and the original accepted_formats set (rrq format handled via its own path). - autoround.py: revert type() check back to isinstance() so OptimizedRTNConfig/RRQConfig routing is preserved; gate the RRQ disable_model_free on an isinstance(quant_config, RRQConfig) check only. Kept (required for RRQ / genuine bug fixes, no regressions): - qlinear_torch*.py _dequantize extraction + device/infeatures (RRQ multi-plane planes call _dequantize() with no input, and pack on a device different from self.device). - cli/algorithms.py option-string-first matching (precise bug fix). Verified: RRQ, CLI, and export unit tests all pass; 958-test common regression sweep passes (remaining failures are local ninja/Marlin env).
for more information, see https://pre-commit.ci
…ogic The earlier change to _collect_modules_to_not_convert was a mask for a real regression: the common-code changes in compressors/base.py and compressors/model_free.py (now reverted) were feeding incorrect to_quant_block_names/layer_config into the AWQ export, which is why modules_to_not_convert came out empty for vision models. With those reverts in place, the original main logic correctly collects the vision modules, so this file no longer needs to diverge from main. test_autoawq_qwen3_vl_infer now passes on the branch with main's AWQ code.
|
/azp run Unit-Test-CUDA-AutoRound |
|
Azure Pipelines successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
🟡 Changes recommended
load_rrq_model can silently leave some packed base layers as uninitialized/random nn.Linear weights when residual planes are missing or skipped, which is correctness-critical for inference.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds Recurrent Residual Quantization (RRQ) to AutoRound, enabling a single INT2-base checkpoint plus packed INT2 residual planes to support dynamic 2/4/6/8-bit (and mixed-precision) inference via a new auto_round:rrq residual artifact and corresponding loader/runtime modules.
Changes:
- Introduces RRQ algorithm config + quantizers (RTN and per-plane SignRound tuning) and registers it in the algorithm registry.
- Adds RRQ residual export format (
auto_round:rrq) and inference-time composition (load_rrq_model,RRQLinear, precision switching utilities). - Updates quantized linear kernels to expose
_dequantize()for reuse, plus adds extensive CPU unit tests and user scripts; updates README(+CN) and ignoresrrq_output/.
File summaries
| File | Description |
|---|---|
test/unit/test_cpu/algorithms/test_rrq.py |
Comprehensive CPU unit tests for RRQ config, packing, reconstruction, inference switching, and Phase 2/3 behaviors. |
test_rrq_qwen3_06b.py |
Standalone script to quantize Qwen3-0.6B with RRQ and verify base/residual layout + (optional) load/forward. |
test_rrq_lm_eval.py |
Standalone script to run lm-eval across RRQ bit-widths (base+residual). |
README.md |
Adds RRQ announcement to “What’s New”. |
README_CN.md |
Chinese counterpart update for the RRQ “What’s New” entry. |
auto_round/utils/common.py |
Adds auto_round:rrq to supported formats list. |
auto_round/inference/rrq_model.py |
New loader that merges base + residual artifacts into an RRQ-enabled model by replacing layers with RRQLinear. |
auto_round/inference/rrq_linear.py |
New RRQLinear module and helpers to set uniform or random mixed precision across layers. |
auto_round/inference/backend.py |
Adds RRQ format constant (RRQ_FORMAT). |
auto_round/export/formats/backends/rrq.py |
New OutputFormat backend for auto_round:rrq residual export. |
auto_round/export/formats/backends/__init__.py |
Exposes RRQFormat in backend imports/exports. |
auto_round/export/export_to_mlx/export.py |
Fail-fast guard rejecting RRQ residual models for MLX export. |
auto_round/export/export_to_gguf/conversion/base.py |
Fail-fast guard rejecting RRQ residual models for GGUF export. |
auto_round/export/export_to_autoround/export_to_rrq.py |
Implements RRQ residual serialization + Phase 2 residual generation from base+raw weights. |
auto_round/compressors/model_free.py |
Frees packed shard tensors earlier to improve memory reclamation; tweaks a log message. |
auto_round/cli/algorithms.py |
Improves CLI arg merge logic to avoid mismatching boolean optional arguments with shared dest. |
auto_round/autoround.py |
Forces RRQ to route through calibrated path (disables model-free path) to avoid dropping residual planes. |
auto_round/algorithms/registry.py |
Registers RRQ config/quantizer modules and adds rrq to built-in algorithm order. |
auto_round/algorithms/quantization/rrq/quantizer.py |
Core RRQ quantizers (RTN multi-plane + SignRound per-plane tuning with frozen prefix) and packing logic. |
auto_round/algorithms/quantization/rrq/config.py |
RRQConfig implementation (fixed INT2 planes, tuning params, calibration requirement). |
auto_round/algorithms/quantization/rrq/__init__.py |
RRQ module exports. |
auto_round/__init__.py |
Exposes RRQConfig and lazily exports load_rrq_model / generate_rrq_residual. |
auto_round_extension/torch/qlinear_torch.py |
Fixes device usage in packing; adds _dequantize() helper and adjusts g_idx logic. |
auto_round_extension/torch/qlinear_torch_zp.py |
Adds _dequantize() helper and adjusts g_idx logic (symmetric/GPTQ-style). |
.gitignore |
Ignores rrq_output/ directory. |
Review details
- Files reviewed: 24/25 changed files
- Comments generated: 7
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
CI Failure Analysis Report (Unit-Test-AutoRound)
New Issues (1)No.1 — 1 occurrence(s)🔍 AssertionError: /auto-round/test/unit/test_cpu/algorithms/test_rrq.py:977 has unclassified reason: Time-consuming lm_eval accuracy check; covered by nightly📝 Basic info
🖥️ Log excerpt✨ AI analysis
|
| _builtin_algorithms_registered = False | ||
| _pipeline_members_registered = False | ||
| _BUILTIN_ALGORITHM_ORDER = ("rtn", "auto_round", "awq", "svdquant", "hadamard", "quarot", "spinquant") | ||
| _BUILTIN_ALGORITHM_ORDER = ("rtn", "rrq", "auto_round", "awq", "svdquant", "hadamard", "quarot", "spinquant") |
There was a problem hiding this comment.
As RRQ is currently difficult to deploy, I’d prefer not to expose this algorithm to users directly if possible. Instead, we can automatically switch to RRQ when a specific format is specified.
| A JSON-serializable dict describing the RRQ residual model. | ||
| """ | ||
| return { | ||
| "quant_method": RRQ_QUANT_METHOD, |
There was a problem hiding this comment.
Typically, we don't change the quantization method; we change the format instead if we want it to be adopted quickly by the community, following the successful adoption of AutoRound.
This comment has been minimized.
This comment has been minimized.
The previous guard set `route_kwargs["disable_model_free"] = True` for
RRQConfig, but that key was never passed into `is_model_free_route`,
which reads from `route_decision_kwargs`. So the automatic model-free
route was never actually blocked, and an explicit `model_free=True`
would unconditionally take the model-free path (checked before
`disable_model_free` in `is_model_free_route`), silently dropping every
RRQ residual plane because `_build_model_free_compressor` never
receives `alg_configs`.
Two guard cases now:
(a) explicit model_free=True -> raise ValueError (cannot be overridden
by disable_model_free)
(b) auto-route (no explicit flag) -> set disable_model_free in
route_kwargs so the regular calibrated path is taken
Verified with Qwen3-0.6B + RRQConfig:
- model_free=True -> raises ValueError
- model_free=False -> CompressionOrchestrator (regular path)
- disable_model_free=True-> CompressionOrchestrator
- no explicit flags -> CompressionOrchestrator (auto-route blocked)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RRQConfig inherits RTNConfig, whose register_args() exposes --enable_opt_rtn / --disable_opt_rtn. If a caller passes disable_opt_rtn=False (via API or CLI --enable_opt_rtn), the entry point's _select_rtn_compressor_base_cls coerces the config to OptimizedRTNConfig (quant_config.__class__ = OptimizedRTNConfig), silently dropping every RRQ residual plane. Add a check_config() assertion that raises ValueError when disable_opt_rtn=False, preventing the coercion before it happens. The per-plane RTN quality is already matched to standard AutoRound inside the quantizer (RRQRTNQuantizer), so this enforcement is safe. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move load_rrq_model and generate_rrq_residual out of the top-level auto_round namespace into their respective sub-packages to match the project's style (config/scheme classes live at top level, I/O helpers live in sub-packages): - auto_round.inference exposes load_rrq_model - auto_round.export exposes generate_rrq_residual (lazy via PEP 562 __getattr__ to avoid a circular import through utils -> export) - Update test scripts and unit test to use the new import paths - Fix unterminated docstring in rrq_linear.py - Add RRQ evaluation scripts (ppl check, partial residual, random seeds, weight error checks, fair L0, joint vs w4, l0 xpu, standard w4/w6)
Add TestRRQAccuracy with test_rrq_w2a16_rtn_lmeval that: - Quantizes OPT-125m with RRQ (4 planes, RTN, W2A16) - Saves base + residual via auto_round + auto_round:rrq formats - Reloads via load_rrq_model and evaluates at 8-bit (all planes) and 4-bit (base + 1 residual) using lambada_openai - Marks skip_ci since it requires a full model + lm_eval run
for more information, see https://pre-commit.ci
- Add 'hidden' flag to AlgRegistryEntry and register_algorithm() - Mark RRQ as hidden: it won't appear in 'list alg' or --help output - Auto-select RRQ algorithm when --format auto_round:rrq is used - Validate format/algorithm compatibility (bidirectional check) - Fix RRQConfig validation to accept None values from CLI defaults - Add unit tests for hidden algorithm and format auto-detection
for more information, see https://pre-commit.ci
PR: Recurrent Residual Quantization (RRQ) for LLMs
Branch:
feat/rrq-phase1→mainScope: 5 commits, 27 files, +4,826 / −6
Recurrent Residual Quantization (RRQ) quantizes each weight tensor into K sequential
INT2 planes (1 base + K−1 residual planes). The base plane is a standard AutoRound
INT2 export; the residual planes are packed together into a new
auto_round:rrqartifact. At inference the effective precision is selected dynamically
(2 / 4 / 6 / 8-bit) — and, as of the latest commit, per-layer mixed precision —
from a single checkpoint, without re-quantizing.
1. Motivation
loading more or fewer residual planes, instead of shipping a separate model per
bit-width.
runtimes can load as-is; RRQ adds residual planes on top.
SignRound machinery as ordinary AutoRound, so RRQ is a fair, drop-in extension rather
than a weaker parallel path.
2. Design
all previous planes. The base plane uses imatrix-weighted opt-RTN (bit-exact with
a standard W2A16 base); residual planes seed their scale search with
search_optimized_init_scale. Withiters>0, per-plane sign-SGD (SignRound)tuning optimizes
value/min_scale/max_scalethrough the STE path while freezing thecompleted prefix.
QuantLinearpack/forward path.qweight / scales / qzeros(standardauto_round).qweight_{1..3} / scales_{1..3} / qzeros_{1..3}in oneauto_round:rrqartifact, tagged
quant_method = "auto-round-rrq".RRQLinearcomputes the base output, then accumulates the firstactive_planes − 1residual outputs.set_rrq_bits(model, bits)switches precisionuniformly;
set_rrq_random_residual(...)/load_rrq_model(residual_fraction=...)assign precision per layer.
3. Commit breakdown
3299eb3eRRQConfig,RRQRTNQuantizer,RRQFormat,RRQLinear,load_rrq_model; export helpers; format/back-end wiring; GGUF/MLX fail-fast guards; 23 tests.6afb9a7fgenerate_rrq_residual5c7c12e5RRQSignRoundQuantizerwith sequential AutoRound rounds; freeze completed prefix; route OPT configs through the calibrated compressor; keep RTN foriters=0.0fd545f3b38849a9init_scaleseeding;need_calibalways; SignRound defaults (iters=200);set_rrq_random_residual+residual_fraction; new test.4. Public API
5. Changed files (cumulative vs
main)algorithms/quantization/rrq/{__init__,config,quantizer}.py,algorithms/registry.pyexport/export_to_autoround/export_to_rrq.py,export/formats/backends/rrq.py,export/formats/backends/__init__.pyinference/rrq_linear.py,inference/rrq_model.py,inference/backend.pyauto_round/__init__.py,autoround.py,compressors/base.py,compressors/model_free.py,utils/common.pyexport/export_to_gguf/conversion/base.py,export/export_to_mlx/export.pyauto_round_extension/torch/qlinear_torch.py(asym packself.device→device)test/unit/test_cpu/algorithms/test_rrq.py(36 tests)test_rrq_qwen3_06b.py,test_rrq_lm_eval.pydocs/rrq_rfc.md,docs/rrq_rfc_CN.md,docs/rrq_progress_CN.md,docs/PR_rrq_phase1.md,docs/rrq_pr_summary.md,.gitignore6. Validation
6a. Base-plane parity
The RRQ base plane is bit-exact with a standard W2A16 imatrix-weighted opt-RTN base
at matched calibration (batch_size=8): 0 / 702 element differences.
6b. Accuracy — Phase 1, HellaSwag (Qwen3-0.6B, group_size=128, asym, XPU, limit=200)
6c. Accuracy — opt-RTN, 5-task mean (Qwen3-0.6B, sym)
5-task mean = piqa / winogrande / hellaswag / arc_easy / arc_challenge. FP = 50.65.
group_size = 128 (default config). The RRQ default base is imatrix-weighted opt-RTN;
residual planes seed the scale search with
init_scale. RTN shown for reference.The
init_scale/opt-RTN path wins at low bits (2/4); plain RTN edges ahead at 6/8-bitbecause opt-RTN's larger scales sit outside SignRound's
[0,1]scale space at highbit-widths.
group_size = 16. Finer groups recover most of the gap; 6-/8-bit reach or exceed FP.
6d. Mixed precision (~3-bit, arc_easy, group_size=16)
Weight-error–based layer ranking (energy / ΔE) concentrates the budget on
mlp.down_projand under-performs random allocation on downstream accuracy; random lands within noise of
a true 3-bit model while keeping a single checkpoint (±1.5 arc_easy variance across seeds).
6e. Storage (Phase 1, Qwen3-0.6B, fp32 = 1.40 GB)
Comparing only the quantized-weight portion, the 3 residual planes are ~3× the single
base plane (as designed); the base artifact's extra bulk is the fp16 non-quantized tensors
that any W2A16 checkpoint also carries.
6f. Unit tests
pytest test/unit/test_cpu/algorithms/test_rrq.py— 36 passed. Coverage: configvalidation, packed INT2 storage, residual convergence, symmetric/asymmetric, RRQLinear
forward & precision switching, sign-SGD prefix accumulation, export buffer rename +
config attach, load validation, incremental residual generation, and the new
random-residual mixed-precision config.
7. Backward compatibility
load_rrq_modelbehaviour (uniformactive_bits) is unchanged whenresidual_fractionis not supplied.RRQConfig.itersdefault changed from0(RTN) to200(SignRound). Passiters=0to restore pure RTN.
num_residual_planesnow accepts1(4-bit scheme) or3(2/4/6/8-bit scheme).dropping).
8. Known limitations & follow-ups
correctness reference, not a fused kernel (packed-INT2 fused kernels are Phase 2+).
act_bits=16); no activation quantization.selector (imatrix-weighted output error) is a promising follow-up to beat random.