Skip to content

Commit a6cbcba

Browse files
authored
Support mixed-precision per layer quant config in config.json (#929)
## What does this PR do? **Type of change:** ? <!-- Use one of the following: Bug fix, new feature, new example, new tests, documentation. --> **Overview:** Support mixed-precision per layer quant config in config.json, since it's the first-class source of truth in deployment FWs. ## Usage <!-- You can potentially add a usage example below. --> ```python python3 -c " from modelopt.torch.export.convert_hf_config import convert_hf_quant_config_format import json # Test 1: Existing FP8 case still works fp8_config = { 'producer': {'name': 'modelopt', 'version': '0.29.0'}, 'quantization': { 'quant_algo': 'FP8', 'kv_cache_quant_algo': 'FP8', 'exclude_modules': ['lm_head'], }, } result = convert_hf_quant_config_format(fp8_config) print('=== FP8 (existing) ===') print(json.dumps(result, indent=2)) assert result['quant_algo'] == 'FP8' assert 'group_0' in result['config_groups'] assert result['ignore'] == ['lm_head'] # Test 2: Mixed precision mixed_config = { 'producer': {'name': 'modelopt', 'version': '0.29.0'}, 'quantization': { 'quant_algo': 'MIXED_PRECISION', 'kv_cache_quant_algo': 'FP8', 'quantized_layers': { 'model.layers.0.self_attn.q_proj': {'quant_algo': 'FP8'}, 'model.layers.0.self_attn.k_proj': {'quant_algo': 'FP8'}, 'model.layers.0.mlp.gate_proj': {'quant_algo': 'NVFP4', 'group_size': 16}, 'model.layers.0.mlp.up_proj': {'quant_algo': 'NVFP4', 'group_size': 16}, 'model.layers.1.self_attn.q_proj': {'quant_algo': 'FP8'}, }, }, } result = convert_hf_quant_config_format(mixed_config) print() print('=== MIXED_PRECISION ===') print(json.dumps(result, indent=2)) assert result['quant_algo'] == 'MIXED_PRECISION' assert 'config_groups' in result assert 'quantized_layers' in result # Should have 2 groups: one for FP8 layers, one for NVFP4 layers assert len(result['config_groups']) == 2 # Verify per-layer detail is preserved assert 'model.layers.0.self_attn.q_proj' in result['quantized_layers'] assert result['quantized_layers']['model.layers.0.mlp.gate_proj']['quant_algo'] == 'NVFP4' # Check that FP8 group has correct targets for gname, gcfg in result['config_groups'].items(): if gcfg.get('weights', {}).get('num_bits') == 8: assert len(gcfg['targets']) == 3 # 3 FP8 layers elif gcfg.get('weights', {}).get('num_bits') == 4: assert len(gcfg['targets']) == 2 # 2 NVFP4 layers print() print('All tests passed!') " ``` ## Testing <!-- Mention how have you tested your change if applicable. --> ## Before your PR is "*Ready for review*" <!-- If you haven't finished some of the above items you can still open `Draft` PR. --> - **Make sure you read and follow [Contributor guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md)** and your commits are signed. - **Is this change backward compatible?**: Yes <!--- If No, explain why. --> - **Did you write any new necessary tests?**: Yes/No - **Did you add or update any necessary documentation?**: Yes/No - **Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?**: Yes/No <!--- Only for new features, API changes, critical bug fixes or bw breaking changes. --> ## Additional Information <!-- E.g. related issue. --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added support for MIXED_PRECISION quantization configurations in model export, enabling automatic aggregation of layers by their individual quantization settings. * Enhanced quantization config handling to dynamically manage multiple configuration groups for complex quantization scenarios. * Maintained backward compatibility with existing quantization algorithms. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com>
1 parent dfe705a commit a6cbcba

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

modelopt/torch/export/convert_hf_config.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,100 @@
1515

1616
"""Convert modelopt quantization export config to align with llm-compressor config format."""
1717

18+
import warnings
19+
from collections import defaultdict
1820
from typing import Any
1921

2022

23+
def _quant_algo_to_group_config(quant_algo: str, group_size: int | None = None) -> dict[str, Any]:
24+
"""Map a per-layer quant_algo string to compressed-tensors config group details.
25+
26+
Args:
27+
quant_algo: The quantization algorithm name (e.g. "FP8", "NVFP4").
28+
group_size: Optional group size for block-wise quantization algorithms.
29+
30+
Returns:
31+
Dictionary with ``input_activations`` and ``weights`` entries suitable for
32+
a compressed-tensors ``config_groups`` entry.
33+
"""
34+
if quant_algo == "FP8":
35+
return {
36+
"input_activations": {"dynamic": False, "num_bits": 8, "type": "float"},
37+
"weights": {"dynamic": False, "num_bits": 8, "type": "float"},
38+
}
39+
elif quant_algo == "FP8_PER_CHANNEL_PER_TOKEN":
40+
return {
41+
"input_activations": {"dynamic": False, "num_bits": 8, "type": "float"},
42+
"weights": {"dynamic": False, "num_bits": 8, "type": "float", "strategy": "channel"},
43+
}
44+
elif quant_algo == "NVFP4":
45+
gs = group_size or 16
46+
return {
47+
"input_activations": {
48+
"dynamic": False,
49+
"num_bits": 4,
50+
"type": "float",
51+
"group_size": gs,
52+
},
53+
"weights": {"dynamic": False, "num_bits": 4, "type": "float", "group_size": gs},
54+
}
55+
elif quant_algo == "W4A16_AWQ":
56+
gs = group_size or 128
57+
return {
58+
"weights": {"dynamic": False, "num_bits": 4, "type": "int", "group_size": gs},
59+
}
60+
elif quant_algo in ("NVFP4_AWQ", "W4A8_AWQ"):
61+
gs = group_size or 128
62+
return {
63+
"input_activations": {
64+
"dynamic": False,
65+
"num_bits": 8,
66+
"type": "float",
67+
"group_size": gs,
68+
},
69+
"weights": {"dynamic": False, "num_bits": 4, "type": "float", "group_size": gs},
70+
}
71+
elif quant_algo == "W8A16":
72+
return {
73+
"weights": {"dynamic": False, "num_bits": 8, "type": "int"},
74+
}
75+
elif quant_algo == "W8A8_SQ_PER_CHANNEL":
76+
return {
77+
"input_activations": {"dynamic": False, "num_bits": 8, "type": "int"},
78+
"weights": {
79+
"dynamic": False,
80+
"num_bits": 8,
81+
"type": "int",
82+
"strategy": "channel",
83+
},
84+
}
85+
elif quant_algo in ("W4A8_NVFP4_FP8", "W4A8_MXFP4_FP8"):
86+
gs = group_size or 16
87+
return {
88+
"input_activations": {"dynamic": False, "num_bits": 8, "type": "float"},
89+
"weights": {"dynamic": False, "num_bits": 4, "type": "float", "group_size": gs},
90+
}
91+
elif quant_algo == "MXFP8":
92+
gs = group_size or 32
93+
return {
94+
"input_activations": {
95+
"dynamic": False,
96+
"num_bits": 8,
97+
"type": "float",
98+
"group_size": gs,
99+
},
100+
"weights": {"dynamic": False, "num_bits": 8, "type": "float", "group_size": gs},
101+
}
102+
else:
103+
warnings.warn(
104+
f"Unsupported quantization algorithm '{quant_algo}' in "
105+
f"_quant_algo_to_group_config. The resulting config group will not contain "
106+
f"'input_activations' or 'weights' keys and may not be compatible with "
107+
f"compressed-tensors consumers. Please add explicit support for this algorithm."
108+
)
109+
return {"quant_algo": quant_algo}
110+
111+
21112
def convert_hf_quant_config_format(input_config: dict[str, Any]) -> dict[str, Any]:
22113
"""Converts modelopt quantization config dictionary to align with llm-compressor config format.
23114
@@ -92,6 +183,30 @@ def convert_hf_quant_config_format(input_config: dict[str, Any]) -> dict[str, An
92183
"targets": ["Linear"],
93184
}
94185
new_config["config_groups"] = {"group_0": config_group_details}
186+
elif quant_algo_value == "MIXED_PRECISION":
187+
quantized_layers = original_quantization_details.get("quantized_layers", {})
188+
189+
# Group layers by their unique quantization config so each distinct
190+
# (quant_algo, group_size, ...) combination becomes one config_group.
191+
algo_to_layers: dict[tuple, list[str]] = defaultdict(list)
192+
for layer_name, layer_cfg in quantized_layers.items():
193+
# Create a hashable key from the layer config
194+
key = tuple(sorted(layer_cfg.items()))
195+
algo_to_layers[key].append(layer_name)
196+
197+
config_groups: dict[str, Any] = {}
198+
for idx, (config_key, layer_names) in enumerate(algo_to_layers.items()):
199+
layer_cfg = dict(config_key)
200+
algo = layer_cfg.get("quant_algo", "")
201+
layer_group_size = layer_cfg.get("group_size")
202+
203+
group_config = _quant_algo_to_group_config(algo, layer_group_size)
204+
group_config["targets"] = sorted(layer_names)
205+
config_groups[f"group_{idx}"] = group_config
206+
207+
new_config["config_groups"] = config_groups
208+
# Preserve the full per-layer detail for consumers that need it.
209+
new_config["quantized_layers"] = quantized_layers
95210

96211
exclude_modules = original_quantization_details.get("exclude_modules")
97212

0 commit comments

Comments
 (0)