-
Notifications
You must be signed in to change notification settings - Fork 603
[OMNIML-5899] Export IQ checkpoints from HF and Megatron #2447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d789b45
9f9519b
13cdcec
542b601
11cd58d
e14a1e9
b8a966b
106686d
070baa5
a232c1a
0ba91aa
5d0bbab
68b7230
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,14 @@ | |
|
|
||
| from modelopt import __version__ | ||
| from modelopt.torch.models import get_spec, list_all_possible | ||
| from modelopt.torch.quantization.ggml import ( | ||
| IQ1_S_BLOCK_BYTES, | ||
| IQ1_S_BLOCK_SIZE, | ||
| IQ1_S_EFFECTIVE_BITS, | ||
| IQ2_XS_BLOCK_BYTES, | ||
| IQ2_XS_BLOCK_SIZE, | ||
| IQ2_XS_EFFECTIVE_BITS, | ||
| ) | ||
|
Comment on lines
+29
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: find modelopt/torch/quantization -maxdepth 3 -type f | sort | sed -n '1,180p'
sed -n '20,45p' modelopt/torch/export/quant_utils.py
sed -n '55,75p' modelopt/torch/export/unified_export_hf.py
sed -n '25,45p' modelopt/torch/export/unified_export_megatron.py
python - <<'PY'
import importlib
for name in (
"modelopt.torch.quantization.ggml",
"modelopt.torch.export.quant_utils",
"modelopt.torch.export.unified_export_hf",
"modelopt.torch.export.unified_export_megatron",
):
try:
importlib.import_module(name)
print(name, "OK")
except Exception as exc:
print(name, type(exc).__name__, str(exc))
PYRepository: NVIDIA/Model-Optimizer Length of output: 6549 🏁 Script executed: set -eu
printf '%s\n' '--- dependency declarations ---'
rg -n -C 4 'ggml|nvidia-modelopt|modelopt.*dependency|dependencies|optional-dependencies|extras' pyproject.toml setup.py setup.cfg requirements* 2>/dev/null || true
printf '%s\n' '--- package metadata and ggml references ---'
sed -n '1,100p' modelopt/__init__.py
rg -n -C 3 'quantization\.ggml|quantize_iq1_s|quantize_iq2_xs|IQ1_S_BLOCK|IQ2_XS_BLOCK' modelopt pyproject.toml tests docs 2>/dev/null || true
printf '%s\n' '--- exact export module imports ---'
sed -n '1,100p' modelopt/torch/export/unified_export_hf.py
sed -n '1,90p' modelopt/torch/export/unified_export_megatron.py
printf '%s\n' '--- relevant current diff summary ---'
git diff --stat -- modelopt/torch/export/quant_utils.py modelopt/torch/export/unified_export_hf.py modelopt/torch/export/unified_export_megatron.py pyproject.tomlRepository: NVIDIA/Model-Optimizer Length of output: 21128 🏁 Script executed: set -eu
printf '%s\n' '--- project dependency block and packaging ---'
sed -n '15,155p' pyproject.toml
printf '%s\n' '--- export package initializer ---'
if test -f modelopt/torch/export/__init__.py; then cat -n modelopt/torch/export/__init__.py; else printf '%s\n' 'missing'; fi
printf '%s\n' '--- all tracked ggml/provider candidates ---'
git ls-files | rg -n '(^|/)(ggml|.*ggml.*|.*modelopt.*gpu.*)(/|$)|quantization/__init__|pyproject|requirements' || true
printf '%s\n' '--- current status and changed paths ---'
git status --short
git diff --name-onlyRepository: NVIDIA/Model-Optimizer Length of output: 10227 Add the provider for Add the provider for the exact 🤖 Prompt for AI Agents |
||
| from modelopt.torch.quantization.model_calib import ( | ||
| enable_stats_collection, | ||
| finish_stats_collection, | ||
|
|
@@ -62,6 +70,8 @@ | |
| QUANTIZATION_INT4_AWQ, | ||
| QUANTIZATION_INT8_SQ, | ||
| QUANTIZATION_INT8_WO, | ||
| QUANTIZATION_IQ1_S, | ||
| QUANTIZATION_IQ2_XS, | ||
| QUANTIZATION_MXFP4, | ||
| QUANTIZATION_MXFP8, | ||
| QUANTIZATION_NONE, | ||
|
|
@@ -440,6 +450,36 @@ def get_weight_block_size(module: nn.Module, weight_name: str = "weight") -> int | |
| return 0 | ||
|
|
||
|
|
||
| def uses_iq_quantization(module) -> bool: | ||
| """Whether any weight quantizer in ``module`` or its children targets an IQ format. | ||
|
|
||
| ``get_quantization_format`` returns the *first* non-``NONE`` format it finds, so in a | ||
| mixed-format model IQ layers sitting behind, say, an FP8 layer are invisible to it. Callers | ||
| that must reject IQ specifically need to see every layer. | ||
|
|
||
| This reads ``num_bits`` directly rather than resolving each layer's full format, so an | ||
| unrelated unsupported quantizer elsewhere in the model cannot turn the check into an error. | ||
|
|
||
| Known gap, shared with ``get_quantization_format``: ``weight_attr_names`` yields nothing for | ||
| a TEGroupedLinear, whose parameters are ``weight0..N`` while its quantizer is a single | ||
| ``GroupedQuantizer`` under ``weight_quantizer``. Neither function sees such a module, so an | ||
| experts-only IQ model reports no format at all -- not just here. Closing it belongs in | ||
| ``weight_attr_names``, where it affects every format, rather than in this helper. | ||
| """ | ||
| for weight_name in weight_attr_names(module): | ||
| weight_quantizer = representative_weight_quantizer(module, weight_name) | ||
| # getattr: a SequentialQuantizer has is_enabled but no num_bits, and is never IQ -- | ||
| # IQ is a single quantizer with backend="ggml". | ||
| if ( | ||
| weight_quantizer is not None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This walk may not see grouped-MoE experts. Net effect: a model whose IQ quantizers live only on grouped MoE experts (an expert-only recipe is a plausible shape for 1–2 bit formats) returns
|
||
| and weight_quantizer.is_enabled | ||
| and getattr(weight_quantizer, "num_bits", None) | ||
| in (QUANTIZATION_IQ1_S, QUANTIZATION_IQ2_XS) | ||
| ): | ||
| return True | ||
| return any(uses_iq_quantization(child) for _, child in module.named_children()) | ||
|
|
||
|
|
||
| def get_quantization_format(module) -> str | None: | ||
| """Gets the quantization string. | ||
|
|
||
|
|
@@ -474,6 +514,24 @@ def _get_quantization_from_layer(layer, quantizer_attr_names: QuantizerAttrNames | |
| return QUANTIZATION_W4A8_AWQ | ||
|
|
||
| # Handle individual num_bits cases | ||
| if weight_quantizer.num_bits in (QUANTIZATION_IQ1_S, QUANTIZATION_IQ2_XS): | ||
| if weight_quantizer.backend != "ggml": | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This branch validates the backend but not the activation side, and both exporters return early for IQ before they collect So a W-IQ + A-FP8 (or AWQ-smoothed) config calibrates cleanly and then exports as weight-only with if input_quantizer is not None and input_quantizer.is_enabled:
raise ValueError(f"{weight_quantizer.num_bits} is weight-only; disable the input quantizer")
if getattr(input_quantizer, "_pre_quant_scale", None) is not None:
raise ValueError("pre_quant_scale (SmoothQuant/AWQ) is not supported with IQ formats")Both export paths then inherit the guard, and the two |
||
| raise ValueError("IQ formats require the built-in 'ggml' quantization backend") | ||
| # Both exporters return before collecting input_scale and before the pre_quant_scale | ||
| # handling below, so an enabled activation quantizer would be dropped without a trace | ||
| # and the checkpoint would load as weight-only. Refuse instead. | ||
| if input_quantizer is not None and input_quantizer.is_enabled: | ||
| raise NotImplementedError( | ||
| "IQ1_S/IQ2_XS export is weight-only, but this layer has an enabled input " | ||
| "quantizer. The GGML block payload carries no activation scale, so the " | ||
| "activation quantization would be silently lost." | ||
| ) | ||
| if input_quantizer is not None and hasattr(input_quantizer, "_pre_quant_scale"): | ||
| raise NotImplementedError( | ||
| "IQ1_S/IQ2_XS export does not support an AWQ-style pre_quant_scale." | ||
| ) | ||
| return weight_quantizer.num_bits | ||
|
|
||
| if weight_quantizer.num_bits == 4: | ||
| assert len(weight_quantizer.block_sizes) > 0 and weight_quantizer.block_sizes[-1] > 0, ( | ||
| "Invalid block_sizes for INT4 quantizer" | ||
|
|
@@ -722,6 +780,26 @@ def process_layer_quant_config(layer_config_dict): | |
| "quant_algo": "MXFP8", | ||
| "group_size": block_size_value, | ||
| } | ||
| elif v in (QUANTIZATION_IQ1_S, QUANTIZATION_IQ2_XS): | ||
| if v == QUANTIZATION_IQ1_S: | ||
| block_size = IQ1_S_BLOCK_SIZE | ||
| payload_bytes = IQ1_S_BLOCK_BYTES | ||
| effective_bits = IQ1_S_EFFECTIVE_BITS | ||
| else: | ||
| block_size = IQ2_XS_BLOCK_SIZE | ||
| payload_bytes = IQ2_XS_BLOCK_BYTES | ||
| effective_bits = IQ2_XS_EFFECTIVE_BITS | ||
| if block_size_value != block_size: | ||
| raise ValueError( | ||
| f"{v.upper()} requires block size {block_size}, got {block_size_value}" | ||
| ) | ||
| layer_config = { | ||
| "quant_algo": v.upper(), | ||
| "group_size": block_size, | ||
| "effective_bits": effective_bits, | ||
| "block_payload_bytes": payload_bytes, | ||
| "packing": "ggml", | ||
| } | ||
| else: | ||
| layer_config = {"quant_algo": v} | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[SUGGESTION] Two things about this block, both about keeping one source of truth for the IQ block contract.
Duplicated metadata table. This exact derivation (block size / payload bytes / effective bits, plus the
packing: "ggml"marker) is repeated verbatim inquant_utils.process_layer_quant_config(lines 740-759), and the two copies already validate differently: heregroup_size in (None, block_size)is accepted, thereblock_size_value != block_sizeraises (so a missingawq_block_size→0is a hard error). Adding a third IQ format, or changingIQ2_XS_BLOCK_BYTES, means touching both. A single helper next to the format constants (e.g.iq_block_metadata(quant_algo) -> dictinquant_format.py) would remove the drift risk, per CONTRIBUTING's "don't repeat yourself; keep a single source of truth". The same applies to the membership testin (QUANTIZATION_IQ1_S, QUANTIZATION_IQ2_XS), which is now spelled out ~10 times acrossquant_utils.py,unified_export_hf.py, andunified_export_megatron.py— a module-levelIQ_FORMATS = frozenset({...})alongsideFUSION_FREE_FORMATSwould make a future IQ3 a one-line change.Two different config shapes for the same information. For a uniform IQ export these keys land at the root of
quantization_config(line 242-244), which is whatdocs/source/deployment/3_unified_hf.rstdocuments. ForMIXED_PRECISIONthe same dict goes through line 286-288 and ends up insideconfig_groups["group_N"](withtargetsappended) — the shape asserted bytest_mixed_iq_config_group_does_not_claim_integer_weight_schema. A loader written against the new docs will look for root-levelpacking/block_payload_bytesand find nothing for a mixed export. Given this PR deliberately rejects layouts that no deployment loader owns yet (fused-MoE), consider either documenting the mixed-precision placement in the same doc section, or rejecting IQ insideMIXED_PRECISIONuntil a loader exists.