Repository navigation
[OMNIML-6083] Write a sharded model's HF checkpoint without gathering it - #2752
Open
shengliangxu wants to merge 42 commits into
Open
shengliangxu wants to merge 42 commits into
shengliangxu wants to merge 42 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. |
Contributor
shengliangxu
force-pushed
the
shengliangx/hfep-12b-writer-pieces
branch
from
October 12, 2026 01:11
1212af6 to
8aef85d
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-12c-nogather-export
branch
2 times, most recently
from
October 12, 2026 02:44
4810e33 to
72eab4a
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-12b-writer-pieces
branch
from
October 12, 2026 02:44
8aef85d to
6994370
Compare
Contributor
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## shengliangx/hfep-12b-writer-pieces #2752 +/- ##
=====================================================================
Coverage ? 62.30%
=====================================================================
Files ? 656
Lines ? 73764
Branches ? 0
=====================================================================
Hits ? 45955
Misses ? 27809
Partials ? 0
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:
|
Collaborator
Author
|
/ok to test 72eab4a |
shengliangxu
force-pushed
the
shengliangx/hfep-12c-nogather-export
branch
from
October 12, 2026 08:39
72eab4a to
a30a335
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-12b-writer-pieces
branch
from
October 12, 2026 08:39
6994370 to
5b2ee4a
Compare
Collaborator
Author
|
/ok to test a30a335 |
shengliangxu
force-pushed
the
shengliangx/hfep-12b-writer-pieces
branch
from
October 13, 2026 00:53
5b2ee4a to
f738cd0
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-12c-nogather-export
branch
from
October 13, 2026 00:53
a30a335 to
751b1d1
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-12b-writer-pieces
branch
from
October 13, 2026 01:32
f738cd0 to
812e4e1
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-12c-nogather-export
branch
from
October 13, 2026 01:32
751b1d1 to
717b73e
Compare
Collaborator
Author
|
/ok to test 717b73e |
shengliangxu
force-pushed
the
shengliangx/hfep-12c-nogather-export
branch
from
October 13, 2026 19:38
717b73e to
446370c
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-12b-writer-pieces
branch
from
October 13, 2026 19:38
812e4e1 to
6c1cb0e
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-12c-nogather-export
branch
from
October 13, 2026 21:12
446370c to
755ebf2
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-12b-writer-pieces
branch
from
October 13, 2026 21:12
6c1cb0e to
a8e0c27
Compare
_write_hf_export_config rewrote generation_config.json, which every export route has already written by then: save_pretrained on the plain path, save_non_weight_artifacts on the streaming paths and _write_base_config, behind can_generate(), on the no-gather path. The layerwise path returns before it. Main's helper never wrote it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…r not The overlap check ran its all_reduce only where extra_state_dict was non-empty, but a sharded export hands the tensors it carries over (an MTP head, an auxiliary tower) to rank 0 alone. Rank 0 then entered the overlap collective while the others went on to "shard counts", and every sharded export carrying weights failed with "Ranks reduced out of step". Every rank joins now; only rank 0, which holds the extras, checks and adds them. The two-rank writer job hands its extras to rank 0 only, as the export does. Found by the epdp port. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The comment credited is_fsdp2_model with choosing the writer; the recorded plan and the MODELOPT_FSDP2_EXPORT opt-in choose it. The assertion stays: it checks the model is really sharded, so the test exercises the shard writer. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…rank 0's writer choice The no-gather export passed the raw kv_cache_quant_algo to the writer, so a mixed-precision KV cache reached postprocess_state_dict as the "MIXED_PRECISION" sentinel and failed its format assert. It now resolves the per-layer map through _get_kv_cache_postprocess_config, as the gather and streaming exports do; the single-process comparison covers an FP8 and NVFP4 KV cache. Which writer runs is rank 0's MODELOPT_FSDP2_EXPORT, broadcast: the two writers issue different collectives. The two-rank export sets the knob on rank 0 only. The no-gather branch drops a save_modelopt_state write it refused earlier, and _write_base_config says why a config save needs no revert_weight_conversion patch. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The no-gather writer keeps the in-memory tensor names on every rank when any rank cannot reverse the conversion mapping (a merge group split across ranks), but export_hf_checkpoint renamed the quant config to hub names regardless, so exclude_modules named modules the checkpoint did not hold and a loader would read an excluded layer as quantized. distributed_save_hf_checkpoint now returns whether the names were reverted, and the config is renamed only when they were. The experts' input-scale all-reduce runs on the scales' own device instead of one picked from the backend name, and the CPU tests now cover it: the one-rank Qwen2-MoE export quantizes expert inputs (its comment called the reduce CUDA-only), and a two-rank export whose ranks calibrated different input amaxes must write the largest on every expert. Also drops a comment that described no code, and the GPU test's description of a staging step this writer never had, with the assertion that nothing left that staging directory behind. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The no-gather writer copied every DTensor's local shard up front to drop the inference flag, though only an inference tensor carries it, and its comments called the copy free: the state dict's values are views of the model's own parameters, so each copy adds to the rank's memory instead of replacing a value. A DTensor is now copied only when its local shard is an inference tensor, as a plain tensor already was, so a caller outside inference mode (hf_ptq exports under no_grad) copies nothing. On two gloo ranks exporting a 223 MiB bf16 Llama, the writer's peak above each rank's 112 MiB shard drops from 278 and 246 MiB to 224 and 217 MiB. The docstrings and comments now state the bound the code has. Also drops an unused loop index. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Since [8/21], get_max_batch_size measures a model with no recorded plan per rank, as it did before, so only export_hf_checkpoint raises for a sharded or multi-rank model that carries no plan. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…xport does The no-gather FSDP2 writer refused save_modelopt_state=True, so opting into it broke an export the default FSDP2 path completes. That path saves the state from rank 0 with torch.save, not through save_pretrained, and every FSDP2 rank holds the same state: the no-gather path now does the same. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The writer max-reduces the experts' shared input scales over keys each rank derives from its own state dict. A rank holding none skipped the reduce and left the others waiting in it, and lists of different lengths mismatched its size, with nothing naming the cause. The ranks now agree on the keys first, through the labeled all_reduce the writer's other checks use, and a difference fails on every rank, naming the keys. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The writer pieces now place a merge group with one owner when they are told the model's groups, but the export did not tell them: a layer's q, k and v could land on different ranks, the merge then failed, and the whole checkpoint kept its in-memory names. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Any value other than nogather quietly took the streaming writer, so a misspelled one (no-gather, NOGATHER, 1) ran the path it was set to avoid. The knob now takes nogather, or streaming, the default, and raises on anything else; rank 0 reads it inside broadcast_from, so the refusal reaches every rank. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…hape The two-rank writer job now writes its MoE in inference mode, with a 63-row buffer made there that two ranks shard 32 + 31, so a DTensor reaches _to_normal as an inference tensor. Without the shape and stride the rewrap passes, the two shards re-infer different global shapes and the write fails on every rank. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_prepare_model_for_export returns a tied-weight map the no-gather arm never reads: the writer builds its own from the model's names. The arm now discards it, and says so. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The writer pieces now import torch.distributed, DTensor, all_reduce and revert_weight_conversion_quant_aware at module scope. The no-gather export's own import of the expert-split helpers joins them, rather than following the checkpoint-file pattern, and its function-local imports of the same three names go. A test that fails the reverse conversion has to patch the name the writer calls, which is now its own module-scope import: patching it on quant_aware_conversion no longer reaches the writer, so the test would see every name reverted and no warning. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
export_hf_checkpoint now reads how a model is distributed from its recorded plan and refuses an FSDP2-sharded model that records none, so hf_ptq --use_fsdp2 records DistributionPlan(world_size, dense="fsdp") right after its load. The recording moved here from the calibration PR, whose export did not need it yet, and brings what it switches on there: on more than one GPU the ranks size the calibration batch together and calibration that keeps its statistics on each rank is refused. So the README notes on the refusal and the batch size, the AutoQuantize clause, the backward-breaking changelog entry and the test that the load records its plan come back with it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The calibration loop retries a batch that runs out of GPU memory as two halves. For a model whose recorded plan spans several ranks, such as hf_ptq's --use_fsdp2 model now that its load records one, a forward can hold collectives: a rank that retried would run more forwards than its peers, which would wait in their collectives. The loop now raises the error for such a model and keeps the retry for any other. The README and the changelog entry say so, and tell the user to set --batch_size. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The changelog entry and a GPU test comment said load_pretrained records the plan export_hf_checkpoint reads. Its recording is not below this change, so the entry says the plan is the one recorded on the model, names hf_ptq --use_fsdp2 as a caller that records it, and keeps the advice for a model distributed by hand. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The writer refuses an export directory that already holds a checkpoint, which the default FSDP2 export overwrites, so the README note that introduces MODELOPT_FSDP2_EXPORT=nogather says so. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
export_hf_checkpoint refuses an FSDP2-sharded model with no recorded plan, and neither fsdp2_wrap nor parallel_load_and_prepare_fsdp2 records one. The carry-over export tests wrap their models with fsdp2_wrap, and the loader tests load with parallel_load_and_prepare_fsdp2, so they now record DistributionPlan(size, dense="fsdp") themselves. The lines are those the load-routing PR adds to the same tests: that PR was below this one before the 2026-10-29 re-cut, so whichever of the two lands second merges them unchanged. The changelog entry names parallel_load_and_prepare_fsdp2 next to fsdp2_wrap as a way to distribute a model that records no plan. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The comment above the held weight windows was written before any caller set keep_fused_experts, and said that until then no window opened there. The no-gather export sets it, so the comment says so. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The writer held ep_rank and dp_idx at 0, so every rank wrote its experts, and the expert count it read off the config could not change an expert's index. Under FSDP2 alone an expert's global index is the DTensor's own dim-0 offset, which _split_local_fused_module reads. The expert-parallel export, which reads both coordinates from the routed experts' ParallelState, adds them back where it sets them. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…s not The writer imported get_model_state_dict, TiedWeightMap and postprocess_state_dict inside its function, though nothing forces it: it already imports unified_export_hf_streaming at module scope, which imports model_utils and quant_utils itself. They move to the top. The export imports the writer itself inside the branch that runs it, as it does the streaming writers: the writer imports unified_export_hf_streaming, which imports unified_export_hf, so an import at module scope there fails import modelopt.torch.export with a circular import. A comment there now says so. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The two-rank input-scale test hands one rank a state dict without a scale by patching get_model_state_dict. The writer now binds that name at module scope, so the test patches it on distributed_writer instead of on torch's module, which no longer reaches the writer. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_write_base_config repeated save_non_weight_artifacts without its copy of the source's non-model files, which the end of export_hf_checkpoint then made for this writer alone. The FSDP2 streaming writer calls save_non_weight_artifacts from rank 0 once its shards are written, so the no-gather writer now does the same, and the final copy is skipped for both FSDP2 writers again, as on main. What differed does not change what is written. transformers sets a model's generation_config only when it can generate, so the can_generate() guard decided nothing the generation_config check does not. A config's save_pretrained never reaches revert_weight_conversion, so the patch the helper applies around it changes nothing. The config, generation config, quant config, index and copied files of a quantized Llama and Qwen2-MoE export are byte-identical before and after. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_split_local_fused_module took an expert-parallel rank and expert count that its one caller, the FSDP2 writer, never passes: an expert's global index is the DTensor's own dim-0 offset. The parameters, the layouts they told apart, and the test of the numbering no caller here can produce go; the expert-parallel export, which sets them, adds them back. The two-rank fold test still checks each rank's own split at its offset. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…ization starts Main now refuses AWQ with --use_fsdp2 on more than one rank before the model loads (#3128), and its README note says so. The --use_fsdp2 note this PR adds still listed AWQ among the calibrations refused when quantization starts. It now lists the others and points to the AWQ note above it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Only rank 0 is handed the tensors the model does not own, yet the two-rank test's comment and docstring, and the writer's comment on them, said every rank holds them. The export's comment on packing named a consolidation step that does not exist and how main once split preparation from packing, and the writer's step (2) comment named an expert-parallel layout this PR does not handle. Comments and a docstring only. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Collaborator
Author
|
/ok to test ac29323 |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Re-cut again on 2026-10-29, so more of the series merges in parallel: this PR's base branch was rebuilt from the current #2751 and #2750 (both now on main; #3099, [1/2] of #2750's split, merged) and #2740 (on #3074). #2779 left it: this PR needed it only for the plan that hf_ptq's
--use_fsdp2load records, and that recording now lands here. The series first became a tree on 2026-10-27, from main 1521053 (this PR was [16/21]); it was rebased onto main 7c3c868 later that day, after #2739 merged, and onto main f03651e that night.Base:
shengliangx/hfep-base-12c-nogather-export, a branch (not a PR) that merges main and #2740, the PR this one still builds on (#2750 and #2751 have merged); #2740 brings #3074 below it. The diff below is this PR's alone (420 source lines), and it merges after all of them.Re-cut: this PR's diff gained what the PRs below it handed over, listed in the last Testing line:
_split_local_fused_module, the per-rank expert split this writer calls, with its two tests (from #2750's split); hf_ptq's--use_fsdp2plan recording, with its README notes, changelog entry and test (from #2740); no out-of-memory retry in the calibration loop for a model over several ranks (asked for in #2740's review); and the writer's module-scope imports from #2751. Review fixes since the first re-cut are listed at the end of Testing.Series layout
main([OMNIML-6073] [2/3] Quantize fused experts under grouped_mm; leave transposed layouts unquantized #2745, [OMNIML-6064] Load each rank's checkpoint shard straight into a meta-built model #2739, [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736, [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737, [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] [1/2] Pack sharded weights in FSDP2 weight-update windows, and NVFP4 DTensor shards #3099, [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 and [OMNIML-6082] [3/3] Write a sharded model's HF checkpoint shards from each tensor's owner rank #2751 have merged)What does this PR do?
Type of change: New feature, with a bug fix for models built from a config
distributed_save_hf_checkpointwrites an already-processed sharded model as HF safetensors with no rank-0 gather:No rank ever holds the whole model. On top of its own shard, which the model keeps, a rank holds what it writes; only an inference tensor is copied up front, so an export under
no_grad, as hf_ptq runs it, copies nothing. A model sharded by FSDP2 alone opts in withMODELOPT_FSDP2_EXPORT=nogather; the expert-parallel export routes to it unconditionally in #2753.Two gloo ranks exporting a 223 MiB bf16 Llama, the writer's peak RSS above each rank's 112 MiB shard:
Why this doesn't build on
torch.distributed.checkpoint'sHuggingFaceStorageWriter: that writer saves each state-dict key as it is, and the checkpoint needs other keys, made from whole tensors:mlp.experts.gate_up_projof shape [experts, 2 × intermediate, hidden], while an HF checkpoint names each expert's projections (mlp.experts.3.gate_proj.weight). DCP can map a key to a file (fqn_to_index_mapping) but cannot write one key as many, so the split comes first. It is shard-local ([OMNIML-6081] [2/2] Fold fused experts' quantizers into their weights shard-locally #2750) and leaves each rank's experts as plain tensors, whole on that rank, which leaves a sharded writer nothing to do for them.attn.gqkv_proj.weightwithChunk(dim=0)into gate, q, k and v, so the export concatenates the four back. Concatenating each rank's row shards gives [gate_r; q_r; k_r; v_r], which is no row range of the whole. So each tensor has to be whole somewhere before the conversion runs: this writer makes it whole on one owner rank, rather than everything on rank 0.save_distributed=Trueand no consolidation, a DTensor's chunks land in several files under one key, placed by offsets in DCP's own metadata, and nomodel.safetensors.index.jsonis written: only DCP's reader reassembles them, notfrom_pretrained, vLLM or TensorRT-LLM. Withenable_consolidation=True, the writer'sfinishrunsconsolidate_safetensors_fileson the coordinator rank, which reads every sharded file back and writes the checkpoint again: every byte is written twice, the second time by rank 0 alone. Withsave_distributed=False, rank 0 saves a full state dict, the gather this path avoids.Once every tensor is whole on exactly one rank, each rank writes its own set once with
safetensors.save_file, and rank 0 unions the index.export_hf_checkpointon this path:keep_fused_experts;config.jsonand a sanitizedgeneration_config.jsonon rank 0 throughsave_non_weight_artifacts, as the FSDP2 streaming writer does, thenhf_quant_config.json.hf_quant_config.jsonnames modules as the weights do: when any rank cannot reverse the conversion mapping, every rank's weights keep their in-memory names, and the quant config keeps them too (distributed_save_hf_checkpointreturns whether the names were reverted);save_non_weight_artifacts.With
save_modelopt_state, it saves the ModelOpt state from rank 0, as the FSDP2 streaming export does.The export reads how the model is sharded from its distribution plan (#2736), not from an FSDP2 module in it: the FSDP2 routes, streaming or no-gather, and which rank reads the carried-over weights. hf_ptq's
--use_fsdp2load records its plan,DistributionPlan(world_size, dense="fsdp"), right after the load. A model sharded by hand, withfsdp2_wrapsay, records it withset_distribution_plan; without it the export raises rather than treat it as whole. Refusing a model with no plan, rather than inferringDistributionPlan(world_size, dense="fsdp")from its FSDP2 modules, is deliberate: the export reads the plan through #2736'sdistribution_plan, which raises for such a model, because an FSDP2 module does not say how the model is distributed. A guessed plan would leave out the expert-parallel degree, which #2753 numbers each rank's experts by. A diffusers pipeline is not a module and is exported without a plan.The calibration loop no longer retries an out-of-memory batch in halves for a model whose recorded plan spans several ranks, such as hf_ptq's
--use_fsdp2model now that it records one: its forward can hold collectives, so a rank that retried would run more forwards than its peers, which would wait in them. The error is raised instead, and the README tells--use_fsdp2users to set--batch_size. Recording the plan also switches on #2740's refusal of calibration that keeps statistics per rank and its collective batch sizing for--use_fsdp2on several GPUs, so their README notes and backward-breaking changelog entry come here too.The KV-cache format goes through
_get_kv_cache_postprocess_config, so a mixed-precision KV cache exports as on the other paths, and which writer runs is rank 0'sMODELOPT_FSDP2_EXPORT.Usage
Testing
tests/unit/torch/export/test_distributed_writer.pyadds end-to-end cases on CPU under gloo:save_pretrained, tied and untied;extra_state_dictkept under its own names;export_hf_checkpointon the no-gather path, root-wrapped and layers-only: byte-identical to the single-process export, plus config files and the copied source sidecar, for FP8 and NVFP4;exclude_modulesas in the weights;save_modelopt_statesaved from rank 0.tests/unit/torch/export/test_unified_export_hf.py: a model built from a config, which main's Speed up CPU unit tests to stay under the 20 min CI limit #2823 gives no source checkpoint, copies nothing from the working directory at export.test_export_diffusers_pipeline_configon a real tiny pipeline. Only rank 0 reading the carried weights is not unit-tested, since it needs two FSDP2 ranks; the GPU carry-over tests cover it.tests/gpu/torch/export/test_distributed_hf_export.py: PTQs a tiny Qwen3 (dense, with tied embeddings, and MoE) under FSDP2 with FP8 and NVFP4, opting each export into the writer, and checks the checkpoint against the safetensors headers: every routed expert exported quantized, and the tied head written once. Its earlier version passed on a 4-GPU node, 8 tests, including checks of extras, ties, refusals and unquantized layers that now run on CPU above, where every PR runs them. On 1 GPU, all skip. Its docstring no longer describes a DCP staging step this writer never had, and an assertion on asharded/staging directory, which could not fail, is dropped; not re-run on GPU after that change.test_export_diffusers.pytest included. Each drop is checked by mutation.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.MODELOPT_FSDP2_EXPORTtakesnogatherorstreaming(the default) and refuses any other value on every rank._revert_whole_sdtests this PR drops still go, since the end-to-end tests fail when that code breaks), and [OMNIML-6081] [2/2] Fold fused experts' quantizers into their weights shard-locally #2750's stand-in window test, dropped with the imports only it used (its window test now lives in [OMNIML-6081] [1/2] Pack sharded weights in FSDP2 weight-update windows, and NVFP4 DTensor shards #3099'stest_sharded_weight_packing.py). Added on top:_split_local_fused_modulewithtest_split_local_numbers_experts_globallyand each rank's own split checked in [OMNIML-6081] [2/2] Fold fused experts' quantizers into their weights shard-locally #2750's two-rank fold test; hf_ptq's plan recording withtest_the_fsdp2_load_records_the_plan_it_loaded_by; the no-retry with a test that fails without it (both a loop that always splits and one that splits for a one-rank plan fail it); and the module-scope imports, with the test that fails the reverse conversion now patching the name the writer calls (it failed on the new base, and passes). Pre-commit, mypy included, passes against the base and against main 6c475d7. On CPU (torch 2.14, transformers 5.18), the export unit tests,test_dataset_utils.py,test_distributed.pyand hf_ptq's argument tests pass (782;test_export_diffusers.pyleft out: its 7 failures here are main's too). At [OMNIML-6084] Export an expert-parallel model through the no-gather writer #2753's head, which carries this PR, they also pass on transformers 5.5.4. A no-gather export on one gloo rank writes INT4-AWQ, MXFP4, NVFP4 and FP8 Llama and Qwen2-MoE checkpoints byte-identical to the single-process export, so [OMNIML-6081] [1/2] Pack sharded weights in FSDP2 weight-update windows, and NVFP4 DTensor shards #3099's refusal to pack a DTensor shard in a 4-bit packer is not reached: the fold packs each rank's experts as plain tensors, and the dense weights are packed inside FSDP2 weight windows. GPU tests not re-run; CI runs them.parallel_load_and_prepare_fsdp2next tofsdp2_wrapas recording no plan, and the comment above the held FSDP2 weight windows says the no-gather export setskeep_fused_experts. Pre-commit, mypy included, passes against the base and against main. The two GPU files collect on CPU. On one gloo rank, the default FSDP2 export of a model with a recorded plan is byte-identical to the single-process export (FP8 and NVFP4, Llama and Qwen2-MoE). The GPU suites were not run.ep_rank/dp_idxheld at 0, the expert count); [OMNIML-6084] Export an expert-parallel model through the no-gather writer #2753 adds it back with what sets it. The writer imports its helpers at module scope, and the export's import of the writer stays local, with a comment naming the circular import a module-scope one causes. Rank 0 writes the non-weight files throughsave_non_weight_artifacts, as the FSDP2 streaming writer does:_write_base_configand the special case in the final copy are gone. Quantized Llama and Qwen2-MoE no-gather exports write byte-identical config, generation config, quant config, index and copied files before and after. On CPU, the export unit tests,test_dataset_utils.py,test_distributed.py,test_moe_a2a.pyand hf_ptq's argument tests pass on transformers 5.18 (829) and 5.5.4 (794 passed, 35 skipped); without transformers, the unit tests pass or skip (438 passed, 93 skipped) andimport modelopt.torch.exportdoes not import the writer. Pre-commit, mypy included, passes against the base and against main. GPU tests not run; CI runs them._split_local_fused_modulenumbers experts by the FSDP2 offset alone. Its expert-parallel parameters and their numbering test move to [OMNIML-6084] Export an expert-parallel model through the no-gather writer #2753, which sets them. On CPU, the export unit tests,test_dataset_utils.py,test_distributed.py,test_moe_a2a.pyand hf_ptq's argument tests pass on transformers 5.18 (824) and 5.5.4 (789 passed, 35 skipped), and without transformers (438 passed, 93 skipped). Pre-commit, mypy included, passes against the base and against main.split_fused_experts_state_dict's note on memory and expert numbering now merges [OMNIML-6081] [2/2] Fold fused experts' quantizers into their weights shard-locally #2750's final wording with this PR's. [OMNIML-6081] [2/2] Fold fused experts' quantizers into their weights shard-locally #2750's final check that the first projection anddown_projhold the same number of experts holds for_split_local_fused_module, which passes each rank's local shards of both, and FSDP2's dim-0 sharding gives them equal counts; the two-rank fold test, three experts over two ranks included, passes. On CPU, the export unit tests,test_dataset_utils.py,test_distributed.py,test_moe_a2a.pyand hf_ptq's argument tests pass on transformers 5.18 (825) and 5.5.4 (790 passed, 35 skipped), and without transformers the unit tests pass or skip (438 passed, 93 skipped). Pre-commit, mypy included, passes against main. GPU tests not run; CI runs them.test_dataset_utils.py,test_distributed.py,test_moe_a2a.pyand hf_ptq's argument tests pass on transformers 5.18 (827) and 5.5.4 (792 passed, 35 skipped), and without transformers the unit tests pass or skip (438 passed, 95 skipped). GPU tests not run; CI runs them.--use_fsdp2on more than one rank before the model loads, so this PR's README note lists only the other calibrations it refuses when quantization starts (SVDQuant, local Hessian, imatrix) and points to [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128's AWQ note. The restack's conflicts were in the changelog and the README only, where main's new entry and notes sit next to this PR's; all are kept. Pre-commit, mypy included, passes against main. On CPU, the export unit tests,test_dataset_utils.py,test_distributed.py,test_moe_a2a.pyand hf_ptq's argument tests pass on transformers 5.18 (828) and 5.5.4 (793 passed, 35 skipped), and without transformers the unit tests pass or skip (438 passed, 95 skipped). GPU tests not run; CI defers them while this PR targets another PR's branch.test_dataset_utils.py,test_distributed.py,test_moe_a2a.pyand hf_ptq's argument tests pass on transformers 5.18 (843) and 5.5.4 (808 passed, 35 skipped), and without transformers the unit tests pass or skip (453 passed, 95 skipped). GPU tests not run; CI defers them while this PR targets another PR's branch.test_distributed_writer.pyandtest_unified_export_hf.pypass (123). The GPU suites are pending:tests/gpu/torch/export/test_distributed_hf_export.py,tests/gpu/torch/export/test_export_carry_over.pyandtests/gpu/torch/utils/test_model_load_utils.pyhave not run at this head; CI runs them once this PR targets main.test_dataset_utils.py,test_distributed.py,test_moe_a2a.pyand hf_ptq's argument tests pass on transformers 5.18 (843) and 5.5.4 (808 passed, 35 skipped), and without transformers the unit tests pass or skip (453 passed, 95 skipped). The GPU suites are still pending: CI runs them once this PR targets main.Before your PR is "Ready for review"
fsdp2_wrapincluded, must record its plan withset_distribution_planbeforeexport_hf_checkpoint; without it the export raises.hf_ptq.py --use_fsdp2on several GPUs now refuses calibration that keeps its statistics on each rank, and raises on a calibration batch that runs out of GPU memory instead of retrying it smaller.CONTRIBUTING.md: N/Ahf_ptq.py --use_fsdp2on several GPUs now refuses.Additional Information
Part of expert-parallel PTQ for Hugging Face MoE models; merges after #2751, #2750 and #2740.
Summary by CodeRabbit
MODELOPT_FSDP2_EXPORT=nogather, ranks write checkpoint shards in parallel without gathering the full model on one host; MoE experts remain sharded.