Skip to content

[OMNIML-6461] Run AutoQuantize on expert-parallel and replicated models - #3250

Draft
shengliangxu wants to merge 2 commits into
mainfrom
shengliangx/dp-ep-autoquant
Draft

shengliangxu wants to merge 2 commits into
mainfrom
shengliangx/dp-ep-autoquant

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: New feature

mtq.auto_quantize refused a model whose routed experts are sharded expert-parallel, and a torchrun launch of examples/hf_ptq/hf_ptq.py refused every AutoQuantize recipe unless --use_fsdp2 was passed. The searcher already sums each module's scores and costs over its TP/DP/EP groups and broadcasts the chosen formats, so ranks that each score their own data can agree. What kept it from running, and from picking the right formats:

  • The experts' all-to-all forwards. Quantizer insertion rebinds forward and drops transformers' wrapper that hands the torch_a2a experts their process group (transformers >= 5.16). distributed_forward_loop re-plants it for mtq.quantize; auto_quantize never did, so its first forward failed. It now takes the mesh from the model's recorded plan before converting it, and re-plants the forwards right after.
  • The experts' cost. From transformers 5.16 the expert-parallel load leaves the experts' weights as DTensors, whose size is already the whole tensor's. get_cost summed that size over the EP group, so the experts counted ep_size times in the effective-bits budget and the search picked other formats. A DTensor weight is no longer summed over TP/EP. A plain tensor is still this rank's shard, as with Megatron or transformers < 5.16.
  • aumann_shapley over the experts alone. Its loss and token count were reduced over the first module carrying a parallel state. A search whose only configurable groups are routed experts took an expert's groups, which span only the ranks holding the same experts, while its expert-parallel ranks score data of their own too. It now reduces over a dense module's groups.
  • hf_ptq.py runs AutoQuantize recipes on its expert-parallel and replicated routes. Each rank scores its own shard of the data, so a step scores batch_size samples per rank: the recipe's score_size is spread over every rank of any distributed plan, not only under --use_fsdp2.

Still refused: a KV-cache search under expert parallelism. Its checks require every rank to hold the same quantizers, and each expert-parallel rank holds its own experts'. The refusal moves from auto_quantize's entry into the KV-cache search, still before anything converts the model. hf_ptq.py now refuses such a recipe before the load, rather than after the weight search it runs first. Rank-local calibrations (AWQ and the others) stay refused per candidate, as before.

Usage

# An MoE under torchrun: experts sharded over the ranks, each rank scoring its own calibration shard.
torchrun --nproc_per_node 4 examples/hf_ptq/hf_ptq.py \
    --pyt_ckpt_path Qwen/Qwen3-30B-A3B \
    --recipe general/auto_quantize/nvfp4_fp8_at_5p4bits \
    --calib_size 512 --export_path <out>
# Before: NotImplementedError: ... AutoQuantize scores each rank's own calibration data and reduces
#         no score across ranks ...; launch hf_ptq without torchrun for an AutoQuantize recipe.
# After:  every rank picks the same per-layer formats, and the ranks write the mixed-format
#         checkpoint together.
# The library, on a model load_pretrained distributed under torchrun (expert-parallel or replicated):
model = load_pretrained(ckpt)  # records the plan, and for EP the mesh the experts are sharded over
model, search_state = mtq.auto_quantize(
    model,
    constraints={"effective_bits": 5.4},
    quantization_formats=[mtq.NVFP4_DEFAULT_CFG, mtq.FP8_DEFAULT_CFG],
    data_loader=this_ranks_shard,  # every rank runs the same number of batches
    forward_step=lambda m, batch: m(**batch),
    loss_func=lambda output, batch: output.loss,
)

Testing

  • New tests/unit/torch/quantization/test_autoquant.py::test_expert_parallel_auto_quantize_picks_one_processes_formats[gradient|aumann_shapley], on two CPU ranks (gloo). A tiny Qwen3-MoE is sharded as transformers' expert-parallel load leaves it (apply_tensor_parallelism with the torch_a2a styles: DTensor experts behind forward wrappers), with each module's groups seeded as the load seeds them. Each rank searches on its own batch; one process searches on every rank's batch, one per step. Formats and costs must be equal, scores within 1e-2 relative (gradient: ~1e-7). The aumann_shapley case searches the experts alone. Each fix fails it when reverted:

    Reverted Result
    re-planting the a2a forwards both ids: torch_a2a experts forward requires the EP process_group
    DTensor weights summed over EP both ids: experts' costs 2x (ep_size)
    shapley's dense-module reduction aumann_shapley id: scores 3-4x off

    About 2 s per id once the shared worker pool is up.

  • Updated: the library's EP refusal test now checks the KV-cache search's refusal. test_hf_ptq_expert_parallel.py checks that a weight recipe runs on every route and a KV-cache recipe is refused before the load under expert parallelism. test_autoquant_utils.py checks the score steps on a replicated plan. Each fails when its change is reverted.

  • Pre-commit, mypy included, passes against main.

  • On CPU with transformers 5.18 and torch 2.14, before rebasing onto current main, tests/unit/torch/quantization, tests/unit/torch/utils and tests/examples/hf_ptq failed exactly the same ids at main and on this branch, all of them failures of that host's environment, and the transformers-free lane passed the changed unit files. Main now carries [OMNIML-6370] [1/5] Isolate weight-rewriting AutoQuantize candidates from the source weights #3154, which wraps the search in a block that hands back the unconverted model on failure; the re-plant now sits inside it. After the rebase, the changed test files plus test_autoquant_shapley.py, test_ggml_gptq.py and plugins/test_fused_experts.py pass (548 passed).

  • New GPU test tests/gpu/torch/utils/test_moe_a2a.py::test_expert_parallel_auto_quantize_and_export_match_the_whole_model, three layouts: experts over every rank, over half of them (experts replicated across two groups) and with the dense part FSDP2-sharded. It uses load_pretrained's expert-parallel load in float32, compares the search with the whole model's, then compares the no-gather writer's mixed-format checkpoint, tensors and hf_quant_config.json, with the whole model's single-process export:

    GPUs transformers Experts over every rank Experts over half the ranks Dense part FSDP2-sharded
    1 5.18 skipped (needs 2 GPUs) skipped (needs 2 GPUs) skipped (needs 2 GPUs)
    2 5.18 passed skipped (needs 4 GPUs) passed
    4 5.18 passed passed passed
    4 5.13 passed passed passed

    The 5.13 run is the pre-5.16 path: the experts load as each rank's plain share behind forward pre-hooks. The rest of test_moe_a2a.py passes and skips the same tests on main and on this branch at 2 and 4 GPUs. The 2- and 4-GPU rows with transformers 5.18 were re-run at the rebased head with the same results; the 1-GPU and 5.13 rows are from this change before its rebase onto current main.

  • Regression on GPUs, main vs this branch: test_fsdp2_autoquant.py passes the same 6 tests on both at 2 GPUs, and skips them at 1.

  • Megatron, at the rebased head on 2 GPUs: test_megatron.py's test_auto_quantize, test_auto_quantize_moe_ep and test_auto_quantize_mamba_hybrid_ep_cost pass. The last checks that Megatron's AutoQuantize costs agree with the experts on 1 rank and on 2, a comparison CI's 1-GPU runner never reaches. Its 2-GPU run takes longer than the suite's default per-test timeout, so it ran with that timeout off.

  • Real model: Qwen3-30B-A3B through hf_ptq.py with general/auto_quantize/nvfp4_fp8_at_5p4bits on 4 GPUs (transformers 5.18, torch 2.11), in three layouts. Each run finished the search and exported its checkpoint, and the layouts chose the same format for all but two of the quantized layers:

    Layout on 4 GPUs Effective bits FP8 layers NVFP4 layers Formats equal to the replicated run's
    Replicated, each rank on its own share of the data 5.40 126 53 (reference)
    Experts over 2 ranks, in two replicas 5.40 126 53 179 of 179
    Experts over all 4 ranks 5.40 126 53 177 of 179

    The two that differ are the attention output projections of layers 35 and 36, which have the same shape and so the same cost. The run with the experts over all 4 ranks gave NVFP4 to layer 35 and FP8 to layer 36, and the other two runs did the reverse, so every run ends with the same number of layers in each format. Spreading the experts differently changes the order of the floating-point arithmetic in them, which can tip a near-tie between two equal-cost layers either way. The runs did not keep the scores that would show that margin. These runs used this change before its rebase onto current main (42570f21ace).

Before your PR is "Ready for review"

Make sure you read and follow Contributor guidelines and your commits are signed (git commit -s -S).

Make sure you read and follow the Security Best Practices (e.g. avoiding hardcoded trust_remote_code=True, torch.load(..., weights_only=False), pickle, etc.).

  • Is this change backward compatible?: ✅ Launches and calls that were refused now run; a KV-cache search under expert parallelism is refused as before, now by the KV-cache search and before hf_ptq.py loads the model. Single-process, FSDP2 and Megatron searches are unchanged: their weights are plain tensors or FSDP2 DTensors whose TP/EP groups are unset.
  • 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?: ✅ A New Features entry, and AutoQuantize recipes dropped from [OMNIML-6085] Run expert-parallel PTQ from hf_ptq.py, with lockstep previews #3139's Backward Breaking entry of what a torchrun launch refuses. Both sit in 0.49.0 for now, the only unreleased section on main. Since this targets 0.50.0, the entry moves to a 0.50.0 section once main has one, and [OMNIML-6085] Run expert-parallel PTQ from hf_ptq.py, with lockstep previews #3139's 0.49.0 entry stays as it is.
  • Did you get Claude approval on this PR?: ❌ Not yet.

Additional Information

Tracked by OMNIML-6461, for ModelOpt 0.50.0. Lifts the refusals from #2740 (library) and #3139 (hf_ptq.py), and builds on the autograd all-to-all from #2738. Overlaps #3236 in that one CHANGELOG line, which both edit; whichever merges second keeps both edits.

@copy-pr-bot

copy-pr-bot Bot commented Nov 6, 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 Nov 6, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test c9a3396

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.2

QR code for preview link

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

Built to branch gh-pages at 2026-11-07 01:34 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov

codecov Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.92%. Comparing base (cf1e43d) to head (e7dd863).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3250      +/-   ##
==========================================
+ Coverage   72.89%   80.92%   +8.03%     
==========================================
  Files         688      688              
  Lines       79957    79960       +3     
==========================================
+ Hits        58284    64707    +6423     
+ Misses      21673    15253    -6420     
Flag Coverage Δ
examples-diffusers 19.77% <8.33%> (+<0.01%) ⬆️
examples-gpt-oss 13.45% <8.33%> (+<0.01%) ⬆️
examples-hf_ptq 23.95% <66.66%> (-0.73%) ⬇️
examples-llm_distill 13.51% <8.33%> (-0.01%) ⬇️
examples-llm_eval 17.44% <8.33%> (+<0.01%) ⬆️
examples-llm_qat 17.27% <8.33%> (-0.03%) ⬇️
examples-llm_sparsity 16.83% <8.33%> (+<0.01%) ⬆️
examples-megatron_bridge 25.98% <8.33%> (-0.21%) ⬇️
examples-specdec_bench 13.23% <8.33%> (+<0.01%) ⬆️
examples-speculative_decoding 17.65% <8.33%> (-0.06%) ⬇️
examples-torch_onnx 20.93% <50.00%> (+0.08%) ⬆️
examples-torch_trt 15.16% <8.33%> (+<0.01%) ⬆️
examples-vllm_serve 17.50% <8.33%> (+<0.01%) ⬆️
gpu 59.13% <50.00%> (+37.23%) ⬆️
regression 14.93% <8.33%> (-0.01%) ⬇️
unit 63.98% <100.00%> (+<0.01%) ⬆️

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.

mtq.auto_quantize refused a model whose routed experts are sharded
expert-parallel, and hf_ptq refused every AutoQuantize recipe under
torchrun without --use_fsdp2. The searcher already sums the scores and
costs over each module's TP/DP/EP groups and broadcasts the chosen
formats. What was missing:

- auto_quantize re-plants the routed experts' all-to-all forwards that
  quantizer insertion drops on transformers >= 5.16, before the search
  calibrates and scores.
- get_cost no longer sums a DTensor weight's size over the TP/EP groups.
  From transformers 5.16 the expert-parallel load leaves the experts'
  weights as DTensors, whose size is already the whole tensor's, so the
  experts counted ep_size times their size in the effective-bits budget.
- aumann_shapley reduces its loss and token count over a dense module's
  groups. A search over the experts alone reduced them over one expert's
  ranks, which hold only part of the data.
- hf_ptq runs AutoQuantize recipes expert-parallel and replicated, its
  score_size spread over all ranks. A KV-cache search, whose checks need
  every rank to hold the same quantizers, stays refused under expert
  parallelism, now before the load.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Below transformers 5.16 the expert-parallel load keeps each rank's plain share of
the experts behind forward pre-hooks, the other half of the cost sum and of
the forwards AutoQuantize runs; the test skipped it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu
shengliangxu force-pushed the shengliangx/dp-ep-autoquant branch from c9a3396 to e7dd863 Compare November 7, 2026 01:27
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test e7dd863

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.

1 participant