Skip to content

Commit c7f98a2

Browse files
committed
Merge branch 'lab/fix-jointq-qep' into 'export/v1-1-1'
Raise clear error for unsupported QEP quantizers See merge request onecomp/onecomp-lab!71
2 parents 85cba91 + ea5dd45 commit c7f98a2

6 files changed

Lines changed: 126 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## [v1.1.1] 2026-05-07
44

5+
## Bug Fixes
6+
7+
- Raise a clear error when ``Runner`` is configured with ``qep=True`` and a quantizer that does not support QEP (currently `JointQ`). Previously the run failed deep inside `quantize_with_qep` / `adjust_weight` with a confusing low-level error. `Runner.check()` now reports e.g. "Quantizer 'JointQ' (or one of its candidate quantizers) does not support QEP (Quantization Error Propagation). Set qep=False, or use a QEP-compatible quantizer (e.g., GPTQ, DBF, AutoBitQuantizer with QEP-compatible candidates)." Implementation: added `flag_qep_supported` (default `True`) on `Quantizer`, set to `False` on `JointQ`, and propagated via `AutoBitQuantizer._sync_flags` (only `True` when *all* candidate quantizers support QEP).
8+
9+
## Tests
10+
11+
- Added `tests/onecomp/test_runner_check.py` covering the new `qep=True` validation path: JointQ + qep=True raises a clear `ValueError`, while JointQ + qep=False and GPTQ + qep=True both pass `Runner.check()`.
12+
513
## [v1.1.0] 2026-04-16
614

715
### Gemma 3 / Gemma 4 & VLM Support

onecomp/quantizer/_quantizer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class Quantizer(metaclass=ABCMeta):
174174
flag_calibration: bool = False
175175
flag_hessian: bool = False
176176
flag_xtx: bool = False # Whether X^T X is needed (e.g., JointQ)
177+
flag_qep_supported: bool = True
177178

178179
def __post_init__(self):
179180
"""__post_init__ method"""

onecomp/quantizer/autobit/_autobit.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,11 @@ def _sync_flags(self):
393393
self.flag_calibration = any(q.flag_calibration for q in self.quantizers)
394394
self.flag_hessian = any(q.flag_hessian for q in self.quantizers)
395395
self.flag_xtx = any(q.flag_xtx for q in self.quantizers)
396+
# AutoBit supports QEP only when *all* candidate quantizers support it
397+
# (the per-layer assignment may dispatch to any child quantizer).
398+
self.flag_qep_supported = all(
399+
q.flag_qep_supported for q in self.quantizers
400+
)
396401

397402
def _validate_manual_fused_consistency(self):
398403
"""Check that manual keyword rules don't split fused groups."""

onecomp/quantizer/jointq/_jointq.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ class JointQ(Quantizer):
250250
flag_calibration: bool = True
251251
flag_hessian: bool = False
252252
flag_xtx: bool = True
253+
# JointQ does not yet support the generic QEP pipeline.
254+
# Planned for a future release.
255+
flag_qep_supported: bool = False
253256
hessian_dtype: torch.dtype = torch.float64
254257

255258
# Parameters for the JointQ quantizer

onecomp/runner.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,15 @@ def check(self):
291291
)
292292
if self.multi_gpu and not self.quantizer.flag_calibration:
293293
raise ValueError("'multi_gpu' requires a quantizer with flag_calibration=True.")
294+
if self.qep and not self.quantizer.flag_qep_supported:
295+
raise ValueError(
296+
f"Quantizer '{type(self.quantizer).__name__}' "
297+
f"(or one of its candidate quantizers) does not support "
298+
f"QEP (Quantization Error Propagation). "
299+
f"Set qep=False, or use a QEP-compatible quantizer "
300+
f"(e.g., GPTQ, DBF, AutoBitQuantizer with "
301+
f"QEP-compatible candidates)."
302+
)
294303

295304
# Cross-validate calibration_dataset when AutoBitQuantizer is used
296305
quantizer = self.quantizer or (self.quantizers[0] if self.quantizers else None)

tests/onecomp/test_runner_check.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Unit tests for ``Runner.check()`` parameter validation.
2+
3+
These tests exercise only the configuration validation path
4+
(``Runner.check()``) and therefore do not require a GPU or model
5+
loading.
6+
7+
Copyright 2025-2026 Fujitsu Ltd.
8+
"""
9+
10+
import pytest
11+
12+
from onecomp import CalibrationConfig, ModelConfig, Runner
13+
from onecomp.quantizer.autobit import AutoBitQuantizer
14+
from onecomp.quantizer.gptq import GPTQ
15+
from onecomp.quantizer.jointq import JointQ
16+
17+
18+
def _model_config() -> ModelConfig:
19+
return ModelConfig(
20+
model_id="TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T",
21+
device="cuda:0",
22+
)
23+
24+
25+
class TestRunnerCheckQEPSupport:
26+
"""``qep=True`` must raise a clear error when the quantizer does
27+
not support the generic QEP pipeline.
28+
"""
29+
30+
def test_jointq_with_qep_true_raises_clear_error(self):
31+
"""JointQ + qep=True should raise a clear ValueError, not an
32+
obscure error from deep inside the QEP runtime.
33+
"""
34+
runner = Runner(
35+
model_config=_model_config(),
36+
quantizer=JointQ(bits=4, group_size=128),
37+
calibration_config=CalibrationConfig(max_length=128, num_calibration_samples=8),
38+
qep=True,
39+
)
40+
41+
with pytest.raises(ValueError, match=r"JointQ.*does not support QEP"):
42+
runner.check()
43+
44+
def test_jointq_with_qep_false_passes_check(self):
45+
"""JointQ + qep=False is the supported configuration."""
46+
runner = Runner(
47+
model_config=_model_config(),
48+
quantizer=JointQ(bits=4, group_size=128),
49+
calibration_config=CalibrationConfig(max_length=128, num_calibration_samples=8),
50+
qep=False,
51+
)
52+
runner.check()
53+
54+
def test_gptq_with_qep_true_passes_check(self):
55+
"""GPTQ supports QEP, so check() must pass."""
56+
runner = Runner(
57+
model_config=_model_config(),
58+
quantizer=GPTQ(wbits=4, groupsize=128),
59+
calibration_config=CalibrationConfig(max_length=128, num_calibration_samples=8),
60+
qep=True,
61+
)
62+
runner.check()
63+
64+
def test_autobit_with_jointq_candidate_and_qep_true_raises(self):
65+
"""AutoBit with a JointQ candidate must also raise on qep=True.
66+
67+
``AutoBitQuantizer.flag_qep_supported`` is True only when *all*
68+
candidate quantizers support QEP, so a JointQ candidate must
69+
propagate the unsupported state.
70+
"""
71+
autobit = AutoBitQuantizer(
72+
quantizers=[GPTQ(wbits=4), JointQ(bits=2)],
73+
target_bit=3.0,
74+
)
75+
runner = Runner(
76+
model_config=_model_config(),
77+
quantizer=autobit,
78+
calibration_config=CalibrationConfig(max_length=128, num_calibration_samples=8),
79+
qep=True,
80+
)
81+
82+
with pytest.raises(
83+
ValueError,
84+
match=r"AutoBitQuantizer.*does not support QEP",
85+
):
86+
runner.check()
87+
88+
def test_autobit_with_only_gptq_candidates_and_qep_true_passes(self):
89+
"""AutoBit with only QEP-compatible candidates must pass."""
90+
autobit = AutoBitQuantizer(
91+
quantizers=[GPTQ(wbits=4), GPTQ(wbits=2)],
92+
target_bit=3.0,
93+
)
94+
runner = Runner(
95+
model_config=_model_config(),
96+
quantizer=autobit,
97+
calibration_config=CalibrationConfig(max_length=128, num_calibration_samples=8),
98+
qep=True,
99+
)
100+
runner.check()

0 commit comments

Comments
 (0)