Repository navigation
[OMNIML-6085] [3/3] Run expert-parallel PTQ from hf_ptq.py - #2754
shengliangxu wants to merge 10 commits into
Conversation
|
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. |
b298b8a to
9364c1b
Compare
4e11e09 to
be3cc39
Compare
9364c1b to
9d9b154
Compare
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## shengliangx/hfep-11c-kimi-k3 #2754 +/- ##
================================================================
+ Coverage 62.63% 62.69% +0.06%
================================================================
Files 668 668
Lines 74485 74490 +5
================================================================
+ Hits 46654 46703 +49
+ Misses 27831 27787 -44
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
/ok to test 9d9b154 |
be3cc39 to
bdf08a2
Compare
9d9b154 to
487a8e8
Compare
|
/ok to test 487a8e8 |
487a8e8 to
c8d0e4f
Compare
136543b to
405ef82
Compare
c8d0e4f to
22ac8fc
Compare
|
/ok to test 22ac8fc |
22ac8fc to
d2b13df
Compare
d2b13df to
f9293cd
Compare
f9293cd to
3be65ef
Compare
|
/ok to test 5b2a422 |
|
/claude review |
| # transformers >= 5.16 (where the styles install a forward rather than a pre-hook). | ||
| # Without it the experts forward runs with process_group=None and raises | ||
| # "torch_a2a experts forward requires the EP `process_group`" at calibration step 0. | ||
| # On <= 5.13 the wrappers are pre-hooks that survive, so the reinstall is a no-op -- |
There was a problem hiding this comment.
[SUGGESTION] The version boundary here contradicts the two other places that state it. Line 699 (three lines above) and line 742 both say the clobber exists on transformers >= 5.16; _reinstall_a2a_forwards' own docstring and body say the same ("install_forward() -- and the clobber it causes -- only exist on transformers >=5.16. Older releases install their TP input/output transforms as forward PRE-HOOKS"), and the PR description says "On 5.15 and earlier the TP transforms are forward hooks". So 5.14 and 5.15 are also pre-hook releases, and <= 5.13 reads as if 5.14/5.15 were a third, unexplained case.
Impact is only on the next reader — the code keys on hasattr(style, "install_forward") per style, not on a version — but a comment that disagrees with the comment three lines up is the kind of thing that sends someone version-hunting.
| # On <= 5.13 the wrappers are pre-hooks that survive, so the reinstall is a no-op -- | |
| # On <= 5.15 the wrappers are pre-hooks that survive, so the reinstall is a no-op -- |
There was a problem hiding this comment.
Fixed in c2dbf28: the trimmed comment gives only the 5.16 boundary, as the other places do; the <= 5.13 sentence is gone.
There was a problem hiding this comment.
Claude review — [3/3] Run expert-parallel PTQ from hf_ptq.py
Findings: CRITICAL 0 · IMPORTANT 0 · SUGGESTION 1 — approving.
Scope
Full review — bare /claude review, no scoping instructions. 10 changed files (+1453/-190), two-dot diff against origin/shengliangx/hfep-14b-hf-ptq-previews. I read every source hunk with surrounding context (examples/hf_ptq/hf_ptq.py, example_utils.py, README.md, modelopt/torch/utils/distributed.py) plus CHANGELOG.rst, and traced the base-branch callees this slice leans on: resolve_distribution_plan (including its config read and the spec/plan broadcasts), load_pretrained, distributed_run / DistributedState / local_rank, distribution_plan / set_distribution_plan, shard_dataloader, distributed_forward_loop / _reinstall_a2a_forwards, expert_parallel_mesh, preview_generate_kwargs / keep_the_experts_implementation, validate_fsdp2_supported, get_model, layerwise_export_requested. Of tests/ I read the four small diffs in full plus the fixtures, helper namespaces, calibration/preview/export cases and test-name index of the new 961-line test_hf_ptq_expert_parallel.py. I could not execute tests in this environment, so pass/fail evidence is the description's.
Delta since the 2026-10-30 approval
One commit, 5b2a4221 ("Drop the failed-preview test's --use_fsdp2 row"), plus the restack onto [2/3]'s get_model review fix. Both check out:
- The dropped row is right. The base tip
7772c0dfmoved the raise down into [2/3] and widened it to every sharded plan, sopreview_generate_kwargsreturnssynced_gpus— and the raise fires — fordense="fsdp"too; a row asserting--use_fsdp2warns and still exports would now fail. The three surviving rows still discriminate what this slice owns:DistributionPlan(4)yieldslockstep == {}, so the replicated row genuinely exercises warn-and-export rather than passing tautologically. - The restack strands no load path.
get_modelnow raises foris_expert_parallel or is_fsdp_dense, andload_model's branch order means only a replicated plan reaches it: EP first, thenis_fsdp_dense, withis_fsdp_dense and not is_expert_parallel and not --use_fsdp2already refused in_resolve_distribution. The hybrid EP+FSDP-dense plan takes the EP branch.
The one finding (non-blocking)
hf_ptq.py:702 — the a2a comment says the wrappers are pre-hooks on transformers <= 5.13, while line 699 three lines above, line 742, and _reinstall_a2a_forwards' own docstring all put the boundary at >= 5.16. No behavioural effect (the code keys on hasattr(style, "install_forward") per style, not on a version), but two comments in the same function disagree.
What I checked this round and found sound
- Dropping the low-memory branch's own config read is safe.
resolve_distribution_planreads the config unconditionally insideall_reduce("checkpoint read", ...)and returns aPretrainedConfig, neverNone, sohf_config.model_typethere cannot hit an attribute error. That branch is single-process-only anyway (--low_memory_modeis refused above world size 1 without--use_fsdp2, and byvalidate_fsdp2_supportedwith it). - The seven pre-load refusals are rank-identical and fire before anything is allocated.
validate_fsdp2_supportedreads onlyargsand a config, ahead of the first collective; everything after derives from the broadcast plan plus rank-identical args. The short-circuit order is right:is_expert_parallel and args.experts_implementation is not Nonenever reads the flag under--use_fsdp2(which forcesep_size=1), while(is_expert_parallel or is_fsdp_dense) and args.vllm_fakequant_exportdoes read it there — which is whytest_fsdp2_autoquant.pyhad to growvllm_fakequant_export. That namespace now carries every attribute_resolve_distributionreads on the--use_fsdp2path. - Calibration shards cannot desync step counts.
make_calib_dataloaderreadsdistribution_plan(language_model).world_size— resolvable on a VLM sub-model since the plan is stamped on every module — and shards withargs.dist_state.rank, the samesize()/rank()the plan was built from. Map-style loaders pad viaDistributedSampler(drop_last=False), the VLM stream drops its trailing partial group, every loader isnum_workers=0, and nodp_sizeis passed, so there is exactly one shard._shard_calib_dataloadercovers both shapes the builder can return, keeps_DeviceDataLoader's device move outermost, tests the non-DataLoadersubclass first, and refuses anything else loudly. The Whisper branch is unreachable above one rank. - The a2a lifetime is covered end to end. The loop re-plants first and asserts
is_expert_parallel == (ep_mesh is not None)andep_mesh.size() == plan.ep_size, so a mismatched mesh fails loudly instead of running withprocess_group=None; the post-quantize re-plant sits outside theuse_calibrationguard, so a recipe that runs no loop keeps its wrappers for the preview and export; and the re-plant skips a wrapper already in place, so the two calls cannot stack transforms. The EP text lambda closes over thelanguage_modelvariable, whichmtq.quantizerebinds only after returning, so during calibration it is the in-place-converted module. - Export gating. The early return filters exactly the non-main ranks of a replicated plan — for which
_hf_shard_flagsreports no sharding, so rank 0 issues no collective they skipped —save_source_configisis_main-gated and ordered ahead of rank 0's own export, and the sharded spec-dec refusal raises on every rank with nothing written, leavingcheckpoint_exportedatFalse. main/_main_inner.distributed_run(timeout=3h, device=args.device)owns the group for the whole run and is exited even when the body raises;args.dist_stateis assigned as its first statement and read only within it. The monitor gateworld_size == 1 or local_rank() == 0short-circuits beforelocal_rank()can warn in a single process, andlocal_rank()falls back to the global rank rather than raising whenLOCAL_RANKis absent.- Tests assert behaviour, not mocks. No checkpoint needed, one gloo rank only for the test that must run a real all-to-all forward, autouse fixtures clear
MODELOPT_EP_DEGREE/MODELOPT_FSDP_SIZEso a developer's shell cannot steer the decisions, and the 2-GPU smoke test requires every expert of every layer to come outfloat8_e4m3fnwith itsweight_scale, so a lost or misindexed rank share fails it. - No prompt-injection or review-steering text anywhere in the diff.
Still open from earlier rounds (non-blocking, not re-filed)
The config read twice under --use_fsdp2 and the recipe parsed twice; the private _reinstall_a2a_forwards import and the doubled expert_parallel_mesh call; the post-quantize re-plant keyed on the quantize block rather than the plan; the EP image route building its VLM forward pre-conversion and skipping full_model.eval(); the plan-ungated Nemotron-VL re-raise (not a regression — that path raised before the series, this only replaces the message); and the README knob table's auto row for a spec that refuses EP.
Wording point for the PR body, not a defect: a multi-rank --use_fsdp2 run of a spec-dec checkpoint without --specdec_offline_dataset is now refused at export, where before every rank raced export_speculative_decoding into the same directory. A fix, but a third item (with --sparsity_fmt <non-dense> and --device cuda:N) the "--use_fsdp2 runs behave as before" answer does not cover.
Risk
Low. The delta is one test-only commit plus a restack, and neither disturbs the invariants this series rests on: one plan decided once, broadcast, and read back off the model everywhere after; refusals before the load from rank-identical inputs, so a rejection leaves nothing allocated; rank 0 writes a replicated model and every rank writes a sharded one; equal per-rank step counts. Residual exposure is where the code says it is — multi-node EP paths CI cannot run, backed by the described end-to-end runs and the 2-GPU smoke test — plus a preview failure on one rank alone parking its peers in generate's collectives until the timeout.
Claude review passed — no blocking issues found. LGTM
🤖 Generated with Claude Code
| ) | ||
|
|
||
|
|
||
| def _resolve_distribution(args: argparse.Namespace): |
There was a problem hiding this comment.
Could we move _resolve_distribution into example_utils.py, alongside validate_fsdp2_supported? It primarily contains example-specific CLI validation, and extracting it would keep hf_ptq.py focused on the PTQ workflow. The generic planning logic should remain in the existing library resolve_distribution_plan.
There was a problem hiding this comment.
Moved in 50a28e9: resolve_distribution now sits in example_utils.py after validate_fsdp2_supported, with exports_tensorrt_llm_checkpoint, which it needs, and load_model calls it. The generic planning stays in the library's resolve_distribution_plan. Its comments are shorter, and the tests call and patch the names where they now live.
meenchen
left a comment
There was a problem hiding this comment.
Could you also add examples of EP for slurm? similar to https://github.com/NVIDIA/Model-Optimizer/blob/5b2a4221ba65e85eb84589e393adb61cb04ac690/examples/hf_ptq/slurm/multinode_fsdp2_ptq.slurm
| # An expert-parallel run has to calibrate through distributed_forward_loop, not the | ||
| # plain one: it calls _reinstall_a2a_forwards first, which re-plants the | ||
| # torch_a2a expert/router forward wrappers that quantizer insertion clobbers on | ||
| # transformers >= 5.16 (where the styles install a forward rather than a pre-hook). | ||
| # Without it the experts forward runs with process_group=None and raises | ||
| # "torch_a2a experts forward requires the EP `process_group`" at calibration step 0. | ||
| # On <= 5.13 the wrappers are pre-hooks that survive, so the reinstall is a no-op -- | ||
| # which is why this is invisible on older transformers and fatal on newer. Other | ||
| # distributed runs (FSDP2, or a model replicated on every rank) keep the plain loop, | ||
| # which retries no batch that runs out of memory on a plan of several ranks: the | ||
| # error reaches distributed_run, which aborts the rank and with it the run. Image | ||
| # batches go through the VLM's own forward, as they do without expert parallelism: | ||
| # Nemotron-H Omni, for one, calibrates on images by default. Text batches go through | ||
| # the language model, but the loop re-plants over the model it is handed, so that is | ||
| # the full model: transformers keeps the expert-parallel plan on the model | ||
| # from_pretrained built, and a VLM's language model names no experts in its own. |
There was a problem hiding this comment.
Could you trim the comments?
|
/ok to test be8d1df |
|
/claude review |
|
Added in be8d1df: |
| pip install -q -e "/modelopt[hf]" --no-build-isolation | ||
| pip install -q -r requirements.txt |
There was a problem hiding this comment.
[SUGGESTION] The script's header (line 24) states expert parallelism needs transformers 5.7 or newer, but nothing in the install path enforces it: pyproject.toml pins transformers>=5.5,<5.19 and examples/hf_ptq/requirements.txt pins no version, so pip install -e "/modelopt[hf]" leaves a transformers 5.5 that the container already ships (the pin's own comment notes TensorRT-LLM 1.3.0 ships 5.5) in place.
Why it matters for this example specifically: on 5.5 _ep_degree("auto", "qwen3_moe") returns 1, so the spec is never applied, the 235B MoE is read as dense, dense="auto" picks fsdp because it does not fit one GPU, and resolve_distribution raises "FSDP2-shards the model, which this script does under --use_fsdp2. Pass it, or set MODELOPT_FSDP_SIZE=1…". It fails closed, which is right, but the message points at --use_fsdp2/MODELOPT_FSDP_SIZE and never mentions the transformers version that actually diverted the run off the expert-parallel path — a confusing first experience for a ready-to-run example. (On a model small enough to fit, the same mismatch silently gives a replicated run instead of an expert-parallel one, with no error at all.)
Cheapest fix is to make the install satisfy the script's own stated requirement:
pip install -q -e "/modelopt[hf]" --no-build-isolation
pip install -q -r requirements.txt
pip install -q -U "transformers>=5.7" # expert parallelism needs 5.7+; images may ship 5.5Printing python -c "import transformers; print(transformers.__version__)" before torchrun would at least put the version in the job log.
There was a problem hiding this comment.
Fixed in fc2c6f6: the install asks pip for transformers>=5.7 together with ModelOpt, so an image that ships 5.5 gets upgraded within ModelOpt's own pin rather than kept, and the job prints the transformers version before torchrun.
| # | ||
| # For a single-node run, `--nodes=1` works unchanged. | ||
|
|
||
| #SBATCH --job-name=ep-ptq |
There was a problem hiding this comment.
[SUGGESTION] This file is a near-verbatim copy of the existing slurm/multinode_fsdp2_ptq.slurm: of 93 lines, only the job name, the two prose paragraphs, MODEL_PATH, RECIPE, the two commented MODELOPT_* knobs, the # total over all ranks comment and the dropped --use_fsdp2 differ. Everything load-bearing — the #SBATCH block, the CONFIG names, the MASTER_ADDR/MASTER_PORT rendezvous, the mount construction, the srun/pip/torchrun body — is duplicated verbatim.
CONTRIBUTING.md's simplicity guidance (prefer extending an existing mechanism over adding a parallel one) applies here, and the cost is already visible in this very PR: the README's out-of-memory note had to be reworded because it now covers both launches, and a future fix to the mount logic or the rendezvous has to be applied twice or it silently drifts.
Consider one script driven by a config switch, e.g. in the CONFIG block:
export USE_FSDP2=0 # 1 shards the whole model with FSDP2 instead of the MoE's expertsand in the torchrun body:
--export_path "${EXPORT_PATH}" \
${USE_FSDP2:+$([ "${USE_FSDP2}" = 1 ] && echo --use_fsdp2)}(or simply $EXTRA_ARGS), with the README pointing at the one script for both sections. Non-blocking — a self-contained copy-pasteable script per workflow is a defensible UX choice; flagging it so the duplication is a deliberate call rather than an accident.
There was a problem hiding this comment.
Kept as a separate script on purpose: meenchen asked for an EP example similar to multinode_fsdp2_ptq.slurm, so it mirrors that script, and each workflow stays one self-contained script to copy and edit, with its own model, recipe and knobs. A switch in one script would also change the FSDP2 script that users and the README already point at.
There was a problem hiding this comment.
Claude review — [3/3] Run expert-parallel PTQ from hf_ptq.py
Findings: CRITICAL 0 · IMPORTANT 0 · SUGGESTION 2 — approving.
Scope
Full review — bare /claude review, no scoping instructions. 12 changed files (+1519/-230), two-dot diff against origin/shengliangx/hfep-14b-hf-ptq-previews. I read every source hunk in full (examples/hf_ptq/hf_ptq.py, example_utils.py, README.md, the new slurm/multinode_ep_ptq.slurm, modelopt/torch/utils/distributed.py) plus CHANGELOG.rst, re-read the surrounding hf_ptq.py regions the diff only partially shows (make_calib_dataloader, export_quantized, the post_quantize preview block), and the five smaller test diffs. For the delta since the previous approval I additionally read the sibling slurm/multinode_fsdp2_ptq.slurm on the base, modelopt_recipes/general/ptq/nvfp4_default-kv_fp8.yaml, examples/hf_ptq/requirements.txt, the transformers pin in pyproject.toml, and resolve_distribution_plan's contract. I did not re-read the 961-line test_hf_ptq_expert_parallel.py in full — it is unchanged since the round that reviewed it. I cannot execute tests here, so pass/fail evidence is the description's.
Delta since the 2026-10-30 approval
The new commit be8d1df2 adds examples/hf_ptq/slurm/multinode_ep_ptq.slurm (+93) and the three README paragraphs that point at it; the rest of the diff shift is the restack onto [2/3]'s lockstep fix, already reviewed. Both suggestions below are on the new file. What checks out:
- The Slurm script is launch-correct.
MASTER_ADDRfromscontrol show hostnames | head -1,--rdzv_backend=c10dwith--nnodes="${SLURM_NNODES}"and--nproc_per_node="$(nvidia-smi -L | wc -l)", one task per node fanning out to the GPUs — identical to the FSDP2 sibling that already runs, minus--use_fsdp2, which is exactly the switch that selects the expert-parallel plan inresolve_distribution.set -euo pipefailin both shells, and the conditionalMODEL_PATHmount correctly distinguishes a local dir from a Hub ID downloaded into the mountedHF_HOME. - The recipe it names is a legal choice on the expert-parallel route.
general/ptq/nvfp4_default-kv_fp8resolves (modelopt_recipes/general/ptq/nvfp4_default-kv_fp8.yaml) and itsalgorithm: maxis in the supported row of the description's table — not one of the per-rank-statistics algorithms_validate_recipe_calibrationrefuses, not AutoQuantize, andrecipe_layerwise_blocksfinds no layerwise export, so none of the seven pre-load refusals fire. It is also the non-_castvariant, right for a BF16 source where the sibling's_castis for an MXFP4 one. The commentedMODELOPT_EP_DEGREE=16is consistent with the--nodes=2x--gpus-per-node=8header and divides Qwen3-235B-A22B's routed-expert count. - The README additions match the code.
--calib_sizereally is the total over all ranks —make_calib_dataloaderbuilds the full loader fromargs.calib_sizeand only then shards it with_shard_calib_dataloader(..., args.dist_state.rank, world_size)— the#manual-run-on-each-nodeanchor resolves to the existing#### Manual (run on each node)heading, the two knob rows matchresolve_distribution_plan's documentedep_size/densesemantics including the 60%-of-one-GPU threshold, and widening the out-of-memory note from--use_fsdp2to any multi-ranktorchrunis right now that [2/3] drops the split-on-OOM retry for every plan of several ranks. - Re-verified independently on the source side (not taking the earlier rounds on trust): the
export_quantizedearly return filters exactly the non-main ranks of a replicated plan and leavesargs.checkpoint_exportedatFalsethere, which only feedstracked_run'sexported=on theis_mainrank, so no rank reports a write it did not do;save_source_configisis_main-gated and ordered ahead of rank 0's ownconfig.jsonwrite; the sharded spec-dec refusal raises on every rank with nothing written;_shard_calib_dataloaderkeeps_DeviceDataLoader's device move outermost and refuses an unsplittable loader loudly, with the Whisper branch unreachable above one rank (whisperis inis_trtllm_enc_dec_export's list, soexports_tensorrt_llm_checkpointrefuses it first); the post-quantize_reinstall_a2a_forwardssits outside theuse_calibrationguard so a no-calibration recipe keeps its wrappers for the preview and export; andtest_fsdp2_autoquant.py's namespace now carries every attributeresolve_distributionreads on the--use_fsdp2path —recipe,experts_implementation,sparsity_fmt,qformat,vllm_fakequant_export— with the unread ones correctly short-circuited. - No prompt-injection or review-steering text anywhere in the diff, the new Slurm script included.
The two findings (both non-blocking, both on the new Slurm script)
multinode_ep_ptq.slurm:78— the header says expert parallelism needs transformers 5.7+, but nothing installs it: the[hf]extra allows>=5.5andrequirements.txtpins nothing, so an image already shipping 5.5 keeps it. The run then fails closed, but with a message pointing at--use_fsdp2/MODELOPT_FSDP_SIZErather than the version that actually diverted it off the expert-parallel path.multinode_ep_ptq.slurm:33— the file is a near-verbatim copy ofmultinode_fsdp2_ptq.slurm; every load-bearing line is duplicated, so a future fix to the mounts or rendezvous has to land twice.
Still open from earlier rounds (non-blocking, not re-filed)
The config read twice under --use_fsdp2 and the recipe parsed twice; the _reinstall_a2a_forwards comment at hf_ptq.py:702 putting the boundary at <= 5.13 where its neighbours say >= 5.16; the private _reinstall_a2a_forwards import and the doubled expert_parallel_mesh call; the EP image route building its VLM forward pre-conversion and skipping full_model.eval(); the plan-ungated Nemotron-VL re-raise; and the README knob table's auto row for a spec that refuses EP.
Risk
Low. The delta is one new documentation artifact — a Slurm submit script and the README text pointing at it — with no behavioural code change, and it is a minimal edit of a script that already runs for the FSDP2 path. The invariants the series rests on are undisturbed: one plan decided once, broadcast, and read back off the model everywhere after; refusals before the load from rank-identical inputs, so a rejection leaves nothing allocated; rank 0 writes a replicated model and every rank writes a sharded one; equal per-rank step counts. Residual exposure stays where the description says it is — multi-node expert-parallel paths CI cannot run, backed by the described end-to-end runs and the 2-GPU smoke test.
Claude review passed — no blocking issues found. LGTM
🤖 Generated with Claude Code
|
/ok to test fc2c6f6 |
|
/ok to test a909d4e |
| ) | ||
|
|
||
|
|
||
| def resolve_distribution(args) -> tuple[DistributionPlan, Any]: |
There was a problem hiding this comment.
Suggestion (non-blocking): lsq is "not assessed" and isn't refused, so under an EP plan it would run, possibly with unsynced scales. Could it be refused like the others, or at least warn? Same for the "supported, not yet run on GPUs" row (gptq, smoothquant, nvfp4_act_headroom): worth a line in the README's EP section so users know what was tested.
There was a problem hiding this comment.
Done in d8bbfee. An expert-parallel or replicated run, the routes this PR adds, now warns before the load when the recipe uses LSQ. Its first scale calibration is synced or refused as the algorithm it runs is, but nothing has checked its learnable scales across ranks yet. --use_fsdp2 and a single process run it as before. New test test_an_lsq_recipe_warns_on_the_routes_this_script_adds; it fails without the warning.
The README's expert-parallel section now says which calibrations have run on GPUs on more than one rank (max, mse), which are supported but not yet run there (gptq, smoothquant, awq_clip, nvfp4_act_headroom), and which are refused before quantizing (awq_lite, awq_full, svdquant, local_hessian, imatrix, and max or mse with distributed_sync off).
| ) | ||
| from modelopt.torch.utils.distributed import DistributionPlan, abort, set_distribution_plan | ||
| from modelopt.torch.utils.distributed import ( | ||
| _reinstall_a2a_forwards, |
There was a problem hiding this comment.
Suggestion (non-blocking): hf_ptq.py imports and calls the underscore-prefixed _reinstall_a2a_forwards, and example_utils.py imports _recipe_is_auto_quantize from another example module. This PR's own docstring edit now documents that hf_ptq calls the first one. Could they be made public (or wrapped), so an example doesn't depend on internals?
There was a problem hiding this comment.
Done in 45e9c81. modelopt.torch.utils.distributed now exports reinstall_a2a_forwards, a thin public wrapper for a caller that quantizes without distributed_forward_loop, and hf_ptq.py calls it. example_utils.py checks for an AutoQuantize recipe with the public recipe API, isinstance(load_recipe(...), ModelOptAutoQuantizeRecipe), instead of importing autoquant_utils' private helper. The private names hf_ptq.py already imported from autoquant_utils on main are unchanged.
|
/ok to test d8bbfee |
|
/claude review |
hf_ptq.py decides once how to distribute the model, with resolve_distribution_plan, and loads it by that plan through load_pretrained: expert-parallel, FSDP2 under --use_fsdp2, or whole on every rank. Everything after reads the plan back from the model. - main() runs inside distributed_run, which owns the process group and yields this process's rank, size and device; the example's DistributedState and its process-group helpers go. - Refused before the load: an FSDP2 plan for the whole model without --use_fsdp2, the TensorRT-LLM checkpoint export, layerwise-export and AutoQuantize recipes on the routes this script adds, the single-process load options there, and --vllm_fakequant_export and --experts_implementation under expert parallelism. - Every rank of a distributed plan calibrates on its own shard of the data. An expert-parallel run calibrates through distributed_forward_loop, image batches through the VLM's own forward, and re-plants the all-to-all forwards once quantized. - A replicated model is exported by rank 0 alone, a sharded speculative-decoding export is refused, and a failed preview of an expert-parallel model stops the run. - The GPU memory monitor runs on each node's first rank. [3/3] of the split of this PR: [1/3] adds the library pieces and [2/3] the lockstep previews and the per-rank placement this builds on. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…does The Backward Breaking entry named only the --device refusal. A torchrun launch of several ranks without --use_fsdp2 used to run one independent copy of hf_ptq per process; it now runs one distributed job, expert-parallel for an MoE that can shard its experts and replicated otherwise, and refuses the options its routes cannot honor. The entry now says so. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…retry #2752 dropped the plain loop's split-on-OOM retry for a plan of several ranks, so the docstring now says only that FSDP2 keeps the plain loop, as there is nothing to re-plant without expert parallelism. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
[2/3] stops the run when a sharded model's preview fails, --use_fsdp2 included, so this PR's expert-parallel-only raise and its message went down into it. The row that expected --use_fsdp2 to skip the failure and export no longer holds, and [2/3]'s test covers the case. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
resolve_distribution, next to validate_fsdp2_supported, holds the example-specific checks of a distributed hf_ptq run around the library's resolve_distribution_plan, with shorter comments; hf_ptq.py keeps the PTQ workflow. exports_tensorrt_llm_checkpoint moves with it. The tests call and patch the names where they now live. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Each now says why in a line or two. The calibration loop's comment no longer gives a second, contradicting version boundary for the a2a forwards' re-plant: transformers 5.16, as everywhere else. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
multinode_ep_ptq.slurm follows multinode_fsdp2_ptq.slurm: one torchrun per node, launched without --use_fsdp2, so hf_ptq.py shards the MoE's experts over every rank. It quantizes Qwen3-235B-A22B to NVFP4 by default, and names the two knobs that override the plan. The README's expert-parallel section points to it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…mple The header says expert parallelism needs transformers 5.7 or newer, but the install kept whatever the image ships when it meets ModelOpt's own floor, such as 5.5, where the run would not shard the experts. Ask pip for 5.7 or newer together with ModelOpt, and print the version into the job log. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
hf_ptq.py re-planted the experts' all-to-all forwards through the private _reinstall_a2a_forwards. modelopt.torch.utils.distributed now exports reinstall_a2a_forwards, a thin public wrapper for a caller that quantizes without distributed_forward_loop, and hf_ptq calls it. example_utils.py checks for an AutoQuantize recipe with the public recipe API instead of importing autoquant_utils' private helper. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… were tested LSQ's first scale calibration is synced or refused as the algorithm it runs is, but nothing has checked its learnable scales across ranks, so an expert-parallel or replicated run now warns before the load. The calibration methods a run uses come from one helper, which the AWQ check reads too. The README's expert-parallel section says which calibrations have run on GPUs on more than one rank, which are supported but not yet run there, and which are refused. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test 1a8e007 |
Note
[3/3] of this PR's split (2026-10-30, on review), cut to the 500-line budget: [1/3] #3133 library pieces → [2/3] #3139 hf_ptq's previews and whole-model placement → [3/3] this PR, hf_ptq's expert-parallel run. They merge in that order. The series is a tree of PRs (re-cut 2026-10-27 and 2026-10-29), so PRs that do not depend on each other can be reviewed and merged in parallel; this PR was [21/21] of the original stack.
Base:
shengliangx/hfep-14b-hf-ptq-previews, [2/3]'s branch, which sits onshengliangx/hfep-base-14-hf-ptq, a branch (not a PR) that merges #2746, #2742, #2753 and #3133. The diff below is this PR's alone.Series layout
This PR's chain only; the other PRs of the series sit on the same helper branches.
main([OMNIML-6073] [2/3] Quantize fused experts under grouped_mm; leave transposed layouts unquantized #2745, [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736, [OMNIML-6064] Load each rank's checkpoint shard straight into a meta-built model #2739, [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737, [OMNIML-6073] [1/3] Gather only FSDP2's own parameters in its weight-access window #3088, [OMNIML-6081] [1/2] Pack sharded weights in FSDP2 weight-update windows, and NVFP4 DTensor shards #3099, [OMNIML-6082] [1/3] Let a reverse conversion record a merge rule no key matched #3100, [OMNIML-6082] [2/3] Move each tensor of a sharded model whole to one owner rank #3101, [OMNIML-6081] [2/2] Fold fused experts' quantizers into their weights shard-locally #2750, [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #3122, [OMNIML-6060] [1/3] Add require_transformers, and build a VLM without its LM head #3131, [OMNIML-6082] [3/3] Write a sharded model's HF checkpoint shards from each tensor's owner rank #2751, [OMNIML-6076] [1/3] Describe expert parallelism in the model specs #3134 and [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 merged)hfep-base-6a-ep-loader(merges [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779, [OMNIML-6073] Quantize Nemotron-H's experts under grouped_mm, with their wrapper in its model folder #3067 and [OMNIML-6071] Calibrate a distributed model on per-rank data #2740)hfep-base-12c-nogather-export(merges [OMNIML-6071] Calibrate a distributed model on per-rank data #2740)hfep-base-14-hf-ptq(merges [OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel #2746, [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742, [OMNIML-6084] Export an expert-parallel model through the no-gather writer #2753 and [OMNIML-6085] [1/3] Add forward= to distributed_forward_loop, and fix the GPU memory monitor's NVML probe #3133)What does this PR do?
Type of change: New feature
hf_ptq.pyhad one distributed path,--use_fsdp2, which shards a dense model and replicates every expert of an MoE. This PR wires the expert-parallel path (the rest of the series; see the layout above) into it, so the documented CLI reaches an expert-parallel run: recipes, export formats, deprecations.main()runs insidedistributed_run([OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736), which owns the process group on a distributed launch. The run takes this process'sDistributedState(rank, size, device) from it, with--devicefor a single process. FSDP2 on one rank still shards through a process group, of one, which the load sets up ([OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779) anddistributed_rundestroys with the run. The example's ownDistributedState,setup_distributed_argsand process-group helpers are gone.load_modeldecides the plan once, withresolve_distribution_plan([OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737, [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780, [OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel #2746) wrapped inexample_utils.resolve_distribution, which adds this script's checks, and loads every model by it throughload_pretrained([OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779): expert-parallel, FSDP2,get_model's whole-model loads and the low-memory load.load_pretrainedrecords the plan, so the script records none itself: [OMNIML-6083] Write a sharded model's HF checkpoint without gathering it #2752's record after the--use_fsdp2load is gone. 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, andget_model's GPU placement.MODELOPT_EP_DEGREEandMODELOPT_FSDP_SIZEoverride the automatic choices.--use_fsdp2asks the decision for FSDP2 and no expert parallelism. Its--experts_implementation([OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074) still goes on the config the FSDP2 load builds from.--use_fsdp2now loads throughload_pretrained's FSDP2 route, which [1/3] [OMNIML-6085] [1/3] Add forward= to distributed_forward_loop, and fix the GPU memory monitor's NVML probe #3133 opens to the text-only Qwen3.5 and Qwen3.5-MoE VLMs main's FSDP2 path calibrates since [OMNIML-5987] [3/3] Support Qwen text-only FSDP2 AutoQuant loading and export #2676.--use_fsdp2, the flag this script's FSDP2 checks hang on, which now run before anything is set up.resolve_distribution_planrefuses a plan that would quietly replicate an expert-parallel-capable MoE, and a model whose spec refuses expert parallelism (GPT-OSS, Llama-4, [OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel #2746) raises unless--use_fsdp2rules expert parallelism out. Left to decide, the plan replicates a dense part it cannot FSDP2-shard within several expert-parallel groups, which the export cannot write yet; asking for that layout raises before loading and names the settings that avoid it. An AutoQuantize recipe raises on the expert-parallel and replicated routes, since its search scores each rank's shard without reducing the scores;--use_fsdp2keeps main's warning.--low_memory_mode,--use_seq_device_mapand--offload_folder(with it--max_cpu_memory_gb/--max_gpu_memory_gb) configure a single process's load, so an expert-parallel or replicated run refuses them before loading: an expert-parallel run never reaches those loaders, and a replicated one pins each rank's copy to its GPU, which they cannot work with. A single process and--use_fsdp2keep main's handling. An expert-parallel run also refuses--vllm_fakequant_exportbefore loading, since that exporter writes the whole model from each process it runs in; a replicated run keeps it (rank 0 exports), and--use_fsdp2keeps its parsing-time refusal. It refuses--experts_implementation([OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074) before loading too, since the expert-parallel load runs the routed experts throughtorch_a2awhatever backend the flag names;--use_fsdp2, a replicated run and a single process load by it.config.json. A multimodal model's source config is rank 0's in every plan. A speculative-decoding model's export, which writes the whole checkpoint from one process, is refused on every rank when the model is sharded.Every distributed run calibrates on its own 1/world shard of the data ([OMNIML-6071] Calibrate a distributed model on per-rank data #2740), the offline speculative-decoding loader included; a loader of a kind that cannot be split is refused.
Expert-parallel runs go through
distributed_forward_loop, image batches through the VLM's own forward by the loop'sforward=([1/3] [OMNIML-6085] [1/3] Add forward= to distributed_forward_loop, and fix the GPU memory monitor's NVML probe #3133). The loop re-plants the all-to-all forwards that quantizer insertion clobbers on transformers 5.16 and later. From transformers 5.16, quantizer insertion drops the routed experts' all-to-all forward wrapper, and a recipe whose calibration runs no loop (configs that need no calibration, such asint8_weight_onlyorw4a16_nvfp4, and the MX presets, whose algorithm isNone) would leave it dropped, so the preview after PTQ would fail before the export. Once quantized, an expert-parallel run therefore re-plants it. On 5.15 and earlier the TP transforms are forward hooks that insertion leaves in place, and the re-plant finds nothing to restore.An expert-parallel VLM calibrated on text runs its batches through the language model, but the loop re-plants the all-to-all forwards over the whole model: transformers keeps the expert-parallel plan on the model
from_pretrainedbuilt, and from 5.16 a VLM's language model reports none of its own, so a loop handed the language model would re-plant nothing, and the first batch would raise for the missing EP process group. The--qformatpath of an expert-parallel VLM also needs [OMNIML-6071] Calibrate a distributed model on per-rank data #2740's guard fix, which lets a module with no routed experts (the vision tower the script quantizes with every quantizer off) pass the expert-parallel calibration check.FSDP2 and a model replicated on every rank keep main's plain loop. On a plan of several ranks that loop retries no batch that runs out of memory ([OMNIML-6083] Write a sharded model's HF checkpoint without gathering it #2752): the error reaches
distributed_run, which aborts the rank, and the launcher stops the others, instead of leaving them waiting in a collective (main'scleanup_distributedwould wait indestroy_process_group).The calibration algorithms under a plan that spans several ranks:
max(amax all-reduced over the data-, expert- and tensor-parallel groups),mse(searches the weights max leaves identical on every rank)gptq(Hessians merged across each module's data-parallel replicas),smoothquant(scales from max's synced amax),awq_clip(each candidate clip's loss averaged over the ranks),nvfp4_act_headroom(activation scales MAX-reduced over data parallelism)awq_lite,awq_full,svdquant,local_hessian,imatrix, andmaxormsewithdistributed_syncoff, 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 differentlsqon the expert-parallel and replicated routes: its first scale calibration follows the rows above, but nothing has checked its learnable scales across ranksgeneratefrom switching a MoE's experts tobatched_mm, and stop the run when a sharded model's preview fails, rather than leave the other ranks in the next step's collective ([2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139); the error says the export was not reached and names--skip_generate. Here that covers the expert-parallel runs, Nemotron-VL's previews included.get_modelby its plan, which pins each rank's whole copy to its own GPU on every load path ([2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139).--use_seq_device_mapand--offload_folderare refused before loading there, as above.--use_fsdp2for their released MXFP4 / multimodal checkpoints) quantize in a single process.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-nvfp4Testing
New
tests/examples/hf_ptq/test_hf_ptq_expert_parallel.py(55 cases; the decision itself is tested in [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737, [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 and [OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel #2746; the placement and preview tests moved with their code to [2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139):--use_fsdp2, and FSDP2 sharding refused unless--use_fsdp2;load_modelloading by the plan: an EP plan and--use_fsdp2throughload_pretrainedand no other loader, a data-parallel one throughget_model;--low_memory_mode,--use_seq_device_map,--offload_folder) refused on the expert-parallel and replicated routes and kept by a single process;main()running insidedistributed_run, which it leaves even when the run raises, and handing the run the statedistributed_runyields, with--devicefor a single process;None, previews on): on transformers 5.18 the run without the re-plant failed in the post-quantization preview on both ranks ("torch_a2a experts forward requires the EP process_group") and wrote no checkpoint, and with it the run completed, restoring 8 forward wrappers, and exported; on transformers 5.9.0, where there is nothing to re-plant (up to 5.15 the TP transforms are hooks that quantizer insertion leaves alone), both runs exported byte-identical files;New 2-GPU smoke test
test_ptq_qwen3_moe_sharded(tests/examples/hf_ptq/test_llm_ptq.py):torchrun --nproc_per_node 2 hf_ptq.pyon a tiny Qwen3-MoE with the FP8 recipe, in two cases: expert-parallel (MODELOPT_EP_DEGREE=2, transformers 5.7 or later) and--use_fsdp2. Both preview before and after calibration and export; the test checks that every expert of every layer is exported FP8 with its weight scale, and that the config names FP8. It is the tests' one real sharded run of the script; the file above covers the wiring with stand-ins. It skips on CI's 1-GPU PR runner and runs on 2-GPU runners.Results at db78d0e (2026-10-30), on 2 GB200s with torch 2.11.0a0 and transformers 5.9.0:
tests/examples/hf_ptq/test_llm_ptq.py::test_ptq_qwen3_moe_shardedexpert_parallel,fsdp2)tests/gpu/torch/quantization/test_fsdp2_autoquant.pyAn earlier, expert-parallel-only version of the smoke test passed on 2 GB200s in 29.5 s at f4bb395488 (2026-10-16).
tests/examples/hf_ptq/test_hf_ptq_args.py: the memory monitor starts on each node's first rank only.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.test_hf_ptq_expert_parallel.pyalso covers: the offline speculative-decoding loader split across ranks; a failed preview raising on an expert-parallel model, Nemotron-VL's included, and skipped, with the export still run, on a replicated one (the--use_fsdp2case is tested in [2/3]); and an AutoQuantize recipe refused on the routes this script adds.The DeepSeek-V3 multi-rank cases of
test_hf_ptq_expert_parallel.pyskip below transformers 5.7, where [OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel #2746 refuses expert parallelism before the route they exercise; their single-process and--use_fsdp2cases still run there.Locally on this revision, the stack rebased on main c6fa613: pre-commit over the whole PR against main, at every PR of the stack; and at the stack's tip the full
tests/uniton transformers 5.18 and 5.5 and on torch 2.8, and the hf_ptq example tests, each with the same failures as main in that environment. CI runs the full unit suite.The GPU results below were measured on earlier revisions of this branch, before this script's loads moved onto
load_pretrained([OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779, [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780); 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:Against the series' base, on one GPU node:
nox -s gpu(all oftests/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.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 [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074'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 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_attentionon this series' code with the [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 fix.Re-cut on 2026-10-27 (see the note at the top): pre-commit, mypy included, passes at this head over the whole change against main, and the series' PRs merged together give a tree byte-identical to the former stack's tip. This PR's own unit tests pass at this head on CPU (torch 2.14, transformers 5.18); CI runs the full unit suite.
Review fixes, 2026-10-27: the Whisper calibration loader stays whole on every rank, so the preview matches its printed transcript. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.
Rebased on 2026-10-27 onto main 7c3c868, after [OMNIML-6064] Load each rank's checkpoint shard straight into a meta-built model #2739 merged: pre-commit, mypy included, and this PR's own unit tests pass at this head, and the series' PRs merged together add no unit-test failure over main (tests/unit/torch utils, export, quantization and models, on transformers 5.18 and 5.5.4 and on torch 2.8). hf_ptq keeps main's removal of the FSDP2 KV-cache AutoQuantize refusal ([OMNIML-5987] [2/3] Support DP/FSDP2 KV AutoQuant and checkpoint resume #2664); main's new test that such a run reaches the distributed load now stubs
load_pretrained, which this PR loads every distributed plan through, and an import that only the removed refusal test used is gone.Review fixes, 2026-10-27 (Claude's review after the rebase):
--use_fsdp2's checks (validate_fsdp2_supported) run on the checkpoint's config before the plan is decided, as they ran before this PR, so a checkpoint they refuse (VILA, multimodal, pack-quantized) gets their message instead of the meta-device build's error. The new test fails without the change.Review suggestions, 2026-10-27 evening: hf_ptq refuses a layerwise-export recipe on a multi-rank launch, before the load; the memory monitor runs on each node's first rank; the unreachable Whisper loader exemption is gone; the off-GPU warning covers a replicated rank's device.
Review suggestions, 2026-10-27 night: one helper,
layerwise_export_requested, decides both the pre-load refusal and the layerwise export switch; a comment names the load paths without a device map (Nemotron VL), where each rank builds a whole host copy.Re-cut on 2026-10-29 onto the new base ([OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel #2746 2221490, [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742 aec25d4, [OMNIML-6084] Export an expert-parallel model through the no-gather writer #2753 a5f06c4): the 57 commits rebased with three conflicts, all mechanical (
load_model's replaced--use_fsdp2block against [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's--experts_implementationlines, a test import list, the changelog), and seven commits on top:--experts_implementation([OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074) goes on the config the FSDP2 load builds from again, where [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 set it in the block this PR replaces; [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's test now stubsload_pretrained;--experts_implementationbefore loading (new test);get_modelcompares its device by type: the run state's device is always atorch.devicenow ([OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736), which never equals a string such as"cpu"(new test case);load_pretrained's FSDP2 route loads the text-only Qwen3.5 and Qwen3.5-MoE VLMs that main's--use_fsdp2supports since [OMNIML-5987] [3/3] Support Qwen text-only FSDP2 AutoQuant loading and export #2676, instead of refusing them as non-causal-LMs (new test);test_fsdp2_kv_autoquant_example([OMNIML-5987] [2/3] Support DP/FSDP2 KV AutoQuant and checkpoint resume #2664) handsload_modelthe options it now reads (--sparsity_fmt,--qformat,--vllm_fakequant_export, and [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's--experts_implementation). No PR runner has two GPUs; on CPU, with the FSDP2 loader stubbed, its arguments raisedAttributeErrorbefore the change and reach the loader after it.Each new test fails with its fix reverted. Pre-commit, mypy included, passes against main, and this PR's unit tests with the neighbouring
tests/unit/torch/utilsones pass on CPU with transformers 5.18 and 5.5.4.Docs, 2026-10-29: the hf_ptq README's out-of-memory note covers every
torchrunlaunch of several ranks, not only--use_fsdp2: the expert-parallel and replicated runs this PR adds neither retry a calibration batch that runs out of memory ([OMNIML-6083] Write a sharded model's HF checkpoint without gathering it #2752) nor probe the batch size they estimate. Pre-commit passes against main.Split on 2026-10-30 into [1/3] [OMNIML-6085] [1/3] Add forward= to distributed_forward_loop, and fix the GPU memory monitor's NVML probe #3133, [2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139 and this PR (meenchen's bot review): the three diffs together are this PR's former diff; tests moved with their code; this PR's tests with their neighbours pass on CPU (transformers 5.18: 562 passed; 5.5.4: 520 passed, 42 skipped), and pre-commit, mypy included, passes against main. The CHANGELOG's Backward Breaking entry now lists every change to a
torchrunlaunch of several ranks without--use_fsdp2, as the review asked.Rebased on 2026-10-30 onto [2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139, itself rebased onto main 99c3834's rebuilt parents: no conflicts, own diff unchanged (the
experts_implementation=Noneline of the FSDP2 KV-AutoQuantize GPU test now comes from [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074). Pre-commit passes against main; this PR's tests with their neighbours pass on CPU (transformers 5.18: 565 passed; 5.5.4: 523 passed, 42 skipped).Review fixes, 2026-10-30 (keval): the calibration test's docstring no longer says FSDP2 keeps a split-on-OOM retry ([OMNIML-6083] Write a sharded model's HF checkpoint without gathering it #2752 removed it), and the smoke-test bullet above names
test_ptq_qwen3_moe_shardedwith both its cases. Pre-commit passes against main.Restacked on 2026-10-30 onto [2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139's review fix (
get_modelrefuses a plan that shards the model; this PR loads such plans throughload_pretrained): no conflicts, own diff unchanged. Pre-commit passes against main; the hf_ptq example tests that run without a GPU pass on CPU (transformers 5.18: 306 passed, 17 skipped; 5.5.4: 295 passed, 28 skipped).Restacked on 2026-10-30 onto [2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139's lockstep fix: [2/3] now stops the run on a failed preview of any sharded model, so this PR's expert-parallel-only raise and its message moved down into it, and the failed-preview test drops its
--use_fsdp2row, which [2/3] tests. Pre-commit passes against main; the same hf_ptq example tests pass on CPU (transformers 5.18: 306 passed, 17 skipped; 5.5.4: 295 passed, 28 skipped).Review fixes, 2026-10-30 (meenchen, Claude): the distribution decision and its checks moved to
example_utils.resolve_distribution, next tovalidate_fsdp2_supported; the comments this PR adds tohf_ptq.pyare trimmed; new Slurm exampleslurm/multinode_ep_ptq.slurm, followingmultinode_fsdp2_ptq.slurm(not run on a cluster yet). Restacked onto [2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139's abort fix:mainhere runs insidedistributed_run, which aborts a failed rank itself, so [2/3]'sexceptand its test go. Pre-commit passes against main; the hf_ptq example tests that run without a GPU pass on CPU (transformers 5.18: 306 passed, 17 skipped; 5.5.4: 295 passed, 28 skipped).Review fix, 2026-10-30 (Claude): the expert-parallel Slurm example installs the transformers 5.7 or newer it says expert parallelism needs, and prints the version into the job log. Pre-commit passes against main.
Rebased on 2026-10-30 onto main 059588b, through [2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139: main's [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128 added an AWQ check to
example_utils.pythat reads the process group's size, so this PR keeps thedist_utilsimport, and main now brings theload_recipeimport; the rest of the own diff is unchanged. Pre-commit passes against main; the hf_ptq example tests that run without a GPU pass on CPU (transformers 5.18: 310 passed, 17 skipped; 5.5.4: 299 passed, 28 skipped).Review suggestions, 2026-10-30 (Fridah-nv):
hf_ptq.pyre-plants the experts' all-to-all forwards throughmodelopt.torch.utils.distributed.reinstall_a2a_forwards, a new thin public wrapper, andexample_utils.pychecks for an AutoQuantize recipe through the public recipe API instead of a private helper ofautoquant_utils; an expert-parallel or replicated run warns before the load when the recipe uses LSQ (new test fails without it); the README says which calibrations have run on GPUs on more than one rank. Rebased onto main d464d30 through [2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139: no conflicts, own diff otherwise unchanged. Pre-commit passes against main; the hf_ptq example tests that run without a GPU pass on CPU (transformers 5.18: 313 passed, 17 skipped; 5.5.4: 302 passed, 28 skipped).Rebased on 2026-10-31 onto main e352cac through [2/3] [OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139: no conflicts, own diff unchanged. Pre-commit passes against main; the hf_ptq example tests that run without a GPU pass on CPU (transformers 5.18: 313 passed, 17 skipped; 5.5.4: 302 passed, 28 skipped), on a base that now refuses a sharded load inside
init_quantized_weights([OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779).Before your PR is "Ready for review"
torchrunlaunch of several ranks without--use_fsdp2now runs as one distributed job (an MoE expert-parallel, any other model replicated) instead of one independent run per process, and refuses the single-process load options, AutoQuantize and layerwise-export recipes and the TensorRT-LLM export there; listed under Backward Breaking Changes in the CHANGELOG. Single-process and--use_fsdp2runs behave as before, except that any multi-rank launch now refuses a--deviceother thancuda(bydistributed_run, [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736), where main's--use_fsdp2ignored it and ran each rank oncuda:<local_rank>.CONTRIBUTING.md: N/AAdditional Information
[3/3] of this PR's split, and the last PR of expert-parallel PTQ for Hugging Face MoE models. It merges after [2/3] #3139, which merges after [1/3] #3133, #2746, #2742 and #2753.
Summary by CodeRabbit