Skip to content

Commit 058601e

Browse files
committed
Validate AutoQuant search checkpoint inputs
Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com>
1 parent 4a25092 commit 058601e

3 files changed

Lines changed: 160 additions & 10 deletions

File tree

modelopt/torch/quantization/algorithms.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,11 @@ def _module_search_space_signature(module_search_spaces) -> tuple:
508508
)
509509

510510

511+
def _quantization_formats_signature(quant_recipes) -> tuple[str, ...]:
512+
"""Return a checkpoint-stable description of the global candidate formats."""
513+
return tuple(sorted(recipe.checkpoint_signature for recipe in quant_recipes))
514+
515+
511516
class _AutoQuantizeBaseSearcher(BaseSearcher, ABC):
512517
"""Base searcher for AutoQuantize algorithm."""
513518

@@ -569,6 +574,7 @@ def default_state_dict(self) -> SearchStateDict:
569574
"cost": {},
570575
"active_moe_expert_ratio": None,
571576
"cost_denominator": None,
577+
"quantization_formats_signature": None,
572578
"module_search_space_signature": None,
573579
"disabled_layers": None,
574580
"candidate_stats": defaultdict(dict),
@@ -911,11 +917,29 @@ def before_search(self):
911917
module_search_spaces = self._normalize_module_search_spaces(
912918
self.config["module_search_spaces"]
913919
)
920+
default_search_recipes = self._get_search_recipes(self.config["quantization_formats"])
921+
quantization_formats_signature = _quantization_formats_signature(default_search_recipes)
914922
module_search_space_signature = _module_search_space_signature(module_search_spaces)
923+
restored_quantization_formats_signature = getattr(
924+
self, "quantization_formats_signature", None
925+
)
915926
restored_module_search_space_signature = getattr(
916927
self, "module_search_space_signature", None
917928
)
918929
has_restored_calibration_or_scores = bool(self.quantizer_states or self.candidate_stats)
930+
if has_restored_calibration_or_scores and restored_quantization_formats_signature is None:
931+
raise ValueError(
932+
"Checkpoint does not record its quantization_formats signature and cannot be "
933+
"safely reused. Use a different checkpoint path."
934+
)
935+
if (
936+
has_restored_calibration_or_scores
937+
and restored_quantization_formats_signature != quantization_formats_signature
938+
):
939+
raise ValueError(
940+
"Checkpoint quantization_formats do not match the current search config. "
941+
"Use a different checkpoint path."
942+
)
919943
if has_restored_calibration_or_scores and (
920944
(restored_module_search_space_signature is None and module_search_space_signature)
921945
or (
@@ -927,9 +951,9 @@ def before_search(self):
927951
"Checkpoint module_search_spaces do not match the current search config. "
928952
"Use a different checkpoint path."
929953
)
954+
self.quantization_formats_signature = quantization_formats_signature
930955
self.module_search_space_signature = module_search_space_signature
931956

932-
default_search_recipes = self._get_search_recipes(self.config["quantization_formats"])
933957
search_recipes = sorted(
934958
{
935959
*default_search_recipes,

modelopt/torch/quantization/model_quant.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -519,9 +519,13 @@ def _process_quantization_formats(formats, custom_name_prefix):
519519
processed.append((quant_cfg, name))
520520
return processed
521521

522+
if not isinstance(quantization_formats, list):
523+
raise TypeError("`quantization_formats` must be a list.")
524+
if not quantization_formats:
525+
raise ValueError("`quantization_formats` must be a non-empty list.")
522526
processed_quantization_formats = _process_quantization_formats(quantization_formats, "CUSTOM")
523-
524-
assert len(processed_quantization_formats) > 0, "`quantization_formats` should not be empty"
527+
if not processed_quantization_formats:
528+
raise ValueError("`quantization_formats` must contain at least one non-None format.")
525529

526530
processed_module_search_spaces = []
527531
for idx, search_space in enumerate(module_search_spaces or []):
@@ -545,12 +549,15 @@ def _process_quantization_formats(formats, custom_name_prefix):
545549
raise ValueError(
546550
"module_search_spaces.module_name_patterns must be a non-empty string list."
547551
)
548-
formats = _process_quantization_formats(
549-
search_space.get("quantization_formats") or [], f"CUSTOM_MODULE_{idx}"
550-
)
552+
raw_formats = search_space.get("quantization_formats")
553+
if not isinstance(raw_formats, list):
554+
raise TypeError("module_search_spaces.quantization_formats must be a list.")
555+
if not raw_formats:
556+
raise ValueError("module_search_spaces.quantization_formats must be a non-empty list.")
557+
formats = _process_quantization_formats(raw_formats, f"CUSTOM_MODULE_{idx}")
551558
if not formats:
552559
raise ValueError(
553-
"module_search_spaces.quantization_formats must contain at least one format."
560+
"module_search_spaces.quantization_formats must contain at least one non-None format."
554561
)
555562
allow_no_quant = search_space.get("allow_no_quant", True)
556563
if not isinstance(allow_no_quant, bool):

tests/unit/torch/quantization/test_autoquant.py

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import modelopt.torch.opt as mto
2626
import modelopt.torch.quantization as mtq
27+
import modelopt.torch.quantization.model_quant as model_quant
2728
from modelopt.torch.quantization._auto_quantize_cost import (
2829
EXCLUDED_MODULE_NAME_PATTERNS_KEY,
2930
_get_module_weight_numel,
@@ -39,7 +40,7 @@
3940
estimate_quant_compression,
4041
)
4142
from modelopt.torch.quantization.config import _base_disable_all, _default_disabled_quantizer_cfg
42-
from modelopt.torch.utils import safe_load
43+
from modelopt.torch.utils import safe_load, safe_save
4344
from modelopt.torch.utils.distributed import DistributedProcessGroup
4445

4546

@@ -366,9 +367,51 @@ def test_auto_quantize_module_search_spaces_keep_fixed_routed_experts_costed():
366367
assert routed_hparam.active == int4_recipe
367368

368369

369-
def test_auto_quantize_fixed_module_isolated_from_unrelated_calibration(monkeypatch):
370-
import modelopt.torch.quantization.model_quant as model_quant
370+
@pytest.mark.parametrize("formats", ["FP8_DEFAULT_CFG", mtq.FP8_DEFAULT_CFG, ()])
371+
def test_auto_quantize_rejects_non_list_global_formats(formats):
372+
with pytest.raises(TypeError, match="`quantization_formats` must be a list"):
373+
mtq.auto_quantize(TransformerBlock(), quantization_formats=formats)
374+
375+
376+
@pytest.mark.parametrize("formats", [[], [None]])
377+
def test_auto_quantize_rejects_empty_global_formats(formats):
378+
with pytest.raises(ValueError, match="`quantization_formats` must"):
379+
mtq.auto_quantize(TransformerBlock(), quantization_formats=formats)
380+
381+
382+
@pytest.mark.parametrize("formats", ["FP8_DEFAULT_CFG", mtq.FP8_DEFAULT_CFG, ()])
383+
def test_auto_quantize_rejects_non_list_module_formats(formats):
384+
with pytest.raises(
385+
TypeError, match=r"module_search_spaces\.quantization_formats must be a list"
386+
):
387+
mtq.auto_quantize(
388+
TransformerBlock(),
389+
quantization_formats=[mtq.INT8_DEFAULT_CFG],
390+
module_search_spaces=[
391+
{
392+
"module_name_patterns": ["*mlp*"],
393+
"quantization_formats": formats,
394+
}
395+
],
396+
)
397+
398+
399+
@pytest.mark.parametrize("formats", [[], [None]])
400+
def test_auto_quantize_rejects_empty_module_formats(formats):
401+
with pytest.raises(ValueError, match=r"module_search_spaces\.quantization_formats must"):
402+
mtq.auto_quantize(
403+
TransformerBlock(),
404+
quantization_formats=[mtq.INT8_DEFAULT_CFG],
405+
module_search_spaces=[
406+
{
407+
"module_name_patterns": ["*mlp*"],
408+
"quantization_formats": formats,
409+
}
410+
],
411+
)
371412

413+
414+
def test_auto_quantize_fixed_module_isolated_from_unrelated_calibration(monkeypatch):
372415
model = TransformerBlock()
373416
calibration_states = []
374417
original_calibrate = model_quant.calibrate
@@ -1011,6 +1054,82 @@ def interrupt_after_calibration(self):
10111054
)
10121055

10131056

1057+
def test_auto_quantize_calibration_only_checkpoint_validates_global_formats_and_legacy(
1058+
tmp_path, monkeypatch
1059+
):
1060+
checkpoint_path = str(tmp_path / "autoquant_calibration_only_checkpoint.pth")
1061+
legacy_checkpoint_path = str(tmp_path / "autoquant_legacy_calibration_only_checkpoint.pth")
1062+
original_estimate_scores = AutoQuantizeGradientSearcher.estimate_sensitivity_scores
1063+
1064+
def interrupt_after_calibration(self):
1065+
raise RuntimeError("interrupt after calibration")
1066+
1067+
monkeypatch.setattr(
1068+
AutoQuantizeGradientSearcher,
1069+
"estimate_sensitivity_scores",
1070+
interrupt_after_calibration,
1071+
)
1072+
model = TransformerBlock()
1073+
with pytest.raises(RuntimeError, match="interrupt after calibration"):
1074+
mtq.auto_quantize(
1075+
model,
1076+
constraints={"effective_bits": 6.0},
1077+
quantization_formats=[
1078+
mtq.INT4_BLOCKWISE_WEIGHT_ONLY_CFG,
1079+
mtq.INT8_DEFAULT_CFG,
1080+
],
1081+
data_loader=[model.get_input()],
1082+
forward_step=lambda model, batch: model(batch),
1083+
loss_func=lambda output, data: output.sum(),
1084+
num_calib_steps=1,
1085+
num_score_steps=1,
1086+
checkpoint=checkpoint_path,
1087+
)
1088+
1089+
saved = safe_load(checkpoint_path)
1090+
assert saved["quantizer_states"]
1091+
assert saved["quantization_formats_signature"]
1092+
legacy_saved = dict(saved)
1093+
legacy_saved.pop("quantization_formats_signature")
1094+
safe_save(legacy_saved, legacy_checkpoint_path)
1095+
1096+
monkeypatch.setattr(
1097+
AutoQuantizeGradientSearcher,
1098+
"estimate_sensitivity_scores",
1099+
original_estimate_scores,
1100+
)
1101+
mismatched_model = TransformerBlock()
1102+
with pytest.raises(ValueError, match="quantization_formats do not match"):
1103+
mtq.auto_quantize(
1104+
mismatched_model,
1105+
constraints={"effective_bits": 6.0},
1106+
quantization_formats=[mtq.INT4_BLOCKWISE_WEIGHT_ONLY_CFG, mtq.FP8_DEFAULT_CFG],
1107+
data_loader=[mismatched_model.get_input()],
1108+
forward_step=lambda model, batch: model(batch),
1109+
loss_func=lambda output, data: output.sum(),
1110+
num_calib_steps=1,
1111+
num_score_steps=1,
1112+
checkpoint=checkpoint_path,
1113+
)
1114+
1115+
legacy_model = TransformerBlock()
1116+
with pytest.raises(ValueError, match="does not record its quantization_formats signature"):
1117+
mtq.auto_quantize(
1118+
legacy_model,
1119+
constraints={"effective_bits": 6.0},
1120+
quantization_formats=[
1121+
mtq.INT4_BLOCKWISE_WEIGHT_ONLY_CFG,
1122+
mtq.INT8_DEFAULT_CFG,
1123+
],
1124+
data_loader=[legacy_model.get_input()],
1125+
forward_step=lambda model, batch: model(batch),
1126+
loss_func=lambda output, data: output.sum(),
1127+
num_calib_steps=1,
1128+
num_score_steps=1,
1129+
checkpoint=legacy_checkpoint_path,
1130+
)
1131+
1132+
10141133
@pytest.mark.parametrize("method", ["gradient", "kl_div"])
10151134
def test_get_auto_quantize_config(method):
10161135
model = TransformerBlock()

0 commit comments

Comments
 (0)