Skip to content

Commit 1377465

Browse files
committed
fix: layerwise calibration backward-compat, recipe split, batch-size guard
- config: accept legacy `use_sequential` via AliasChoices on `layerwise` so pre-#1251 PTQ checkpoints load; still serializes as `layerwise` - recipes: split nvfp4_experts_only-fp8_kv into default (no layerwise) and _layerwise variants - hf_ptq: auto batch-size detection not supported with layerwise; default to batch_size=1 in that case - tests: cover alias accept, current-name accept, dump under current name, and extra='forbid' still rejecting unknowns Signed-off-by: realAsma <akuriparambi@nvidia.com>
1 parent 946639a commit 1377465

5 files changed

Lines changed: 148 additions & 10 deletions

File tree

examples/llm_ptq/hf_ptq.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,18 @@ def quantize_main(
955955
default_pad_token,
956956
device: torch.device,
957957
):
958+
# Load the recipe up front so we can detect layerwise calibration before batch-size probing.
959+
recipe = None
960+
if args.recipe is not None and not args.auto_quantize_bits:
961+
print(f"Use recipe {args.recipe} for quantization")
962+
recipe = load_recipe(args.recipe)
963+
assert isinstance(recipe, ModelOptPTQRecipe), (
964+
f"Expected PTQ recipe, but got {type(recipe).__name__} from {args.recipe}"
965+
)
966+
967+
recipe_algorithm = recipe.quantize.model_dump().get("algorithm") if recipe else None
968+
is_layerwise = isinstance(recipe_algorithm, dict) and recipe_algorithm.get("layerwise", False)
969+
958970
if args.batch_size == 0:
959971
# For VL models with image-text calibration, skip automatic batch size detection
960972
# since get_max_batch_size can't handle multimodal inputs
@@ -968,6 +980,11 @@ def quantize_main(
968980
"Offline speculative decoding calibration enabled. Using default batch_size=1 for calibration."
969981
)
970982
args.batch_size = 1
983+
# Layerwise calibration processes one layer at a time; auto batch-size probing runs a
984+
# full-model forward which defeats the point and can OOM on very large models.
985+
elif is_layerwise:
986+
print("Layerwise calibration enabled. Using default batch_size=1 for calibration.")
987+
args.batch_size = 1
971988
else:
972989
# Calibration/sparsification will actually take much more memory than regular inference
973990
# due to intermediate tensors for fake quantization. Setting sample_memory_usage_ratio
@@ -1027,12 +1044,7 @@ def quantize_main(
10271044
else:
10281045
# mono quantization
10291046

1030-
if args.recipe is not None:
1031-
print(f"Use recipe {args.recipe} for quantization")
1032-
recipe = load_recipe(args.recipe)
1033-
assert isinstance(recipe, ModelOptPTQRecipe), (
1034-
f"Expected PTQ recipe, but got {type(recipe).__name__} from {args.recipe}"
1035-
)
1047+
if recipe is not None:
10361048
quant_cfg = recipe.quantize.model_dump()
10371049

10381050
else:

modelopt/torch/quantization/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@
154154
import warnings
155155
from typing import Any, Literal, cast
156156

157-
from pydantic import ValidationInfo, field_validator, model_validator
157+
from pydantic import AliasChoices, ValidationInfo, field_validator, model_validator
158158
from typing_extensions import Required, TypedDict
159159

160160
from modelopt.torch.opt.config import ModeloptBaseConfig, ModeloptField
@@ -1219,6 +1219,7 @@ class QuantizeAlgorithmConfig(ModeloptBaseConfig):
12191219

12201220
layerwise: bool = ModeloptField(
12211221
default=False,
1222+
validation_alias=AliasChoices("layerwise", "use_sequential"),
12221223
title="Enable layerwise (layer-by-layer) calibration.",
12231224
description=(
12241225
"If True, the calibration algorithm is applied layer by layer. "

modelopt_recipes/general/ptq/nvfp4_experts_only-fp8_kv.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@
1515

1616
metadata:
1717
recipe_type: ptq
18-
description: NVFP4 static weight and dynamic activation for expert layers only (W4A4), FP8 KV cache, max layerwise calibration.
18+
description: NVFP4 static weight and dynamic activation for expert layers only (W4A4), FP8 KV cache, max calibration.
1919
quantize:
2020
algorithm:
2121
method: max
22-
# Max calibration is fast and does not typically need checkpointing.
23-
layerwise: true
2422
quant_cfg:
2523
- quantizer_name: '*'
2624
enable: false
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
metadata:
17+
recipe_type: ptq
18+
description: NVFP4 static weight and dynamic activation for expert layers only (W4A4), FP8 KV cache, max layerwise calibration.
19+
quantize:
20+
algorithm:
21+
method: max
22+
# Max calibration is fast and does not typically need checkpointing.
23+
layerwise: true
24+
quant_cfg:
25+
- quantizer_name: '*'
26+
enable: false
27+
- quantizer_name: '*mlp.experts*weight_quantizer'
28+
enable: true
29+
cfg:
30+
block_sizes:
31+
-1: 16
32+
type: dynamic
33+
scale_bits: e4m3
34+
num_bits: e2m1
35+
- quantizer_name: '*mlp.experts*input_quantizer'
36+
enable: true
37+
cfg:
38+
block_sizes:
39+
-1: 16
40+
type: dynamic
41+
scale_bits: e4m3
42+
num_bits: e2m1
43+
- quantizer_name: '*block_sparse_moe*weight_quantizer'
44+
enable: true
45+
cfg:
46+
block_sizes:
47+
-1: 16
48+
type: dynamic
49+
scale_bits: e4m3
50+
num_bits: e2m1
51+
- quantizer_name: '*block_sparse_moe*input_quantizer'
52+
enable: true
53+
cfg:
54+
block_sizes:
55+
-1: 16
56+
type: dynamic
57+
scale_bits: e4m3
58+
num_bits: e2m1
59+
- quantizer_name: '*[kv]_bmm_quantizer'
60+
enable: true
61+
cfg:
62+
num_bits: e4m3
63+
- quantizer_name: '*block_sparse_moe.gate*'
64+
enable: false
65+
- quantizer_name: '*linear_attn.conv1d*'
66+
enable: false
67+
- quantizer_name: '*lm_head*'
68+
enable: false
69+
- quantizer_name: '*mixer.conv1d*'
70+
enable: false
71+
- quantizer_name: '*mlp.gate.*'
72+
enable: false
73+
- quantizer_name: '*mlp.shared_expert_gate.*'
74+
enable: false
75+
- quantizer_name: '*output_layer*'
76+
enable: false
77+
- quantizer_name: '*proj_out.*'
78+
enable: false
79+
- quantizer_name: '*router*'
80+
enable: false
81+
- quantizer_name: 'output.*'
82+
enable: false
83+
- parent_class: 'nn.BatchNorm1d'
84+
quantizer_name: '*'
85+
enable: false
86+
- parent_class: 'nn.BatchNorm2d'
87+
quantizer_name: '*'
88+
enable: false
89+
- parent_class: 'nn.BatchNorm3d'
90+
quantizer_name: '*'
91+
enable: false
92+
- parent_class: 'nn.LeakyReLU'
93+
quantizer_name: '*'
94+
enable: false

tests/unit/torch/quantization/test_config_validation.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
INT4_AWQ_CFG,
2626
NVFP4_DEFAULT_CFG,
2727
W4A8_AWQ_BETA_CFG,
28+
MaxCalibConfig,
2829
QuantizeConfig,
2930
find_quant_cfg_entry_by_path,
3031
need_calibration,
@@ -525,3 +526,35 @@ def test_validate_quant_cfg_entries_accepts_valid_cfg(self):
525526
algorithm="max",
526527
)
527528
assert len(cfg.quant_cfg) == 2
529+
530+
531+
class TestLayerwiseUseSequentialAlias:
532+
"""`layerwise` accepts the legacy `use_sequential` name via validation_alias.
533+
534+
Old PTQ checkpoints serialized the field as `use_sequential` before #1251 renamed
535+
it to `layerwise`. AliasChoices lets those checkpoints load without a migration
536+
validator while still serializing under the current name.
537+
"""
538+
539+
def test_use_sequential_true_sets_layerwise(self):
540+
cfg = MaxCalibConfig(use_sequential=True)
541+
assert cfg.layerwise is True
542+
543+
def test_use_sequential_false_sets_layerwise(self):
544+
cfg = MaxCalibConfig(use_sequential=False)
545+
assert cfg.layerwise is False
546+
547+
def test_layerwise_name_still_accepted(self):
548+
cfg = MaxCalibConfig(layerwise=True)
549+
assert cfg.layerwise is True
550+
551+
def test_serializes_under_current_name(self):
552+
"""Dump must use `layerwise`, not the legacy alias."""
553+
dumped = MaxCalibConfig(use_sequential=True).model_dump()
554+
assert dumped["layerwise"] is True
555+
assert "use_sequential" not in dumped
556+
557+
def test_unknown_field_still_rejected(self):
558+
"""extra='forbid' must still reject unrelated unknown fields."""
559+
with pytest.raises(ValidationError):
560+
MaxCalibConfig(not_a_real_field=True)

0 commit comments

Comments
 (0)