Repository navigation
Recurrent Residual Quantization (RRQ) for LLMs - #2308
luoyu-intel wants to merge 49 commits into
Conversation
Qwen3 Quantization Benchmark: W4A16 vs RRQ (Intel Arc Pro B60 XPU)Test Environment
Test Configurations
Qwen3-0.6B Results (Correct Calibration)
All 4 configurations successfully quantized 196/197 layers (lm_head kept unquantized). Timing ComparisonRTN vs OPT (within same scheme)
W4A16 vs RRQ (within same optimization mode)
Per-Layer Breakdown
Memory Comparison
Qwen3-8B Results (Preliminary — Suboptimal Calibration)
All 4 configurations successfully quantized 252/253 layers (lm_head kept unquantized). Device: xpu:1. 8B Timing Comparison (Preliminary)RTN vs OPT (within same scheme)
W4A16 vs RRQ (within same optimization mode)
8B Per-Layer Breakdown (Preliminary)
8B Memory Comparison (Preliminary)
SummaryQwen3-0.6B (Correct Calibration: nsamples=512, seqlen=2048, bs=8)
Qwen3-8B (Preliminary: nsamples=4, seqlen=64, bs=1)
Conclusions
Appendix: Preliminary 8B Results (nsamples=4, seqlen=64, bs=1)These were the first 8B runs (2026-09-12/13) using suboptimal calibration parameters. They are kept for reference but should be superseded by a proper run with nsamples=512, seqlen=2048, bs=8. Key observations from preliminary data:
|
|
implementation of #2300 |
|
/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.
CI Failure Analysis Report (Unit-Test-AutoRound)
New Issues (1)No.1 — 1 occurrence(s)🔍 AssertionError: 'model.visual.merger.linear_fc2' should be in modules_to_not_convert. Got: []📝 Basic info
🖥️ Log excerpt✨ AI analysis
|
This comment has been minimized.
This comment has been minimized.
Qwen3 Quantization Benchmark Report (0.6B & 8B)Test Environment
Test Configurations
Key Results
All 4 configurations successfully quantized 196/197 layers (lm_head kept unquantized). Timing ComparisonRTN vs OPT (within same scheme)
W4A16 vs RRQ (within same optimization mode)
Per-Layer Breakdown
Memory Comparison
Qwen3-8B Results
All 4 configurations successfully quantized 252/253 layers (lm_head kept unquantized). Device: xpu:1. 8B Timing ComparisonRTN vs OPT (within same scheme)
W4A16 vs RRQ (within same optimization mode)
Cross-Model RRQ/W4 Ratio
8B Per-Layer Breakdown
8B Memory Comparison
Summary (Qwen3-0.6B)
Summary (Qwen3-8B)
|
7006c7a to
f4aae12
Compare
|
/azp run Unit-Test-CUDA-AutoRound |
|
Azure Pipelines successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
🟡 Changes recommended
Critical API, export sequencing, metadata, quantization, and inference issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (12)
auto_round/algorithms/quantization/rrq/quantizer.py:446
- This residual packing path assumes
original_weightis[out_features, in_features], but a TransformersConv1Dweight is stored transposed. It constructs the residualQuantLinearusing the raw dimensions, while the standard exporter transposes Conv1D before packing, so the residual qweight shape/semantics will not match the base artifact and can fail inload_rrq_model. Apply the same Conv1D normalization as the standard exporter before calling_pack_plane.
# Residual plane: store packed INT2 (W2A16 layout) so the
# on-disk artifact is a standard single-plane INT2 layout.
in_features = original_weight.shape[1]
qweight, scales, qzeros = self._pack_plane(quantized, scale, zp, bits, group_size, in_features)
auto_round/algorithms/quantization/rrq/quantizer.py:626
- The configured
dynamic_max_gapearly-stop behavior is not implemented here: after tracking the best loss, this branch executespassfor every positive gap. RRQ tuning therefore always runs all iterations even when users request the same stopping criterion supported by SignRound; add the best-iteration tracking and break condition before merging this path.
if not self.not_use_best_mse and 0 < self.dynamic_max_gap:
# Keep the same early-stop contract as SignRound; the default
# -1 disables this path.
pass
auto_round/export/export_to_autoround/export_to_rrq.py:260
save_quantized_rrq()always pops and renames the module buffers in place, butRRQFormat.save_quantized()accepts the standardinplaceargument and does not pass it through. Thereforesave_quantized(..., inplace=False)still mutates the quantized model and can make a subsequent base export lose the residual buffers, violating the exporter contract.
# Build a state dict containing *only* the packed residual planes, and
# rename the in-memory ``rrq_*_k`` buffers to the on-disk ``*_k`` names.
residual_state: dict[str, torch.Tensor] = {}
for name, module in model.named_modules():
if not hasattr(module, "rrq_total_planes"):
continue
for k in range(1, module.rrq_total_planes):
for src, dst in (
(f"rrq_qweight_{k}", f"qweight_{k}"),
(f"rrq_scales_{k}", f"scales_{k}"),
(f"rrq_qzeros_{k}", f"qzeros_{k}"),
):
if src in module._buffers:
module._buffers[dst] = module._buffers.pop(src)
auto_round/export/export_to_autoround/export_to_rrq.py:561
- The incremental generator has the same Conv1D orientation mismatch: raw GPT-2-style weights are
[in, out], whereasQuantLinear.forward(identity).There is[out, in]. The subtraction in the next step will either fail for non-square layers or generate transposed residuals, so this path cannot generate valid residuals for Conv1D-based LLMs.
# Dequant by running forward on identity
identity = torch.eye(in_features, dtype=torch.float32)
with torch.no_grad():
out = ql.forward(identity) # (in_features, out_features)
W_dequant_base = out.T.to(torch.float32) # (out_features, in_features)
auto_round/export/export_to_autoround/export_to_rrq.py:411
- The public
deviceargument is documented as the computation device, but this implementation forces dequantization, identity inputs, quantization, and packing to CPU (ql.to("cpu"),device="cpu"). Passing a GPU/XPU device therefore has no effect and makes residual generation for a large model unnecessarily CPU-bound. Thread the requested device through the per-layer quantization and packing path.
device: Union[str, torch.device] = "cpu",
auto_round/export/export_to_autoround/export_to_rrq.py:266
- If the standard base exporter has already replaced the RRQ layers, this loop finds no
rrq_*buffers, yet the code still writes an artifact. The resulting residual directory looks valid butload_rrq_modellater fails because every base layer is missing residual planes. Reject an empty residual state before serializing it.
# Serialize the residual state dict (safetensors by default, torch otherwise).
_save_state_dict_sharded(residual_state, output_dir, safe_serialization)
auto_round/export/formats/backends/rrq.py:72
inplaceis part of this backend's save contract, but it is never forwarded tosave_quantized_rrq; that helper renames buffers directly on the original model. Consequentlysave_quantized(..., inplace=False)still mutates and removes therrq_*buffers, which can break later exports or use of the source model. Thread the flag through and clone before renaming, or reject unsupportedinplace=Falseexplicitly.
inplace: Whether to modify the model in place.
auto_round/inference/rrq_model.py:304
- Catching every
Exceptionhere turns real load failures (for example missing/corrupt weights, an invalid config, or OOM) into a config-only model with randomly initialized non-quantized parameters.load_rrq_modelcan therefore return a plausible-looking but incorrect model. Restrict the fallback to a known packed-loading incompatibility and otherwise re-raise, or load non-quantized weights through a controlled path.
except Exception: # pragma: no cover - fall back to architecture-only load
logger.warning(
"from_pretrained(%s) failed; loading architecture without weights.",
auto_round/inference/rrq_model.py:324
- Skipping a missing module leaves that packed base layer unreconstructed, but the function only fails when all layers are skipped. The returned model can therefore contain a normal/random
nn.Linearfor one or more checkpoint layers while silently omitting their residuals. Treat any missing eligible module as an incompatible checkpoint and raise instead of continuing.
try:
base_model.get_submodule(layer_name)
except AttributeError:
logger.warning(f"Base module {layer_name!r} not found; skipping.")
continue
test/unit/test_cpu/algorithms/test_rrq.py:1020
set_rrq_bitsonly traversesRRQLinearmodules, butAutoRound.quantize()returns the original quantized linear modules withrrq_*buffers; the conversion toRRQLinearhappens inload_rrq_model. This call therefore only logs a warning and the following supposed 4-bit evaluation still uses the 8-bit/full-plane model, so the regression test does not exercise precision switching.
set_rrq_bits(model, 4)
test/unit/test_cpu/algorithms/test_rrq.py:200
- This test does not exercise the new
AutoRound.__new__format auto-selection at all; it only resolves the registry entry directly. A regression in the constructor/format path would still pass. Exercise the constructor or CLI path and assert both the selected RRQ config and resolved RRQ format.
# Simulate what _CompressorBuilder.__new__ does:
# when alg_configs is None and format is auto_round:rrq, it picks "rrq".
from auto_round.algorithms.registry import resolve_alg_config, resolve_algorithm_names
config = resolve_alg_config("rrq")
test/unit/test_cpu/algorithms/test_rrq.py:198
resolve_algorithm_namesis imported but never used in this test; the unused local import will be flagged by the repository's Ruff/Pyflakes checks. Remove it (or use it in the assertion).
from auto_round.algorithms.registry import resolve_alg_config, resolve_algorithm_names
- Files reviewed: 26/27 changed files
- Comments generated: 11
- Review effort level: Lite
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
- Auto-set scheme to W2A16 when --format auto_round:rrq is used without --scheme - Force disable_opt_rtn=True in RRQConfig when None (CLI default), preventing the entry from coercing to OptimizedRTNConfig which would drop residual planes
for more information, see https://pre-commit.ci
RRQ is a hidden algorithm (not user-facing), so it shouldn't appear in the builtin algorithm order that defines user-visible enumeration. It remains resolvable by name via _ALG_REGISTRY and _ALIAS_TO_NAME.
…ound:rrq'
Per reviewer feedback, RRQ should use the same quant_method as standard
AutoRound ('auto-round') and differentiate via packing_format instead of
using a separate quant_method. This aligns with the project convention
where quant_method describes the quantization algorithm and packing_format
describes the storage format.
Changes:
- export_to_rrq.py: RRQ_QUANT_METHOD now 'auto-round', added RRQ_PACKING_FORMAT
constant, quantization_config now has both fields
- rrq_model.py: _validate_base_matches_residual checks both quant_method
and packing_format
- GGUF export: check packing_format instead of quant_method
- MLX export: check packing_format instead of quant_method
- Tests updated to verify both fields
for more information, see https://pre-commit.ci
Conv1D (GPT-2 style) stores weights as (in, out) while the quant helpers and QuantLinear.pack expect (out, in). This fix mirrors the standard WrapperLinear._qdq_weight / QuantLinear.pack path: - transpose weight to (out, in) before quantization - restore native (in, out) orientation when storing base plane - also handle in the standard RTN path (_quantize_layer_via_rtn) Also: - add device check to RRQLinear._get_packed_weight cache hit - fix typo 'auto-round-rrq' -> 'auto_round:rrq' in docstring - add Conv1D regression tests (weight shape, plane shape, recon)
The per-call format parameter in save_quantized() was only applied when self.formats was None. This prevented users from saving a base model in standard auto_round format after an RRQ quantize session had set self.formats to 'auto_round:rrq'. Now an explicitly-passed format always overrides self.formats, enabling the documented two-step RRQ export: 1. save_quantized(format='auto_round') -> base artifact 2. save_quantized(format='auto_round:rrq') -> residual artifact Co-Authored-By: GitHub Copilot <273000991+Copilot@users.noreply.github.com>
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
…q_model
The compressor's public quantize_and_save(format="auto_round:rrq") path only
invoked RRQFormat.save_quantized -> save_quantized_rrq, which wrote the residual
artifact but never the base model. The documented workaround (call
save_rrq_base_model after save_quantized_rrq) could not be expressed through
the public API: BaseCompressor.save_quantized cached self.formats after the
first save, so a subsequent save_quantized(format="auto_round") silently
re-emitted the residual, and doing base-first would let pack_layer replace the
nn.Linear modules and drop the rrq_* residual buffers.
Fix (approach B — a dedicated export path that preserves both artifacts):
- export_to_rrq.py: add save_rrq_model(), an orchestrator that writes
{output_dir}/residual/ (save_quantized_rrq, buffers still intact)
{output_dir}/base/ (save_rrq_base_model, standard INT2 export)
The residual-first ordering is enforced here, so callers no longer have to
sequence the two low-level helpers.
- export_to_rrq.py: save_rrq_base_model gains tokenizer/processor params and
its docstring documents the ordering constraint as a low-level-helper note.
- backends/rrq.py: RRQFormat.save_quantized now delegates to save_rrq_model so
the quantize_and_save(format="auto_round:rrq") public path produces both
artifacts (base/ + residual/) in one call.
- base.py: add OutputFormat.is_rrq() helper (mirrors is_fake/is_gptq).
- export_to_autoround/__init__.py: export save_rrq_model alongside the others.
- test_rrq.py: add test_save_rrq_model_ordering_residual_before_base to lock in
the residual-before-base invariant.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The default active_bits=8 caused ValueError for checkpoints with fewer than 4 planes (e.g. num_residual_planes=1 → max 4-bit). Now defaults to None, resolved to total_planes * bits from the loaded config. Explicit values exceeding the checkpoint's max are rejected with a clear error.
for more information, see https://pre-commit.ci
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- RRQ: fix active_bits default from 8 to None (auto-detect from checkpoint) - RRQ: remove W2A16 auto-injection in CLI (auto-scheme handles it) - export_to_mlx: remove unreachable RRQ guard (format routing prevents reaching) - export_to_gguf: remove unreachable RRQ guard (same reasoning) - registry: add docstring to register_algorithm() - README: simplify RRQ description, add example commands - tests: add TestResolveActiveBits unit tests for validation logic
for more information, see https://pre-commit.ci
027407b to
378a129
Compare
| * [2026/09] We now support 5/6/7-bit WOQ models in vLLM and Transformers on CUDA devices, thanks to Humming Kernel. | ||
|
|
||
| * [2026/09] We experimentally support **Recurrent Residual Quantization (RRQ)**. A single checkpoint serves 2/4/6/8-bit — and per-layer mixed precision — selectable at load time without re-quantizing: [*Paper*](https://arxiv.org/abs/2608.04048). | ||
| * [2026/10] We experimentally support **Recurrent Residual Quantization (RRQ)**. A single checkpoint serves 2/4/6/8-bit — and per-layer mixed precision — selectable at load time without re-quantizing: [*Paper*](https://arxiv.org/abs/2608.04048). |
There was a problem hiding this comment.
Could you upload a model to the Intel space and add the link here as an example model? Please also include detailed generation commands and an inference script in the model card. I think this would make things clearer for users.
RRQConfig inherits RTNConfig.register_args, which registers --enable_neuqi with field="enable_neuqi". The CLI contract test (test_registered_cli_fields_are_accepted_by_config_constructors) iterates ALL registered algorithm entries -- including hidden ones like RRQ -- and verifies every registered field is either a named __init__ parameter or a model field. Since RRQ previously only accepted enable_neuqi via **kwargs, the test failed. Fix: declare enable_neuqi as a named parameter in RRQConfig.__init__ and forward it to super().__init__. RTNConfig's existing check then correctly raises when --enable_neuqi is used with RRQ (which forces disable_opt_rtn=True and does not support the optimized RTN path).
- quantizer.py: import logger (was used but never imported) - export_to_rrq.py, rrq_linear.py: convert implicit Optional annotations (T = None) to explicit T | None per PEP 484 (RUF013)
Summary
Recurrent Residual Quantization (RRQ) packs a weight into K sequential INT2 planes (1 base + K−1 residuals). A single checkpoint serves 2 / 4 / 6 / 8-bit weight-only precision — switchable at load/runtime — without re-quantizing, and it is selectable per layer for mixed precision.
Each plane is quantized with the same RTN / SignRound machinery as a standard AutoRound W2A16 model, so per-plane quality parity is guaranteed. The residual planes are stored in the stock AutoRound INT2 layout (
qweight/scales/qzerosper plane, indexed*_k), so they reuse existing W2A16QuantLinearpack/forward kernels with no new runtime.API
Design highlights
search_optimized_init_scale. Withiters>0, per-plane sign-SGD tunes each plane against the block calibration loss while the completed prefix is frozen.effective bits = active_planes × 2(base=2, +1=4, +2=6, +3=8).set_rrq_bits/set_rrq_random_residual/load_rrq_model(residual_fraction=...)select precision without re-quantizing.quant_methodstays"auto-round"; RRQ is distinguished bypacking_format = "auto_round:rrq"(consistent with how AutoRound differentiates formats likeauto_round:auto_gptq)._BUILTIN_ALGORITHM_ORDER. Users opt in via--format auto_round:rrqoralg_configs=RRQConfig(...).disable_opt_rtn=Trueis enforced inRRQConfig.check_config()(RTN alone only produces one plane).Changed files
algorithms/quantization/rrq/{__init__,config,quantizer}.py—RRQConfig,RRQRTNQuantizer,RRQSignRoundQuantizer(per-plane sign-SGD);algorithms/registry.pyexport/export_to_autoround/export_to_rrq.py(save_quantized_rrq,save_rrq_base_model,generate_rrq_residual);export/formats/backends/rrq.py(auto_round:rrqOutputFormat);export/__init__.py,export/formats/backends/__init__.pyinference/rrq_linear.py(RRQLinear,set_rrq_bits,set_rrq_random_residual);inference/rrq_model.py(load_rrq_model);inference/backend.py,inference/__init__.py__init__.py,autoround.py,compressors/model_free.py,cli/{algorithms,main}.py,utils/common.pyexport/export_to_gguf/conversion/base.py,export/export_to_mlx/export.py(reject RRQ residual models, no silent drop)auto_round_extension/torch/qlinear_torch{,_zp}.py(asym packself.device→device)test/unit/test_cpu/algorithms/test_rrq.py(45 unit tests, incl. lm-eval accuracy)Validation
pytest test/unit/test_cpu/algorithms/test_rrq.py— 45 passed (config validation, packed-INT2 storage, residual convergence, sym/asym, forward & precision switching, sign-SGD prefix accumulation, export buffer rename, load validation, incremental residual generation, random-residual mixed-precision config, lm-eval accuracy).Backward compatibility & known limitations
load_rrq_modelbehavior (uniformactive_bits) is unchanged whenresidual_fractionis not supplied.act_bits=16); no activation quantization.