Repository navigation
[2/3] Speed up layerwise GPTQ on MoE models #2864
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
0101143
a1ff4c7
bf99bc3
6cd5d46
5487343
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 | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -114,6 +114,14 @@ | ||||||||||||||||||||||
| from modelopt.torch.utils.vlm_dataset_utils import get_vlm_dataset_dataloader | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| RAND_SEED = 1234 | |||||||||||||||||||||||
| # Tokens per calibration batch when layerwise calibration picks the batch size (16 x 512). | |||||||||||||||||||||||
| LAYERWISE_CALIB_TOKENS = 8192 | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| def _layerwise_calib_batch_size(calib_seq: int, num_samples: int) -> int: | |||||||||||||||||||||||
| """Default layerwise batch size: LAYERWISE_CALIB_TOKENS tokens, but 1 to all of the samples.""" | |||||||||||||||||||||||
| return max(1, min(LAYERWISE_CALIB_TOKENS // calib_seq, num_samples)) | |||||||||||||||||||||||
|
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.
The previous non-positive |
|||||||||||||||||||||||
|
|
|||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| mto.enable_huggingface_checkpointing() | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
|
|
@@ -989,10 +997,15 @@ def quantize_main( | ||||||||||||||||||||||
| ) | |||||||||||||||||||||||
| args.batch_size = 1 | |||||||||||||||||||||||
| # Layerwise calibration processes one layer at a time; auto batch-size probing runs a | |||||||||||||||||||||||
| # full-model forward which defeats the point and can OOM on very large models. | |||||||||||||||||||||||
| # full-model forward which defeats the point and can OOM on very large models. A batch | |||||||||||||||||||||||
| # then only holds one layer's activations, so size it by tokens: batch 1 makes an MoE | |||||||||||||||||||||||
| # layer run its per-expert loop once per sample. | |||||||||||||||||||||||
| elif is_layerwise: | |||||||||||||||||||||||
| print("Layerwise calibration enabled. Using default batch_size=1 for calibration.") | |||||||||||||||||||||||
| args.batch_size = 1 | |||||||||||||||||||||||
| args.batch_size = _layerwise_calib_batch_size(args.calib_seq, sum(args.calib_size)) | |||||||||||||||||||||||
|
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 | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: rg -n -F -e '_layerwise_calib_batch_size' -e 'LAYERWISE_CALIB_TOKENS' -- examples/hf_ptq/hf_ptq.py || test "$?" -eq 1
echo '--- helper and call site ---'
sed -n '1,5p' /dev/null
ast-grep run --lang python --pattern 'def _layerwise_calib_batch_size($$$): $$$' examples/hf_ptq/hf_ptq.py
echo '--- OOM handling in quantization/calibration code ---'
rg -n -i -e 'OutOfMemoryError' -e 'out of memory' -e 'OOM' -e 'torch.cuda.OutOfMemory' -- modelopt/torch/quantization examples/hf_ptq || test "$?" -eq 1
echo '--- layerwise calibration entry points ---'
rg -n -F -e 'layerwise' -- modelopt/torch/quantization/model_calib.py modelopt/torch/quantization/algorithms.py || test "$?" -eq 1Repository: NVIDIA/Model-Optimizer Length of output: 11162 Guard the new layerwise default batch against OOM. At the default Choose one of these changes:
A retry with a halved batch after an OOM error is also possible. It requires more change than the two options above. 🤖 Prompt for AI Agents
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. What's the reasoning behind to change the default BS for layerwise? For some algo like local-hessian we recommend BS=1 now to avoid data padding and ensure correctness.
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. Maybe we can just set --batch_size flag explicitly for a specific experiment setting?
Collaborator
Author
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. Thanks. The reason is speed on HF fused-MoE models. Their experts run a Python loop over experts on every forward, so a forward costs about the same per call whatever the batch: on a Qwen3.8-Flash-Next-shaped layer, the calibration forward over 32 × 512 tokens takes 8.9 s at batch 1 and 1.0 s at batch 16. Batch 1 was the layerwise default only because the auto-probe runs a full-model forward. A layerwise batch holds one layer's activations, so the default is now 8,192 tokens per batch. An explicit On padding, batch size makes no difference in
So batch 1 doesn't keep pad tokens out of the statistics today. That needs per-batch padding, packing, or masking pad positions in the Hessians; I can look at that in a separate PR. Results change with batch size only through floating-point rounding; two batch-1 runs are bit-identical. phi-2, NVFP4 W4A4, layerwise, 128 × 512 cnn_dailymail calibration; KL to BF16 on wikitext-2 (40 × 512 tokens, second half scored), difference paired per chunk (± one standard error):
Neither difference is significant, and they go in opposite directions. I'd like to keep the token-budget default so large MoE runs aren't slow by default. If you'd still rather keep 1, I'll revert it and pass
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. Thanks for the detailed explanation! I agree with your point about padding in the current dataloader implementation. My main concern is that the fixed 8K-token budget may not generalize well across different models and calibration settings, and could potentially lead to OOM for some workloads. Just something to consider for the default behavior. I wonder if a more general solution would be a layerwise-aware batch size probe, similar to what we do for general HF PTQ, but probing a representative layer instead of the full model. That might help balance performance and memory constraints across different workloads.
Collaborator
Author
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. Yes I have PR #2945 to address this
Collaborator
Author
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. Correction: #2945 doesn't change the batch size. It keeps 1.5x the measured calibration-forward peak free when placing Hessians, so Hessians can't crowd out the forward, but a layer whose 8K-token forward doesn't fit will still OOM. A layerwise-aware batch-size probe on a representative layer is a separate change; I'll open a follow-up for it.
Collaborator
Author
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. The layerwise batch probe is now in #2945 (21c8b94). With |
|||||||||||||||||||||||
| print( | |||||||||||||||||||||||
| f"Layerwise calibration enabled. Using default batch_size={args.batch_size} " | |||||||||||||||||||||||
| f"({LAYERWISE_CALIB_TOKENS} tokens per batch) for calibration." | |||||||||||||||||||||||
| ) | |||||||||||||||||||||||
| else: | |||||||||||||||||||||||
| # Calibration/sparsification will actually take much more memory than regular inference | |||||||||||||||||||||||
| # due to intermediate tensors for fake quantization. Setting sample_memory_usage_ratio | |||||||||||||||||||||||
|
|
@@ -1420,6 +1433,9 @@ def parse_args() -> argparse.Namespace: | ||||||||||||||||||||||
| if args.moe_calib_experts_ratio is not None and not (0.0 < args.moe_calib_experts_ratio <= 1.0): | |||||||||||||||||||||||
| parser.error("--moe_calib_experts_ratio must be in the range (0.0, 1.0].") | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| if args.calib_seq <= 0: | |||||||||||||||||||||||
| parser.error("--calib_seq must be positive.") | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| if args.specdec_offline_dataset is not None and args.sparsity_fmt != "dense": | |||||||||||||||||||||||
| parser.error("--specdec_offline_dataset is only supported with --sparsity_fmt dense (PTQ).") | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -339,8 +339,9 @@ def max_calibrate( | |
| skip_forward_without_activation_calib: If True, skip the (potentially expensive) | ||
| ``forward_loop`` when no enabled quantizer needs data-driven activation statistics | ||
| (see :func:`_needs_activation_forward_for_max_calib`). Weight calibration still runs. | ||
| Only opt-in for the top-level ``max`` path; algorithms that always need activations | ||
| (MSE, local Hessian, SmoothQuant, SVDQuant, GPTQ) call this with the default False. | ||
| Opted into by the top-level ``max`` path and by GPTQ and imatrix, which run their own | ||
| forward; algorithms that always need activations (MSE, local Hessian, SmoothQuant, | ||
| SVDQuant) call this with the default False. | ||
|
|
||
| See :class:`MaxCalibConfig <modelopt.torch.quantization.config.MaxCalibConfig>` for | ||
| details on the remaining arguments. | ||
|
|
@@ -2320,12 +2321,14 @@ def gptq( | |
| * **Non-layerwise** (``layerwise.enable=False``): called once on the full | ||
| model. All layers are quantized in parallel from the original activations. | ||
|
|
||
| We recommend enabling layerwise calibration | ||
| (``"layerwise": {"enable": True}``) and using a calibration batch size of 1. | ||
| We recommend enabling layerwise calibration (``"layerwise": {"enable": True}``). The | ||
| calibration batch size only trades memory for speed: each forward of an HF fused-MoE layer | ||
| loops over its experts, so batch 1 pays that loop once per sample. | ||
|
|
||
| Per-module steps: | ||
|
|
||
| 1. ``max_calibrate`` to set amax values from the current activations. | ||
| 1. ``max_calibrate`` to set weight amax, running a forward only when an enabled activation | ||
| quantizer needs data-driven amax. | ||
| 2. Promote eligible quantizers to ``StaticBlockScaleQuantizer`` (two-level scaling). | ||
| 3. Collect Hessian matrices via forward hooks: one per quantized linear layer, and one per | ||
| expert and projection of HF fused-MoE experts, over the tokens routed to that expert. | ||
|
|
@@ -2353,7 +2356,8 @@ def gptq( | |
| _refuse_packed_fused_expert_weights(name, module, "GPTQ", lambda q: q.is_enabled) | ||
|
|
||
| # TODO: Add support for other scale setting strateiges like weight-mse or local-hessian | ||
| max_calibrate(model, forward_loop=forward_loop) | ||
| # The Hessian pass below is GPTQ's own forward; max only runs one if activations need amax. | ||
| max_calibrate(model, forward_loop=forward_loop, skip_forward_without_activation_calib=True) | ||
|
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.
The prior ZeRO-3 materialization finding needs an explicit disposition. |
||
|
|
||
| quantized_layers = [ | ||
| (n, m) | ||
|
|
@@ -2381,7 +2385,7 @@ def gptq( | |
| "exhaust it. Enable layerwise calibration to hold one layer's Hessians at a time." | ||
| ) | ||
|
|
||
| def _make_gptq_handle(name, m, weight_quantizer=None, weight=None): | ||
| def _make_gptq_handle(name, m, weight_quantizer=None, weight=None, log_mse=True): | ||
| quantizer = m.weight_quantizer if weight_quantizer is None else weight_quantizer | ||
| backend = getattr(quantizer, "backend", None) | ||
| if backend is None: | ||
|
|
@@ -2396,6 +2400,7 @@ def _make_gptq_handle(name, m, weight_quantizer=None, weight=None): | |
| importance_weighted=importance_weighted, | ||
| weight_quantizer=weight_quantizer, | ||
| weight=weight, | ||
| log_mse=log_mse, | ||
| ) | ||
|
|
||
| gptq_handles = {name: _make_gptq_handle(name, m) for name, m in quantized_layers} | ||
|
|
@@ -2422,8 +2427,11 @@ def _expert_hook(_input_quantizer, _args, output): | |
| for weight_name, weight, quantizers, input_quantizer in _fused_expert_projections( | ||
| module | ||
| ): | ||
| # An expert's logged error would cost two full matmuls; gptq() logs a summary. | ||
| per_expert = { | ||
| idx: _make_gptq_handle(f"{name}.{weight_name}.{idx}", module, q, weight[idx]) | ||
| idx: _make_gptq_handle( | ||
| f"{name}.{weight_name}.{idx}", module, q, weight[idx], log_mse=False | ||
| ) | ||
| for idx, q in enumerate(quantizers) | ||
| if q.is_enabled | ||
| } | ||
|
|
@@ -2457,13 +2465,21 @@ def _expert_hook(_input_quantizer, _args, output): | |
| with enable_weight_access_and_writeback(handle.module, model, names): | ||
| handle.update_weights(block_size, perc_damp) | ||
| handle.free() | ||
| for _, module in fused_experts: | ||
| for name, module in fused_experts: | ||
| updated = unreached = 0 | ||
| with enable_weight_access_and_writeback(module, model, names): | ||
| for weight, quantizer in module.iter_weights_for_calibration(): | ||
| handle = expert_handles.pop(id(quantizer), None) | ||
| if handle is not None: | ||
| unreached += handle.n_samples == 0 | ||
| handle.update_weights(block_size, perc_damp, weight) | ||
| handle.free() | ||
| updated += 1 | ||
| if updated: | ||
| print_rank_0( | ||
| f"[{name}] GPTQ updated {updated} expert weights, {unreached} of them never " | ||
| "reached by calibration" | ||
| ) | ||
| del gptq_handles, expert_handles | ||
|
|
||
| if torch.cuda.is_available(): | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.