Skip to content

[OMNIML-6083] Write a sharded model's HF checkpoint without gathering it - #2752

Open
shengliangxu wants to merge 42 commits into
shengliangx/hfep-base-12c-nogather-exportfrom
shengliangx/hfep-12c-nogather-export
Open

shengliangxu wants to merge 42 commits into
shengliangx/hfep-base-12c-nogather-exportfrom
shengliangx/hfep-12c-nogather-export

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

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_fsdp2 load 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_fsdp2 plan 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

What does this PR do?

Type of change: New feature, with a bug fix for models built from a config

distributed_save_hf_checkpoint writes 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 with MODELOPT_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:

Writer Peak RSS above the shard, per rank
copying every DTensor up front (previous revision) 278 / 246 MiB
copying only inference tensors 224 / 217 MiB
copying only inference tensors, with #2751's send-copy fix (this revision) 162 / 166 MiB

Why this doesn't build on torch.distributed.checkpoint's HuggingFaceStorageWriter: that writer saves each state-dict key as it is, and the checkpoint needs other keys, made from whole tensors:

  • Per-expert keys. The model holds a layer's experts fused, as mlp.experts.gate_up_proj of 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.
  • The reverse conversion needs whole tensors. transformers' conversion ops take whole tensors and know no placement, and some reverse merges concatenate along dim 0, the dim FSDP2 shards. hrm_text, for one, loads attn.gqkv_proj.weight with Chunk(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.
  • Files a loader reads. With save_distributed=True and no consolidation, a DTensor's chunks land in several files under one key, placed by offsets in DCP's own metadata, and no model.safetensors.index.json is written: only DCP's reader reassembles them, not from_pretrained, vLLM or TensorRT-LLM. With enable_consolidation=True, the writer's finish runs consolidate_safetensors_files on 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. With save_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_checkpoint on this path:

  • packs quantized weights with keep_fused_experts;
  • writes config.json and a sanitized generation_config.json on rank 0 through save_non_weight_artifacts, as the FSDP2 streaming writer does, then hf_quant_config.json. hf_quant_config.json names 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_checkpoint returns whether the names were reverted);
  • copies the source's non-model files (tokenizer, processor, remote code, chat templates), which the writer never sees, also through 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_fsdp2 load records its plan, DistributionPlan(world_size, dense="fsdp"), right after the load. A model sharded by hand, with fsdp2_wrap say, records it with set_distribution_plan; without it the export raises rather than treat it as whole. Refusing a model with no plan, rather than inferring DistributionPlan(world_size, dense="fsdp") from its FSDP2 modules, is deliberate: the export reads the plan through #2736's distribution_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_fsdp2 model 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_fsdp2 users 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_fsdp2 on 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's MODELOPT_FSDP2_EXPORT.

Usage

MODELOPT_FSDP2_EXPORT=nogather torchrun --nproc_per_node 8 examples/hf_ptq/hf_ptq.py --use_fsdp2 ...

Testing

Before your PR is "Ready for review"

  • Is this change backward compatible?: ⚠️ The no-gather writer is opt-in for FSDP2. A model FSDP2-sharded by hand, fsdp2_wrap included, must record its plan with set_distribution_plan before export_hf_checkpoint; without it the export raises. hf_ptq.py --use_fsdp2 on 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.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅ The no-gather FSDP2 export opt-in, and, under Backward Breaking Changes, that the export needs a distributed model's plan recorded and what hf_ptq.py --use_fsdp2 on several GPUs now refuses.
  • Did you get Claude approval on this PR?: ✅ Approved on 2026-10-29 after meenchen's round. Its one suggestion was taken in a later round.

Additional Information

Part of expert-parallel PTQ for Hugging Face MoE models; merges after #2751, #2750 and #2740.

Summary by CodeRabbit

  • New Features
    • Added an opt-in no-gather export for FSDP2-sharded models. With MODELOPT_FSDP2_EXPORT=nogather, ranks write checkpoint shards in parallel without gathering the full model on one host; MoE experts remain sharded.
    • Distributed exports include non-model files from the source checkpoint when available.
  • Bug Fixes
    • Export and batch-size checks now use the distribution plan recorded during model loading. Manually distributed models need a plan recorded before calibration or export; operations raise an error when a required plan is missing.

@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

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

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

Adds an opt-in FSDP2 no-gather Hugging Face export path. The exporter uses the distribution plan and environment setting to select the path. Distributed ranks process and write checkpoint shards. Tests cover dense and MoE exports, quantization, metadata, and multi-rank writing.

Changes: FSDP2 No-Gather Export

Layer / File(s) Summary
Export routing, packing, and configuration
modelopt/torch/export/unified_export_hf.py, CHANGELOG.rst, examples/hf_ptq/README.md
The exporter uses the distribution plan and MODELOPT_FSDP2_EXPORT=nogather to select the no-gather path. Packing can retain fused experts. The path writes base and generation configuration, and rejects save_modelopt_state=True for non-offloaded exports. The changelog and README document the option and distribution-plan requirements.
Distributed shard processing and writing
modelopt/torch/export/distributed_writer.py
Adds a writer that processes sharded state, synchronizes expert input scales, processes dense and expert weights, assigns tensors to owner ranks, and writes checkpoint shards. Rank 0 adds non-overlapping extra state.
Writer and exporter validation
tests/unit/torch/export/test_distributed_writer.py, tests/gpu/torch/export/test_distributed_hf_export.py, tests/unit/torch/export/test_unified_export_hf.py, tests/unit/torch/export/test_fused_experts_keep_fused.py
Adds CPU, multi-rank, and GPU tests for tensor contents, quantization, checkpoint metadata, sidecar files, and shard assembly. Updates source-file-copy coverage and removes earlier helper-level writer checks and the fused-expert event-order test.

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant unified_export_hf
  participant distributed_save_hf_checkpoint
  participant FSDP2 ranks
  unified_export_hf->>distributed_save_hf_checkpoint: packed model and export options
  distributed_save_hf_checkpoint->>FSDP2 ranks: process sharded tensors and assign owners
  FSDP2 ranks->>distributed_save_hf_checkpoint: owned tensors for checkpoint writing
Loading
Merge Risk: Moderate · up to 12267

Ordinary unsharded exports in multi-rank jobs can fail before reaching their existing export path. Preserve that path before merging. CPU-only gloo exports no longer have the previously reported CUDA-device failure.

Pre-merge checks: Passed 5
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed PASS. The PR changes only modelopt/torch/export/distributed_writer.py and modelopt/torch/export/unified_export_hf.py in the package; their added lines contain no `torch.load(..., weights_only=Fals…
Title check Passed The title clearly and concisely describes the main change: writing Hugging Face checkpoints for sharded models without gathering the full model.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

PASS. The PR changes only modelopt/torch/export/distributed_writer.py and modelopt/torch/export/unified_export_hf.py in the package; their added lines contain no torch.load(..., weights_only=False), pickle-enabled NumPy load, hardcoded trust_remote_code=True, eval, exec, or # nosec. The PR does not modify pyproject.toml or requirements files, and it adds no new PIP dependency.

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

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12b-writer-pieces branch from 1212af6 to 8aef85d Compare October 12, 2026 01:11
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12c-nogather-export branch 2 times, most recently from 4810e33 to 72eab4a Compare October 12, 2026 02:44
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12b-writer-pieces branch from 8aef85d to 6994370 Compare October 12, 2026 02:44
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

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

Built to branch gh-pages at 2026-10-27 00:05 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.54545% with 6 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (shengliangx/hfep-12b-writer-pieces@7a249ef). Learn more about missing BASE report.

Files with missing lines Patch % Lines
modelopt/torch/export/distributed_writer.py 96.15% 3 Missing ⚠️
modelopt/torch/export/unified_export_hf.py 90.62% 3 Missing ⚠️
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           
Flag Coverage Δ
unit 62.30% <94.54%> (?)

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

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

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

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 72eab4a

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12c-nogather-export branch from 72eab4a to a30a335 Compare October 12, 2026 08:39
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12b-writer-pieces branch from 6994370 to 5b2ee4a Compare October 12, 2026 08:39
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test a30a335

@shengliangxu shengliangxu changed the title [17/19] Write a sharded model's HF checkpoint without gathering it [OMNIML-6083] [17/19] Write a sharded model's HF checkpoint without gathering it Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12b-writer-pieces branch from 5b2ee4a to f738cd0 Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12c-nogather-export branch from a30a335 to 751b1d1 Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12b-writer-pieces branch from f738cd0 to 812e4e1 Compare October 13, 2026 01:32
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12c-nogather-export branch from 751b1d1 to 717b73e Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 717b73e

@shengliangxu
shengliangxu removed this pull request from stack #2755 October 13, 2026 17:42
@shengliangxu
shengliangxu added this pull request to stack #2775 October 13, 2026 17:43
@shengliangxu shengliangxu changed the title [OMNIML-6083] [17/19] Write a sharded model's HF checkpoint without gathering it [OMNIML-6083] [17/20] Write a sharded model's HF checkpoint without gathering it Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12c-nogather-export branch from 717b73e to 446370c Compare October 13, 2026 19:38
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12b-writer-pieces branch from 812e4e1 to 6c1cb0e Compare October 13, 2026 19:38
@shengliangxu
shengliangxu removed this pull request from stack #2775 October 13, 2026 19:38
@shengliangxu
shengliangxu added this pull request to stack #2781 October 13, 2026 19:40
@shengliangxu shengliangxu changed the title [OMNIML-6083] [17/20] Write a sharded model's HF checkpoint without gathering it [OMNIML-6083] [19/21] Write a sharded model's HF checkpoint without gathering it Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12c-nogather-export branch from 446370c to 755ebf2 Compare October 13, 2026 21:12
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-12b-writer-pieces branch from 6c1cb0e to a8e0c27 Compare October 13, 2026 21:12
@shengliangxu
shengliangxu removed this pull request from stack #2781 October 14, 2026 00:13
_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>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test ac29323

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants