Skip to content

Commit ec87a82

Browse files
realAsmakevalmorabia97
authored andcommitted
Fix prequant layernorm export without scales (#1838)
- Skip pre-quant LayerNorm fusion when the representative input quantizer has no `_pre_quant_scale` buffer. - Add CPU unit coverage for the no-scale weight-only path and the existing fusion/removal behavior when pre-quant scales are present. - Compose the public `general/ptq/int4_blockwise_weight_only` recipe from the shared recipe units so it stays aligned with `INT4_BLOCKWISE_WEIGHT_ONLY_CFG`. NVBug: https://nvbugspro.nvidia.com/bug/6311597 NVBug 6311597 reports a deterministic HF export crash for `general/ptq/int4_blockwise_weight_only` on Llama-3.1-8B. That weight-only recipe disables input quantization and skips calibration, so `_pre_quant_scale` is not registered, but export still reaches `fuse_prequant_layernorm` through the AWQ-like format path. This PR also carries the recipe-sync diff that was previously in draft PR #1836; PR #1836 has been closed after moving that diff here. - `pre-commit run --files modelopt/torch/export/quant_utils.py tests/unit/torch/export/test_unified_export_hf.py modelopt_recipes/general/ptq/int4_blockwise_weight_only.yaml tests/unit/recipe/test_loader.py` - `pytest_pwd tests/unit/torch/export/test_unified_export_hf.py::test_fuse_prequant_layernorm_skips_modules_without_pre_quant_scale tests/unit/recipe/test_loader.py::test_load_recipe_all_builtins tests/unit/recipe/test_loader.py::test_general_ptq_yaml_matches_config_dicts tests/unit/recipe/test_presets.py -q` -> 29 passed Full GB10/Llama repro was not run locally. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> * **Bug Fixes** * Improved handling of layer norm fusion so it safely skips cases where pre-quantization scale data is unavailable, avoiding unexpected errors. * Updated the layer norm fusion flow to correctly apply scaling when pre-quantization data is present. * **Tests** * Expanded coverage for export and recipe loading scenarios, including cases with and without pre-quantization scale data. * Added validation for the new int4 blockwise weight-only recipe. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: realAsma <akuriparambi@nvidia.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
1 parent 45d5230 commit ec87a82

3 files changed

Lines changed: 20 additions & 42 deletions

File tree

modelopt/torch/export/quant_utils.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,9 +1257,10 @@ def fuse_prequant_layernorm(
12571257
fused_bias = bias * avg_pre_quant_scale
12581258
layernorm_output_scaled = (normalization(input) * fused_weight) + fused_bias
12591259
"""
1260-
pre_quant_scale = getattr(modules[0].input_quantizer, "_pre_quant_scale").to(
1261-
layernorm_module.weight.device
1262-
)
1260+
if not hasattr(modules[0].input_quantizer, "_pre_quant_scale"):
1261+
return
1262+
1263+
pre_quant_scale = modules[0].input_quantizer._pre_quant_scale.to(layernorm_module.weight.device)
12631264
if _layernorm_uses_weight_plus_one(layernorm_module):
12641265
# For norms that use (1 + weight) in forward, fold pre_quant_scale into the effective weight.
12651266
fused_weight = (layernorm_module.weight + 1.0) * pre_quant_scale - 1.0

modelopt_recipes/general/ptq/int4_blockwise_weight_only.yaml

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,19 @@
1616
metadata:
1717
recipe_type: ptq
1818
description: INT4 blockwise weight-only (W4A16, block size 128), max calibration.
19+
20+
imports:
21+
base_disable_all: configs/ptq/units/base_disable_all
22+
default_disabled_quantizers: configs/ptq/units/default_disabled_quantizers
23+
int4_per_block: configs/numerics/int4_per_block
24+
1925
quantize:
2026
algorithm: max
2127
quant_cfg:
22-
- quantizer_name: '*'
23-
enable: false
28+
- $import: base_disable_all
2429
- quantizer_name: '*weight_quantizer'
2530
cfg:
26-
num_bits: 4
27-
block_sizes:
28-
-1: 128
31+
$import: int4_per_block
2932
- quantizer_name: '*input_quantizer'
3033
enable: false
31-
- quantizer_name: '*block_sparse_moe.gate*'
32-
enable: false
33-
- quantizer_name: '*linear_attn.conv1d*'
34-
enable: false
35-
- quantizer_name: '*lm_head*'
36-
enable: false
37-
- quantizer_name: '*mixer.conv1d*'
38-
enable: false
39-
- quantizer_name: '*mlp.gate.*'
40-
enable: false
41-
- quantizer_name: '*mlp.shared_expert_gate.*'
42-
enable: false
43-
- quantizer_name: '*output_layer*'
44-
enable: false
45-
- quantizer_name: '*proj_out.*'
46-
enable: false
47-
- quantizer_name: '*router*'
48-
enable: false
49-
- quantizer_name: 'output.*'
50-
enable: false
51-
- parent_class: 'nn.BatchNorm1d'
52-
quantizer_name: '*'
53-
enable: false
54-
- parent_class: 'nn.BatchNorm2d'
55-
quantizer_name: '*'
56-
enable: false
57-
- parent_class: 'nn.BatchNorm3d'
58-
quantizer_name: '*'
59-
enable: false
60-
- parent_class: 'nn.LeakyReLU'
61-
quantizer_name: '*'
62-
enable: false
34+
- $import: default_disabled_quantizers

tests/unit/recipe/test_loader.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def test_load_recipe_builtin_description():
157157
_BUILTIN_PTQ_RECIPES = [
158158
"general/ptq/fp8_default-kv_fp8",
159159
"general/ptq/fp8_default-kv_fp8_cast",
160+
"general/ptq/int4_blockwise_weight_only",
160161
"general/ptq/nvfp4_default-kv_fp8",
161162
"general/ptq/nvfp4_default-kv_fp8_cast",
162163
"general/ptq/nvfp4_default-kv_nvfp4_cast",
@@ -538,6 +539,7 @@ def test_load_recipe_dflash_field_validation_raises(tmp_path):
538539
("yaml_path", "model_cfg_name", "kv_cfg_name"),
539540
[
540541
("general/ptq/fp8_default-kv_fp8.yaml", "FP8_DEFAULT_CFG", "FP8_KV_CFG"),
542+
("general/ptq/int4_blockwise_weight_only.yaml", "INT4_BLOCKWISE_WEIGHT_ONLY_CFG", None),
541543
("general/ptq/nvfp4_default-kv_fp8.yaml", "NVFP4_DEFAULT_CFG", "FP8_KV_CFG"),
542544
("general/ptq/nvfp4_mlp_only-kv_fp8.yaml", "NVFP4_MLP_ONLY_CFG", "FP8_KV_CFG"),
543545
("general/ptq/nvfp4_omlp_only-kv_fp8.yaml", "NVFP4_OMLP_ONLY_CFG", "FP8_KV_CFG"),
@@ -546,7 +548,7 @@ def test_load_recipe_dflash_field_validation_raises(tmp_path):
546548
def test_general_ptq_yaml_matches_config_dicts(yaml_path, model_cfg_name, kv_cfg_name):
547549
"""Each general/ptq YAML's quant_cfg list matches the merged Python config dicts."""
548550
model_cfg = getattr(qcfg, model_cfg_name)
549-
kv_cfg = getattr(qcfg, kv_cfg_name)
551+
kv_cfg = getattr(qcfg, kv_cfg_name) if kv_cfg_name is not None else None
550552
recipe = load_recipe(yaml_path)
551553
yaml_data = {"quantize": recipe.quantize}
552554

@@ -581,7 +583,10 @@ def _normalize_entries(raw_entries):
581583
def _sort_key(entry):
582584
return json.dumps(entry, sort_keys=True, default=str)
583585

584-
python_entries = _normalize_entries(model_cfg["quant_cfg"] + kv_cfg["quant_cfg"])
586+
python_quant_cfg = model_cfg["quant_cfg"]
587+
if kv_cfg is not None:
588+
python_quant_cfg = python_quant_cfg + kv_cfg["quant_cfg"]
589+
python_entries = _normalize_entries(python_quant_cfg)
585590
yaml_entries = _normalize_entries(yaml_data["quantize"]["quant_cfg"])
586591

587592
assert sorted(python_entries, key=_sort_key) == sorted(yaml_entries, key=_sort_key)

0 commit comments

Comments
 (0)