Skip to content

[OMNIML-6085] [21/21] Run expert-parallel PTQ from hf_ptq.py - #2754

Open
shengliangxu wants to merge 29 commits into
shengliangx/hfep-11c-kimi-k3from
shengliangx/hfep-14-hf-ptq
Open

shengliangxu wants to merge 29 commits into
shengliangx/hfep-11c-kimi-k3from
shengliangx/hfep-14-hf-ptq

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: New feature

hf_ptq.py had one distributed path, --use_fsdp2, which shards a dense model and replicates every expert of an MoE. This PR wires the expert-parallel path ([1/21]-[20/21]) into it, so the documented CLI reaches an expert-parallel run: recipes, export formats, deprecations.

  • Process group. main() runs inside distributed_run ([4/21]), which owns the process group on a distributed launch. The run takes this process's DistributedState (rank, size, device) from it, with --device for a single process. FSDP2 on one rank still shards through a process group, of one, which the load sets up ([6/21]) and distributed_run destroys with the run. The example's own DistributedState, setup_distributed_args and process-group helpers are gone.
  • One decision, read everywhere. load_model decides the plan once, with resolve_distribution_plan ([5/21], [12/21], [13/21]), and loads every model by it through load_pretrained ([6/21]): expert-parallel, FSDP2, get_model's whole-model loads and the low-memory load. Everything after reads the plan back from the model, not the flag or the process group: the calibration device, the data sharding, the calibration loop, and get_model's GPU placement.
    • MODELOPT_EP_DEGREE and MODELOPT_FSDP_SIZE override the automatic choices.
    • --use_fsdp2 asks the decision for FSDP2 and no expert parallelism.
  • Refusals. A plan that shards the whole model with FSDP2 raises without --use_fsdp2, the flag this script's FSDP2 checks hang on, which now run before anything is set up. resolve_distribution_plan refuses a plan that would quietly replicate an expert-parallel-capable MoE, and a model whose spec refuses expert parallelism (GPT-OSS, Llama-4, [13/21]) raises unless --use_fsdp2 rules expert parallelism out. A plan that FSDP2-shards the dense part within several expert-parallel groups (the automatic degree when the world size does not divide the expert count), which the export cannot write yet, raises before loading and names the settings that avoid it.
  • Export. A model the plan replicates, such as a small dense model under torchrun, is exported by rank 0 alone; every rank used to write the same directory and race on config.json. A multimodal model's source config is rank 0's in every plan.
  • Calibration.
    • Every distributed run calibrates on its own 1/world shard of the data ([8/21]).

    • Expert-parallel runs go through distributed_forward_loop, which re-plants the all-to-all forwards that quantizer insertion clobbers on transformers 5.16 and later.

    • FSDP2 keeps main's plain loop, with its split-on-OOM retry.

    • The calibration algorithms under a plan that spans several ranks:

      Algorithms
      Supported and run on GPUs max (amax all-reduced over the data-, expert- and tensor-parallel groups), mse (searches the weights max leaves identical on every rank)
      Supported, not yet run on GPUs gptq (Hessians merged across each module's data-parallel replicas), smoothquant (scales from max's synced amax), nvfp4_act_headroom (activation scales MAX-reduced over data parallelism)
      Refused before quantizing ([8/21]) awq_lite, awq_clip, awq_full, svdquant, local_hessian, imatrix, and max or mse with distributed_sync off, also as another algorithm's weight or scale algorithm: each keeps its statistics on its rank, so the ranks' copies of a weight would come out different
      Not assessed lsq's learnable scales (its first scale calibration follows the rows above)
  • Previews keep a sharded model's ranks together. Each rank previews the first sample of its own calibration shard, so the ranks finish decoding at different steps. Under expert parallelism or FSDP2 every forward runs a collective, and a rank that stopped decoding left the others blocked in it: Qwen3-30B-A3B at NVFP4 and EP4 hung in the post-PTQ preview. The previews, Nemotron-VL's included, now generate with synced_gpus when the model's plan shards it.
  • GPU placement. When the plan gives every one of several ranks the whole model, each rank pins it to its own GPU. A single process keeps spreading a model too big for one GPU, as before.
  • Docs. README section "Expert-Parallel Post-Training Quantization for MoE Models": how to launch, what the knobs override, which models are refused.
  • Memory report fix. launch_memory_monitor probed NVML before initializing it. Every probe raised Uninitialized, so the per-GPU peak-memory report never started on any host where nothing else had initialized NVML. The probe now initializes NVML itself.

Usage

# One node, 8 GPUs: the experts shard over all 8 ranks, each calibrating its own data.
torchrun --nproc_per_node 8 examples/hf_ptq/hf_ptq.py \
    --pyt_ckpt_path Qwen/Qwen3-30B-A3B --qformat nvfp4 --export_path /out/qwen3-30b-a3b-nvfp4

Testing

  • New tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py (31 cases; the decision itself is tested in [5/21], [12/21] and [13/21]):
    • what the script adds to the decision: FSDP2 without expert parallelism for --use_fsdp2, and FSDP2 sharding refused unless --use_fsdp2;
    • load_model loading by the plan: an EP plan and --use_fsdp2 through load_pretrained and no other loader, a data-parallel one through get_model;
    • per-rank GPU pinning by the plan, and single-process placement left alone;
    • main() running inside distributed_run, which it leaves even when the run raises, and handing the run the state distributed_run yields, with --device for a single process;
    • per-rank calibration data;
    • the distributed loop, with the EP mesh, for an expert-parallel plan, and the plain one for FSDP2.
    • the previews, before and after quantization and Nemotron-VL's, generating with synced_gpus exactly when the plan shards the model (EP or FSDP2 over several ranks);
    • exporting a replicated model from rank 0 only, and every rank of a sharded one;
    • the plan the export cannot write refused before loading, while the world-size degree still resolves;
    • Mutation-checked: each test fails under a deliberate break of the code it covers.
  • New 2-GPU smoke test test_ptq_qwen3_moe_expert_parallel (tests/examples/hf_ptq/test_llm_ptq.py): torchrun --nproc_per_node 2 hf_ptq.py with MODELOPT_EP_DEGREE=2 on a tiny Qwen3-MoE and the FP8 recipe. It checks that every expert of both ranks is exported FP8 with its weight scale, and that the config names FP8. It is the tests' one real expert-parallel run of the script; the file above covers the wiring with stand-ins. It passed on 2 GB200s in 29.5 s at f4bb395488 (2026-10-16), which carries this test on a revision before the latest loader and calibration fixes ([7/21], [8/21]). It skips on CI's 1-GPU PR runner and runs on 2-GPU runners.
  • tests/examples/hf_ptq/test_hf_ptq_model_type.py (main's): the preview tests' stand-in models are modules now, since the previews read their distribution plan.
  • New tests/unit/torch/utils/test_memory_monitor.py, against a fake reference-counted NVML: the monitor starts when NVML can report memory (this fails on the old code), and is skipped with NVML shut down when it cannot.
  • Plan tests the library's and get_model's tests cover are dropped, and the load-call, calibration-loop and memory-monitor pairs are one parametrized test each. Each drop is checked by mutation.
  • Locally on this revision, the reordered series rebased on main 200011e: the full unit suite on transformers 5.18, and on 5.5 apart from the tests whose ONNX, TensorRT or diffusers packages that environment lacks; CI's transformers-free unit lane; pre-commit over the whole PR against main; and the Sphinx docs build. The same at every PR of the stack.
  • The GPU results below were measured on earlier revisions of this branch, before this script's loads moved onto load_pretrained ([6/21], [12/21]); this revision's GPU tests run in CI only. The series' GPU tests on this branch: 19 passed on a 4-GPU node. On 1 GPU, CI's PR runner, 6 passed and 13 skipped. They cover:
    • the expert-parallel load with the dense part replicated and sharded, at EP 4 and EP 2;
    • amax sync;
    • the distributed export.
  • Against the series' base, on one GPU node:
    • nox -s gpu (all of tests/gpu, 1 GPU, CI's PR-runner shape): every test failing on this branch also fails at the base, and the only new tests are the series' own.
    • The hf_ptq example lane (tests/examples/hf_ptq, TensorRT-LLM image, 1 GPU): no outcome changes on the 296 shared tests. The series' 30 pass, except 2 that need transformers 5.7.
    • tests/gpu_megatron (2 GPUs): no outcome changes over the 311 tests both runs reached. The rest time out identically at the base in that image.
  • Expert-parallel runs end to end. All of them predate [3/21]'s expert weight-scale fix: on every rank but rank 0, an expert's weight scale came out too large wherever rank 0's expert at the same index had the larger one, which costs precision but never clips; the fix can only tighten such scales.
    • Qwen3-30B-A3B at EP16 over 4 nodes, NVFP4 and FP8: the exports with the dense part FSDP-sharded over the world and with it replicated have byte-identical weights and weight scales; only calibration-derived input scales differ. Every dense FP8 weight is FP8 with its scale, and no NVFP4 weight has the all-zero high nibbles of a lost pack shift.

    • Qwen3-VL-30B and MiniMax-M3: tensor counts match single-GPU exports, and every expert weight is packed.

    • MiniMax-M2.7 (block-FP8 source) at EP16 over 4 nodes, NVFP4: every exported weight name is the source checkpoint's, and routed experts are block_sparse_moe.experts.N.w1/w2/w3. Served by vLLM (0.30 dev build, TP4), it scores 0.94 / 0.93 on gsm8k (5-shot, 100 samples, flexible / strict), against the FP8 source's 0.93 / 0.95.

    • DeepSeek-V3 and GLM: on an earlier revision.

    • Kimi-K3 NVFP4 at EP64 over 16 GB200 nodes, in 10 minutes (peak 155 GiB per GPU on an earlier build of this series), checked against the source checkpoint:

      • the router correction biases match;
      • routed and shared experts carry the names vLLM 0.28's loader reads;
      • dense weights dequantize back to the BF16 source at cosine 0.9955;
      • the remote code, tokenizer and processor files ship with the export;
      • the modules vLLM builds unquantized stay BF16, as in nvidia/Kimi-K3-NVFP4.

      The same checks flag the export made before this series' fixes. vLLM 0.28 loads the export and starts serving, but it cannot serve a Kimi-K3 NVFP4 export with an FP8 KV cache: its FP8-KV path needs an FP8 prefill query, which its NVFP4 linears reject. TBD: generation and gsm8k from the checkpoint-mirror recipe models/moonshotai/Kimi-K3/ptq/nvfp4_experts-fp8_pb_attention on this series' code with the [3/21] fix.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Single-process and --use_fsdp2 runs behave as before. A torchrun launch without --use_fsdp2 now runs an MoE expert-parallel.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅ New feature, 0.49.0.
  • Did you get Claude approval on this PR?: ❌ Not yet.

Additional Information

Part 21 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after [20/21].

Summary by CodeRabbit

  • New Features
    • Added distributed, expert-parallel post-training quantization for Hugging Face MoE models, with calibration data distributed across ranks and checkpoint shards written in parallel.
    • Added options to control expert-parallel degree and dense-layer FSDP2 sharding, with validation for unsupported configurations.
    • Added synchronized preview generation for distributed runs.
  • Bug Fixes
    • Improved GPU memory monitoring when NVML initialization or memory reporting is unavailable.
  • Documentation
    • Added setup guidance, launch examples, requirements, and distribution options for expert-parallel quantization.

@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: NVIDIA/Model-Optimizer/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: a3be3d46-b98f-4ccd-8e07-1df4498d6504
📥 Commits

Reviewing files that changed from the base of the PR and between 12e1719 and 78f56fe.

📒 Files selected for processing: 9
  • examples/hf_ptq/example_utils.py
  • examples/hf_ptq/hf_ptq.py
  • examples/hf_ptq/vlm_utils.py
  • modelopt/torch/utils/distributed.py
  • tests/examples/hf_ptq/test_hf_ptq_args.py
  • tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py
  • tests/examples/hf_ptq/test_hf_ptq_model_type.py
  • tests/examples/hf_ptq/test_hf_ptq_vision_quantization.py
  • tests/unit/torch/utils/test_distributed_calibration.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

📝 Walkthrough

The HF PTQ example adds expert-parallel and dense-FSDP distribution support for model loading, calibration, previews, and checkpoint export. The changes also update NVML initialization around the memory probe and add tests and documentation for these behaviors.

Changes: Hugging Face PTQ distribution

Layer / File(s) Summary
Distribution planning and model loading
examples/hf_ptq/hf_ptq.py, examples/hf_ptq/example_utils.py, tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py, tests/examples/hf_ptq/test_hf_ptq_args.py, tests/examples/hf_ptq/test_hf_ptq_vision_quantization.py, examples/hf_ptq/README.md, CHANGELOG.rst
The example resolves distribution plans, checks supported layouts, and passes plans to pretrained-loading paths. The guide describes expert-parallel configuration and launch options. Tests cover plan selection and loading behavior.
Distributed execution and calibration
examples/hf_ptq/hf_ptq.py, examples/hf_ptq/example_utils.py, modelopt/torch/utils/distributed.py, tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py, tests/examples/hf_ptq/test_llm_ptq.py, tests/unit/torch/utils/test_distributed_calibration.py
The run uses a distributed context with a timeout above 30 minutes. Supported calibration data loaders are sharded across ranks, and expert-parallel plans use distributed_forward_loop. The integration test runs two-rank Qwen3-MoE PTQ and checks the exported FP8 expert weights and scales.
Synchronized previews and checkpoint export
examples/hf_ptq/example_utils.py, examples/hf_ptq/hf_ptq.py, examples/hf_ptq/vlm_utils.py, tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py, tests/examples/hf_ptq/test_hf_ptq_model_type.py
Generation kwargs enable synchronized previews for sharded models. Replicated models export on rank 0; dense-FSDP and expert-parallel models export on all ranks. Tests cover preview kwargs and export participation.

NVML memory probe

Layer / File(s) Summary
NVML probe lifecycle
modelopt/torch/utils/memory_monitor.py, tests/unit/torch/utils/test_memory_monitor.py
The memory monitor initializes NVML before probing GPU memory and shuts it down after the probe. Tests cover supported and unsupported memory reporting.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Ranks as torchrun ranks
  participant PTQ as HF PTQ
  participant Loader as load_pretrained
  participant Data as Calibration loader
  participant Forward as distributed_forward_loop
  participant Export as Checkpoint export
  Ranks->>PTQ: Resolve distribution plan
  PTQ->>Loader: Load model with distribution plan
  PTQ->>Data: Shard calibration data across ranks
  Data->>Forward: Provide rank-local calibration batches
  Forward->>Export: Complete expert-parallel calibration
  Ranks->>Export: Export checkpoint shards
Loading
Merge Risk: ⚪ Minimal · up to 78f56

No confirmed issue blocks merging. The distributed speculative-decoding calibration path remains unverified.

Pre-merge checks: Passed 5 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 56.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 83 functions across 12 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 5 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed No CRITICAL security anti-pattern was introduced. The changed modelopt and examples Python code adds no unsafe torch.load, numpy.load(..., allow_pickle=True), eval/exec, or # nosec bypas…
Title check Passed The title '[OMNIML-6085] [21/21] Run expert-parallel PTQ from hf_ptq.py' names the main change. The PR adds expert-parallel PTQ to hf_ptq.py, and the changeset adds distribution planning, expert-paral…
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

No CRITICAL security anti-pattern was introduced. The changed modelopt and examples Python code adds no unsafe torch.load, numpy.load(..., allow_pickle=True), eval/exec, or # nosec bypass. Transformer loading receives caller-controlled trust_remote_code values, with defaults set to False; the literal True values occur only in newly added test fixtures. The pull request changes no pyproject.toml or requirements file.

Full details: Title check

Explanation

The title '[OMNIML-6085] [21/21] Run expert-parallel PTQ from hf_ptq.py' names the main change. The PR adds expert-parallel PTQ to hf_ptq.py, and the changeset adds distribution planning, expert-parallel model loading, calibration, previews, and rank-aware export there. The ticket and series tags are noise, but the core change is clear and specific.

✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-14-hf-ptq branch from b298b8a to 9364c1b Compare October 12, 2026 01:11
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-14-hf-ptq branch from 9364c1b to 9d9b154 Compare October 12, 2026 02:44
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2754/

Built to branch gh-pages at 2026-10-24 01:30 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (shengliangx/hfep-11c-kimi-k3@15287ee). Learn more about missing BASE report.

Additional details and impacted files
@@                       Coverage Diff                       @@
##             shengliangx/hfep-11c-kimi-k3    #2754   +/-   ##
===============================================================
  Coverage                                ?   62.28%           
===============================================================
  Files                                   ?      667           
  Lines                                   ?    74107           
  Branches                                ?        0           
===============================================================
  Hits                                    ?    46159           
  Misses                                  ?    27948           
  Partials                                ?        0           
Flag Coverage Δ
unit 62.28% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 9d9b154

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-14-hf-ptq branch from 9d9b154 to 487a8e8 Compare October 12, 2026 08:39
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 487a8e8

@shengliangxu shengliangxu changed the title [19/19] Run expert-parallel PTQ from hf_ptq.py [OMNIML-6085] [19/19] Run expert-parallel PTQ from hf_ptq.py Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-14-hf-ptq branch from 487a8e8 to c8d0e4f Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-14-hf-ptq branch from c8d0e4f to 22ac8fc Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 22ac8fc

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-14-hf-ptq branch from 22ac8fc to d2b13df Compare October 13, 2026 17:41
@shengliangxu
shengliangxu removed this pull request from stack #2755 October 13, 2026 17:42
@shengliangxu
shengliangxu changed the base branch from shengliangx/hfep-13-ep-export to shengliangx/hfep-13b-load-routing October 13, 2026 17:42
@shengliangxu
shengliangxu added this pull request to stack #2775 October 13, 2026 17:43
@shengliangxu shengliangxu changed the title [OMNIML-6085] [19/19] Run expert-parallel PTQ from hf_ptq.py [OMNIML-6085] [20/20] Run expert-parallel PTQ from hf_ptq.py Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-14-hf-ptq branch from d2b13df to f9293cd Compare October 13, 2026 19:38
Base automatically changed from shengliangx/hfep-13b-load-routing to shengliangx/hfep-13-ep-export October 13, 2026 19:38
@shengliangxu
shengliangxu removed this pull request from stack #2775 October 13, 2026 19:38
@shengliangxu
shengliangxu added this pull request to stack #2781 October 13, 2026 19:40
@shengliangxu shengliangxu changed the title [OMNIML-6085] [20/20] Run expert-parallel PTQ from hf_ptq.py [OMNIML-6085] [21/21] Run expert-parallel PTQ from hf_ptq.py Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-14-hf-ptq branch from f9293cd to 3be65ef Compare October 13, 2026 21:12
hf_ptq already had a distributed path, but only one: --use_fsdp2, which shards
a dense model and replicates every expert of a MoE, so the documented CLI --
recipes, export formats, the flag deprecations -- did not reach an
expert-parallel run.

A distributed launch now runs inside a ModelOptSession, which owns the process
group and is what the expert-parallel loader reads its rank and device from.
args.dist_state becomes a view of that session rather than a second,
independently derived one.

load_model decides the plan after applying the model's spec -- for several
models the spec is what supplies the base_model_ep_plan, so deciding first
would see an expert-parallel-capable MoE as dense -- and gains an
expert-parallel arm ahead of the FSDP2 one. It is an arm of the same chain
rather than an early return, so the shared post-processing (tokenizer, VLM
language-model extraction) still runs. --use_fsdp2 skips the decision
entirely, so an explicit request for the FSDP2 loader is honored.
MODELOPT_EP_DEGREE and MODELOPT_FSDP_SIZE override the automatic degrees. A
model that CAN be expert-parallel but resolves to no sharding at all raises:
that configuration runs and quietly replicates the whole MoE on every rank.

Every distributed path calibrates on its own 1/world shard of the data, and
through distributed_forward_loop, which re-plants the torch_a2a forward
wrappers quantizer insertion clobbers on transformers >= 5.16 (without it the
experts forward runs with no EP process group and raises at the first step).
A dense model under torchrun is placed on this rank's GPU rather than by
accelerate's "auto" heuristic, which picks the same card on every rank of an
idle node and aborts the first collective with "Duplicate GPU detected".

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Rebasing onto a main that had opened a new release section left the entry where
its context matched: in the Misc list of 0.47.0, a release that has already
shipped. It belongs with the other new quantization features of 0.49.0, the
section main opened for what merges after 0.48.0's cut.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Cover how hf_ptq.py runs under torchrun:
- the MODELOPT_EP_DEGREE / MODELOPT_FSDP_SIZE knobs, read at call time;
- applying a model's spec before the EP decision;
- the decision itself: the auto degree, the knobs overriding it, --use_fsdp2
  and single-process runs opting out, an unsharded MoE refused;
- load_model handing an EP plan to load_and_prepare_ep and to no other loader;
- each rank pinning a model to its own GPU;
- dist_state following the torchrun session;
- main() closing the session even when the run raises;
- calibration on per-rank data through distributed_forward_loop.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Only the expert-parallel loader shards a dense part by itself. When the
plan asked for dense sharding (MODELOPT_FSDP_SIZE, or "auto" finding the
dense part too big for one GPU) but resolved to ep_size=1, the decision
returned no plan without complaint, and load_model replicated the whole
model -- MoE or dense -- on every rank. The error for an unsharded MoE
even recommended MODELOPT_FSDP_SIZE=world, which only silenced it.

Raise instead, pointing at --use_fsdp2 (and at MODELOPT_EP_DEGREE for an
MoE), and drop the bad advice and the unreachable --use_fsdp2 check.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
get_model pinned every indexed device to that one GPU so that torchrun's
ranks stop sharing a card, but it did so for a single process too: a run
passing --device cuda:0 no longer spread a model too big for one GPU across
the others, as it did before. Pin only when the run has more than one rank.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Say what running hf_ptq.py under torchrun does to an MoE, how to launch it
on one node or several, what MODELOPT_EP_DEGREE and MODELOPT_FSDP_SIZE
override, and which models it refuses.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Every distributed run calibrated through distributed_forward_loop, which
moved --use_fsdp2 off main's create_forward_loop and with it the split-on-OOM
retry and the encoder-decoder generate path. Only expert parallelism needs
the distributed loop -- it re-plants the all-to-all forwards quantizer
insertion clobbers -- so it is now chosen from the session's ep_mesh, which
load_and_prepare_ep records, and FSDP2 keeps the plain loop.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
launch_memory_monitor checks that NVML can report memory before starting
the monitor, so hosts where it cannot (#374) skip monitoring instead of
crashing. But the probe ran before nvmlInit, which only GPUMemoryMonitor's
constructor calls, so every NVML call in it raised NVMLError_Uninitialized:
the monitor printed "Failed to get GPU memory info: Uninitialized" and never
started on any host. hf_ptq.py launches it on every run, so no run has
reported its per-GPU peak memory since.

The probe now initializes NVML for itself and shuts it down afterwards, so
the init count stays balanced with the monitor's own. Hosts where memory
info is unsupported still skip the monitor.

Unit tests run launch_memory_monitor against a fake, reference-counted
NVML: the monitor starts when memory info is available (this fails on the
old code), and is skipped with NVML left shut down when it is not.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
get_model now loads through load_pretrained(ckpt_path, auto_class, ...), which
replaced example_utils' own recording loader.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel and --use_fsdp2 runs now load through load_pretrained,
with the plan or "fsdp2", instead of calling load_and_prepare_ep and
parallel_load_and_prepare_fsdp2 themselves, so they get its checks: the
expert-parallel run now refuses a Nemotron-H MTP checkpoint it would have
exported with the head unquantized. resolve_distribution_plan takes over
the decision, including the refusal to replicate an expert-parallel MoE
on every rank; this script keeps only its own rule that FSDP2 runs under
--use_fsdp2. get_model and the low-memory load ask for "sequential":
their run is already decided, and they place the model themselves.

The tests of the decision itself moved with it to
test_load_pretrained_distribution.py.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
hf_ptq's FSDP2 load now goes through load_pretrained, so the test that
checks the MTP refusal comes before it stubs that instead of
parallel_load_and_prepare_fsdp2, which hf_ptq no longer imports.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The plan is now decided through the session, whose decide is collective.
These tests are about what gets decided, so each simulated rank acts as
its group's first.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…EP mesh where needed

main() runs inside distributed_run, which sets the process group up on a distributed launch
and tears it down even when the run raises. The example's DistributedState reads rank and world
from that group, the calibration data is sharded by it, and an expert-parallel model calibrates
through distributed_forward_loop with the mesh its experts are sharded over
(expert_parallel_mesh).

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…ibutes

hf_ptq.py decides the plan up front with resolve_distribution_plan, --use_fsdp2 asking it for FSDP2
without expert parallelism, and loads every model by it through load_pretrained, get_model's
whole-model loads and the low-memory load included. A plan that FSDP2-shards the whole model still
needs --use_fsdp2, whose checks now run before anything is set up.

Everything after reads the plan back from the model instead of the flag or the process group: the
calibration device, the data sharding, the expert-parallel calibration loop, and get_model's
pinning of each rank's whole model to its own GPU. The one-rank process group FSDP2 shards through
is set up for the plan, right before the load, and torn down with the run; a distributed launch's
group stays distributed_run's.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
hf_ptq built its DistributedState itself, in setup_distributed_args,
from the process group distributed_run had just set up. distributed_run
yields that state now, so main() hands it to the run, with --device for
a single process, and the example's own class and setup are gone.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
hf_ptq set up the one-rank group FSDP2 needs in a single process and
tore it down in its own finally. Both live in modelopt's distributed
module now: load_model sets the group up with setup_fsdp2_process_group,
keeping its two-hour collective timeout, and distributed_run destroys it
with the run, as it does a launch's. The example's own helpers go.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
load_pretrained sets up the one-rank group its FSDP2 load shards
through ([3/21]), and distributed_run destroys it with the run, so
hf_ptq no longer sets it up before loading. The group keeps PyTorch's
default collective timeout: on one rank no collective waits for a peer,
which is what the two hours were for.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Each rank previews the first sample of its own calibration shard, so the ranks
reach EOS at different steps. Under expert parallelism, or with the dense part
FSDP2-sharded, every forward runs a collective (the experts' all-to-all,
FSDP2's all-gathers), and a rank that stopped decoding left the others blocked
in it: Qwen3-30B-A3B at NVFP4 and EP4 hung in the post-PTQ preview, while the
same run with --skip_generate finished. The previews, Nemotron-VL's included,
now generate with synced_gpus when the model's plan shards it, so every rank
keeps stepping until all have finished. The preview tests' stand-in models are
modules now, since the previews read their distribution plan.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…ge preview

A one-rank FSDP2 plan needs no lockstep, but no case pinned it, so a check
dropping world_size went unnoticed. The Nemotron-VL test stubbed vlm_utils
whole, so nothing checked that the image preview merges the kwargs it is
handed into the config its chat() gets. Both gaps found by the epdp port.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A dense model under torchrun gets a replicated plan, and every rank then
ran the export into the same directory, racing each other's files and the
in-place config.json rewrite. Rank 0 alone writes a replicated model; a
sharded one still needs every rank. The source config of a multimodal
model is rank 0's in every plan: another rank's, written after rank 0's
export, would replace the config.json carrying the quantization config.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With fewer expert-parallel ranks than the world (the automatic degree when
the world size does not divide the expert count), FSDP2 shards the dense
part within each replica. The no-gather export cannot write that layout,
and found out only after the load and the whole calibration. Refuse the
plan up front and name the settings that avoid it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…ches

resolve_distribution_plan reads the checkpoint through the failure-carrying
all_reduce, which gathers from every rank of a launch. The torchrun fixture
answers that gather as it answers the plan's broadcast: every rank read the
checkpoint as this one did.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Nothing ran examples/hf_ptq/hf_ptq.py expert-parallel: its wiring tests
approximate a torchrun launch with stubbed collectives, loaders and
forward loops. The new test launches it under torchrun on two GPUs with
MODELOPT_EP_DEGREE=2 on a tiny Qwen3-MoE, calibrates FP8 on a local
calibration file, and checks that every expert of every layer is
exported FP8 with its scale: a rank whose share of the experts is lost,
or written under the wrong expert index, fails it. It skips below two
GPUs, so it runs on the multi-GPU runners.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
- The single-process and dense-under-torchrun plan tests go: the
  library's single-process tests and the get_model load test fail on
  the same regressions.
- The EP and --use_fsdp2 load_pretrained-call tests become one table;
  the get_model test stays, a different loader and device placement.
- The two calibration-loop tests become one parametrized test, its
  helper inlined.
- The memory-monitor pair becomes one parametrized test.
- Kept: the Nemotron-VL preview test's full plan table, whose cases are
  the only guard of the VL preview's own lockstep decision (a VL preview
  deciding by expert parallelism alone fails only [fsdp2], one ignoring
  the world size only [one_rank_fsdp2]); and the main-owns-the-process-
  group test, the only guard of main's collective timeout above NCCL's
  default, which the GPU smoke test runs too briefly to reach.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
That exporter reads the process count as Megatron's training tensor
parallelism. Under torchrun with a replicated plan, rank 0 ran it alone
and waited in its barrier for ranks that had already returned; run on
every rank, it would merge whole replicas as if they were shards. hf_ptq
now refuses an encoder-decoder, sparsity or int8_smoothquant export with
more than one process before it loads the model.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
hf_ptq picked the VLM calibration loop for image calibration before it
checked for expert parallelism, so an expert-parallel run with images
never re-planted the all-to-all forwards that quantizer insertion drops.
On transformers 5.16 and later, the first routed forward then raised for
want of the EP process group. Nemotron-H Omni, which has an EP spec and
calibrates on images by default, hit this with default flags.

distributed_forward_loop now takes an optional per-batch forward, and the
VLM loop's per-batch call is factored out as
create_vlm_calibration_forward. An expert-parallel image run calibrates
through distributed_forward_loop with that forward on the whole VLM.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
resolve_distribution_plan now hands ensure_local_checkpoint token,
cache_dir and local_files_only. The stub these tests put in its place
took only the path and the group, so every test that reached the plan's
checkpoint read failed with an unexpected keyword. (Found by the epdp
port.)

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…s preview

On a GPU, Transformers' generate runs the experts of a grouped_mm MoE
on batched_mm for the decoding stage and switches them back
(_optimize_model_for_decode, Transformers 5.0 and later). Two things
follow.

An FSDP2-wrapped model, whose class is a subclass built outside its
modeling module, is refused the switch. A Qwen3-30B-A3B run under
--use_fsdp2 on 4 GPUs failed in the pre-quantization preview with
"FSDPQwen3MoeForCausalLM does not support setting experts
implementation" (Transformers 5.13).

And ModelOpt's quantizers sit on the eager, grouped_mm and torch_a2a
forwards, which batched_mm bypasses, so the preview after PTQ of any
grouped_mm MoE on a GPU decoded on unquantized experts. That predates
the FSDP2 and expert-parallel work.

The pre- and post-quantization previews and the Nemotron VL preview now
run inside a context manager that keeps generate's hook off the model
and puts it back. A model without the hook is left alone.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…FSDP2

The two-GPU hf_ptq test skipped generation, so no test reached a sharded
preview. A real Qwen3-30B-A3B run did: under FSDP2 Transformers refused
the experts switch generate makes for an FSDP2-wrapped class, after the
previews of an earlier round had hung ranks that finished decoding at
different steps. The test now runs both plans, expert parallelism and
FSDP2, with the previews on, on a tiny Qwen3-MoE.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test fb135b1

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants