Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4f33f20
Carry the per-rank expert split the no-gather writer calls
shengliangxu Oct 29, 2026
afe23a9
Write a sharded model's HF checkpoint without gathering it
shengliangxu Oct 11, 2026
3de2041
Pick the HF export's route by the model's distribution plan
shengliangxu Oct 14, 2026
3f2ce92
Record the plan the no-gather export tests wrap their models by
shengliangxu Oct 15, 2026
95f0b1a
Note in the changelog that a distributed model must carry its plan
shengliangxu Oct 15, 2026
7413183
File the no-gather export entries under 0.49.0 in the changelog
shengliangxu Oct 15, 2026
4ef871b
Run the no-gather writer's two-rank test on the shared gloo workers
shengliangxu Oct 15, 2026
4283681
Expect the cross-rank name collision on every rank in the two-rank wr…
shengliangxu Oct 15, 2026
c97f616
Guard the fused-expert fold's FSDP2 window with a real export
shengliangxu Oct 16, 2026
6ba1e4a
Check on CPU what the GPU-only no-gather export tests checked
shengliangxu Oct 16, 2026
4e0157f
Trim the no-gather export's tests: fold its end-to-end cases, drop co…
shengliangxu Oct 16, 2026
98dda9a
Run the writer's input-scale reduce on the CPU under a gloo-only group
shengliangxu Oct 16, 2026
3b6066e
Check the extra tensors' overlap inside a collective every rank joins
shengliangxu Oct 16, 2026
34d62b8
Drop the export config helper's second generation-config write
shengliangxu Oct 16, 2026
39a8398
Join the extra tensors' overlap check on every rank, holding extras o…
shengliangxu Oct 17, 2026
aa4814e
Say what picks the no-gather writer in the distributed export test
shengliangxu Oct 22, 2026
87304a9
Resolve a mixed-precision KV cache on the no-gather export, and take …
shengliangxu Oct 24, 2026
0e0c496
Keep the no-gather export's quant config names in step with its weights
shengliangxu Oct 24, 2026
ef9af9f
Copy only inference tensors before the no-gather export's transfer
shengliangxu Oct 25, 2026
59d543d
Drop get_max_batch_size from the changelog's plan-required entry
shengliangxu Oct 25, 2026
3ee4765
Save the ModelOpt state from the no-gather export, as the streaming e…
shengliangxu Oct 27, 2026
32b2ec5
Agree on the expert input-scale keys before the writer reduces them
shengliangxu Oct 27, 2026
b921ae2
Keep the blank line ruff format wants before the carried-tensors comment
shengliangxu Oct 27, 2026
4b404ad
Keep each merge group on one rank in the no-gather export
shengliangxu Oct 27, 2026
1f318dc
Refuse a MODELOPT_FSDP2_EXPORT value the export does not know
shengliangxu Oct 27, 2026
8b2e412
Test that an uneven inference-mode shard is written with its global s…
shengliangxu Oct 27, 2026
1159619
Drop the no-gather arm's unused tied-weight map
shengliangxu Oct 27, 2026
2926d00
Use the writer's module-scope imports in the no-gather export
shengliangxu Oct 29, 2026
f22b9dc
Record the plan hf_ptq's FSDP2 load shards the model by
shengliangxu Oct 29, 2026
30ecd64
Raise a calibration out-of-memory error for a model over several ranks
shengliangxu Oct 29, 2026
c9ffe80
Say who records the plan the export reads, now that the loader does not
shengliangxu Oct 29, 2026
d43544f
Say the no-gather export does not overwrite an earlier checkpoint
shengliangxu Oct 29, 2026
5bfe811
Restore the full stop the no-gather README note lost
shengliangxu Oct 29, 2026
23c0b55
Record the plan in the GPU tests that export a model they shard by hand
shengliangxu Oct 29, 2026
db65c36
Say what sets keep_fused_experts now that the no-gather export does
shengliangxu Oct 29, 2026
c167a6a
Drop the expert-parallel scaffolding the FSDP2 writer never sets
shengliangxu Oct 29, 2026
c57a8cf
Import the writer's helpers at module scope, and say why the writer i…
shengliangxu Oct 29, 2026
4ce6a49
Patch the state-dict read where the writer now imports it
shengliangxu Oct 29, 2026
f6dfb95
Write the no-gather export's config files with save_non_weight_artifacts
shengliangxu Oct 29, 2026
7c5955d
Number the local split's experts by their FSDP2 offset alone
shengliangxu Oct 30, 2026
55f0bd9
Leave AWQ out of the README's list of calibrations refused when quant…
shengliangxu Oct 30, 2026
ac29323
Fix stale comments on the carried tensors, consolidation and EP
shengliangxu Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Changelog
- Speed up NVFP4 QAT and QAD of Hugging Face fused MoE experts: all experts of a projection are now fake-quantized in one kernel launch instead of one per expert, about 2x faster per MoE layer on Qwen3-30B-A3B with FSDP2.
- ``hf_ptq.py`` calibrates Qwen3.8 (``qwen4_exp``) without running its QSA indexer's per-query Python loop when every visible key fits the indexer's token budget, with bit-identical results. On Qwen3.8-Flash-Next this saves about two hours of layerwise calibration.
- Speed up Hugging Face QAT and QAD with ``QATTrainer``, ``QADTrainer`` or ``--manual_gc``: the ``gc.collect()`` run before each training step now skips the model and optimizer state, saving about 1 s per step (30%) on Qwen3-30B-A3B.
- Add an opt-in no-gather HuggingFace export for FSDP2-sharded models: with ``MODELOPT_FSDP2_EXPORT=nogather``, ``export_hf_checkpoint`` sends each tensor to a single owner rank and every rank writes its own shards in parallel, instead of gathering the full state dict on one host. MoE experts stay sharded through the export, so each rank only materializes its own experts.

*Speculative Decoding*

Expand All @@ -46,6 +47,7 @@ Changelog

**Backward Breaking Changes**

- ``examples/hf_ptq/hf_ptq.py --use_fsdp2`` on more than one GPU now refuses calibration that keeps its statistics on each rank, which calibrated the ranks' shards of a weight differently: AWQ-Lite, AWQ-Full, SVDQuant, local Hessian and imatrix, also as an AutoQuantize candidate (run them without ``--use_fsdp2``). It also raises when a calibration batch runs out of GPU memory, instead of retrying it in smaller pieces: set ``--batch_size`` to one that fits.
- ``export_mcore_gpt_to_hf`` given a Hugging Face Hub model ID no longer writes the tokenizer, processor, generation config or remote-code files. Pass a local directory instead, e.g. from ``modelopt.torch.export.ensure_local_checkpoint``.
- Drop support for Python 3.10, which has reached end-of-life. ModelOpt now requires Python 3.11 or later.
- Bump minimum required PyTorch version to 2.10.
Expand All @@ -59,6 +61,7 @@ Changelog
- ``mtq.calibrate`` now raises an error, naming the modules, when a Hugging Face fused-experts forward reaches neither quantizer hook (a hand-written ``bmm`` forward that transformers cannot redirect, say), instead of exporting weight-derived input scales with a warning.

- Layerwise-calibration resume (``checkpoint_dir``) now loads checkpoint files with ``weights_only=True``. A checkpoint directory written by an earlier release whose layer inputs hold objects other than tensors, plain containers or Hugging Face dynamic caches can no longer be resumed; recalibrate in a fresh directory, or disable checkpointing.
- ``export_hf_checkpoint`` now reads how a model is distributed from the plan recorded on it, and raises for an FSDP2-sharded model, or one in a multi-rank run, that carries none. ``examples/hf_ptq/hf_ptq.py --use_fsdp2`` records it; if you distribute a model yourself, for example with ``fsdp2_wrap`` or ``parallel_load_and_prepare_fsdp2``, which record no plan, record one with ``modelopt.torch.utils.distributed.set_distribution_plan`` before calibrating or exporting it.

**Deprecations**

Expand Down
8 changes: 7 additions & 1 deletion examples/hf_ptq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,14 +610,18 @@ mtq.calibrate(model, algorithm="max", forward_loop=calibrate_loop)

ModelOpt enables quantization of LLMs across multiple GPU nodes using FSDP2 for distributed model sharding and calibration, exposed via the `--use_fsdp2` flag on the standard `hf_ptq.py` entry point.

> *Weight and KV-cache AutoQuantize with `--use_fsdp2` are experimental.*
> *Weight and KV-cache AutoQuantize with `--use_fsdp2` are experimental, and refused when a candidate format is one of those refused below.*

> *Nemotron-H checkpoints containing MTP weights are not supported with `--use_fsdp2` and are rejected before distributed model loading. Remove `--use_fsdp2` to load and calibrate the MTP modules; the distributed loader does not construct them.*

> *AWQ calibration (`awq_lite`, `awq_full`), as the AWQ formats and recipes use, AutoQuantize candidates included, is not supported with `--use_fsdp2` on more than one rank and is rejected before distributed model loading: each rank would fold its own smoothing scale into its weight shard. Drop `--use_fsdp2` (the single-process `device_map` still spreads the model over all the GPUs), run on one rank, or use a non-AWQ format.*

> *With `--use_fsdp2` on more than one GPU, calibration other than AWQ that keeps its statistics on each rank is refused when quantization starts: SVDQuant (`--qformat nvfp4_svdquant`), local Hessian (`nvfp4_w4a4_weight_local_hessian`), imatrix (the `iq*` formats), the recipes that use them, and AutoQuantize recipes with such a candidate format. Each rank calibrates on its own share of the data, so the ranks would calibrate their shards of a weight differently. Run these without `--use_fsdp2`. For AWQ, see the note above.*

> *`nvfp4_awq_clip` still works with `--use_fsdp2` on more than one GPU: it calibrates with AWQ-Clip alone, which folds no smoothing scale into the weights, and its clip search averages each candidate clip's loss over the ranks, so every rank takes the same clip.*

> *With `--use_fsdp2` on more than one GPU, a calibration batch that runs out of memory is not retried in smaller pieces, since a rank that runs out of memory partway through a forward would leave the others waiting in its collectives. With `--batch_size 0`, each rank estimates the batch size from one forward and the ranks use the smallest, which is not probed for out-of-memory either: set `--batch_size` if calibration runs out of memory.*

### Usage

#### Slurm (recommended)
Expand Down Expand Up @@ -654,6 +658,8 @@ Qwen checkpoints with `model_type` `qwen3_5` or `qwen3_5_moe` support this path

> *Performance Note: FSDP2 is designed for training workloads and may result in longer calibration and export times. For faster calibration, maximize the batch size based on available GPU memory and choose the right number of GPUs to avoid unnecessary communication.*

> *Set `MODELOPT_FSDP2_EXPORT=nogather` to have every rank write its own shards of the checkpoint in parallel instead of gathering the full state dict on one host. It does not overwrite a checkpoint already in the export directory.*

## Evaluate Accuracy

### TensorRT-LLM Validation
Expand Down
5 changes: 5 additions & 0 deletions examples/hf_ptq/hf_ptq.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
get_max_batch_size,
get_supported_datasets,
)
from modelopt.torch.utils.distributed import DistributionPlan, set_distribution_plan
from modelopt.torch.utils.memory_monitor import launch_memory_monitor
from modelopt.torch.utils.mlflow import add_mlflow_args, resolve_mlflow_args
from modelopt.torch.utils.plugins.hf_checkpoint_utils import (
Expand Down Expand Up @@ -339,6 +340,10 @@ def load_model(args: argparse.Namespace):
attn_implementation=args.attn_implementation,
hf_config=hf_config,
)
# Calibration sizes its batch, and the export writes, by how the model is distributed.
set_distribution_plan(
full_model, DistributionPlan(args.dist_state.world_size, dense="fsdp")
)
Comment on lines +343 to +346

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] This recording closes the CI gap I raised last pass (the two GPU suites now record their own plans), so the slice stands on its own. What's left is that the plan is recorded by every caller of parallel_load_and_prepare_fsdp2 rather than by the loader, and the identical one-liner now appears at three sites, all immediately after the loader returns:

  • here, examples/hf_ptq/hf_ptq.py:344
  • tests/gpu/torch/utils/test_model_load_utils.py:68 and :114
  • (plus the hand-fsdp2_wrap sites in tests/gpu/torch/export/test_export_carry_over.py)

Two things follow:

  1. parallel_load_and_prepare_fsdp2 is public (modelopt.torch.utils.plugins.model_load_utils) and is the only distributed loader ModelOpt ships today. A library caller that pairs it with export_hf_checkpoint now gets RuntimeError: ... is FSDP2-sharded, but has no distribution plan recorded, and the remedy is the exact line above. The loader knows world_size and that it FSDP2-shards (fsdp2_wrap at model_load_utils.py:845), so set_distribution_plan(model, DistributionPlan(world_size, dense="fsdp")) inside it would serve all three call sites and leave nothing for users to remember. Note that would then need this call dropped, since set_distribution_plan refuses to record twice.
  2. resolve_distribution_plan's docstring (model_load_utils.py:1103) already asserts the loader-side contract — "a distributed load loads the model by the plan this returns and records it on the model" — which no shipped loader satisfies. A reader of that docstring will not expect to need this line. Either record in the loader, or soften that sentence to say which loaders record and which do not, matching the CHANGELOG entry that now names parallel_load_and_prepare_fsdp2 as recording none.

Non-blocking: the behaviour is documented under Backward Breaking Changes and the error names the fix, so this is a duplication/discoverability point, not a defect.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving the recording with the callers.

elif args.specdec_offline_dataset is not None or not args.low_memory_mode:
full_model = get_model(
args.local_checkpoint_path,
Expand Down
Loading
Loading