Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
ef4d4b9
Pack NVFP4 weights that are DTensor shards
shengliangxu Oct 28, 2026
34d5415
Pack an FSDP2-sharded model's weights in held weight-update windows
shengliangxu Oct 28, 2026
f774bed
Fold fused experts' quantizers into their weights shard-locally
shengliangxu Oct 11, 2026
cc0384c
Open the FSDP2 weight window by the model's distribution plan
shengliangxu Oct 14, 2026
d69333e
Run the keep-fused two-rank test on the shared gloo workers
shengliangxu Oct 15, 2026
4d2d40a
Export a fused-experts projection with no quantized expert as is
shengliangxu Oct 15, 2026
87a6386
Trim the keep-fused fold's tests: one export table, real two-rank shards
shengliangxu Oct 16, 2026
ec602ef
Import the keep-fused fold test's export handlers at module scope
shengliangxu Oct 16, 2026
f8657d5
Fall back per quantizer stage in the keep-fused fold, as the split ex…
shengliangxu Oct 16, 2026
eafde05
Test the keep-fused fold's per-stage amax fallback with a W4A8 expert
shengliangxu Oct 17, 2026
703f299
Drop the source checkpoint's expert scales in the keep-fused fold too
shengliangxu Oct 24, 2026
158cc7f
Split block and packed expert scales by their rows, and refuse a rank…
shengliangxu Oct 24, 2026
8502776
Pair shard-local expert quantizers and refuse what the fold can't write
shengliangxu Oct 24, 2026
8492e0c
Refuse native FP8 experts the fold would leave without their scale_inv
shengliangxu Oct 25, 2026
73570e5
Decide a fused expert projection's fold on every rank alike
shengliangxu Oct 25, 2026
390346a
Fold fused experts in the weight domain the per-expert export packs
shengliangxu Oct 27, 2026
d62f873
Refuse a keep-fused export of fused experts not named experts
shengliangxu Oct 27, 2026
89504e4
Pack each FSDP2 unit of a keep-fused export in one weight window
shengliangxu Oct 27, 2026
efca07b
Wrap the keep-fused fixes' long lines
shengliangxu Oct 27, 2026
64b9c94
Hold an FSDP2 unit's weight window open only where the export opts in
shengliangxu Oct 27, 2026
faa81c6
Add a module to the held weight window once
shengliangxu Oct 27, 2026
044460a
Put the keep-fused fold's stacked scales on the weight's device
shengliangxu Oct 27, 2026
833d957
Hand the fold's expert accessor the module, with only the projection …
shengliangxu Oct 27, 2026
6c93a25
Stack the keep-fused fold's packed experts in place
shengliangxu Oct 27, 2026
455fa15
Say what orders an expert-parallel rank's quantizers, without a symbo…
shengliangxu Oct 27, 2026
aaf9c1e
Say which experts a rank folds inside an FSDP2 weight window
shengliangxu Oct 27, 2026
da2c12f
Replace zero NVFP4 block scales with a scalar, not a tensor of ones
shengliangxu Oct 27, 2026
3ed0377
Reuse the model's module names for a window outside every FSDP2 unit
shengliangxu Oct 27, 2026
b0e1038
Say that a weight-update window reads its module list again on exit
shengliangxu Oct 27, 2026
62a0893
Refuse GGML formats in the keep-fused fold before folding anything
shengliangxu Oct 27, 2026
71b2564
Say which reads of the projection the local rows stand in for
shengliangxu Oct 27, 2026
906c355
Run the keep-fused tests' ungated case on the real NemotronHExperts
shengliangxu Oct 27, 2026
eebbcbe
Refuse BMM experts in the keep-fused export, whose split would cut th…
shengliangxu Oct 27, 2026
00804c5
Say what an unknown expert count means to the local split, not that i…
shengliangxu Oct 27, 2026
d242e3c
Say export handlers pack through _export_weight, inside the held window
shengliangxu Oct 27, 2026
d8f9a0d
Say why the fold's GGML refusal needs no gathered format
shengliangxu Oct 27, 2026
9b06335
Say why the fold's replicated input_scale agrees across ranks
shengliangxu Oct 27, 2026
60c3d81
Drop the split's unreachable checks for a missing fused weight
shengliangxu Oct 27, 2026
912e953
Destroy the gloo fixture's group even when its mesh fails to build
shengliangxu Oct 27, 2026
0a40404
Let the meta-DCP load tests use the shared gloo_world_of_one fixture
shengliangxu Oct 28, 2026
b8be313
Import the keep-fused fold's torch.distributed and GGML names at modu…
shengliangxu Oct 28, 2026
daed636
Merge [1/2], the sharded-weight packing, into the keep-fused fold
shengliangxu Oct 28, 2026
2982230
Leave the sharded-packing tests to [1/2] and the local split to #2752
shengliangxu Oct 28, 2026
136c766
List a module in its unit's held weight window once, however many wei…
shengliangxu Oct 29, 2026
ebfb6a7
Keep the sharded packing's comments to their constraint, and its impo…
shengliangxu Oct 29, 2026
e95c664
Merge [1/2]'s review round, the held window's dedup and the comment t…
shengliangxu Oct 29, 2026
d341421
Drop the BMM experts handler's claim that a rank packs its experts wi…
shengliangxu Oct 29, 2026
3a86980
Test the sharded fold on two ranks again, without the per-rank split
shengliangxu Oct 29, 2026
54f8632
Hand FSDP2 the packed weight of a unit that owns it directly, and tes…
shengliangxu Oct 29, 2026
a322d41
Merge [1/2]'s second review round, the unit-owned weight's naming and…
shengliangxu Oct 29, 2026
51f187f
Test the sharded packing over NCCL on every visible GPU
shengliangxu Oct 29, 2026
0759477
Merge [1/2]'s GPU test of the sharded packing into the keep-fused fold
shengliangxu Oct 29, 2026
af4c213
Pack NVFP4 weights that are DTensor shards
shengliangxu Oct 28, 2026
889d630
Pack an FSDP2-sharded model's weights in held weight-update windows
shengliangxu Oct 28, 2026
8dd7169
List a module in its unit's held weight window once, however many wei…
shengliangxu Oct 29, 2026
34b4f7c
Keep the sharded packing's comments to their constraint, and its impo…
shengliangxu Oct 29, 2026
2da1b3f
Hand FSDP2 the packed weight of a unit that owns it directly, and tes…
shengliangxu Oct 29, 2026
d0cef07
Test the sharded packing over NCCL on every visible GPU
shengliangxu Oct 29, 2026
5dcb830
Merge [1/2], rebased onto main after #3100 merged, into the keep-fuse…
shengliangxu Oct 29, 2026
8674b73
Pack NVFP4 weights that are DTensor shards
shengliangxu Oct 28, 2026
c186322
Pack an FSDP2-sharded model's weights in held weight-update windows
shengliangxu Oct 28, 2026
8d1caf3
List a module in its unit's held weight window once, however many wei…
shengliangxu Oct 29, 2026
9f30d03
Keep the sharded packing's comments to their constraint, and its impo…
shengliangxu Oct 29, 2026
3c7c5d9
Hand FSDP2 the packed weight of a unit that owns it directly, and tes…
shengliangxu Oct 29, 2026
a48ce7a
Test the sharded packing over NCCL on every visible GPU
shengliangxu Oct 29, 2026
cdd8d7c
Merge [1/2], rebased onto main 2b900c8fa41, into the keep-fused fold
shengliangxu Oct 29, 2026
26e9863
Drop the fused-experts handler's claim that FSDP2 ranks pack their ex…
shengliangxu Oct 29, 2026
d568dac
Refuse a DTensor shard in the 4-bit packers that cannot pack one
shengliangxu Oct 29, 2026
6033bb8
Keep a module the weight window cannot name off the root's own key
shengliangxu Oct 29, 2026
fa69b30
Merge [1/2]'s third review round, the packers' DTensor refusals and t…
shengliangxu Oct 29, 2026
ec2741c
Test the sharded fold over NCCL on every visible GPU
shengliangxu Oct 29, 2026
46742ff
Say where the held weight windows take effect, and what ends a hung G…
shengliangxu Oct 29, 2026
6f1d1da
Merge [1/2]'s comments on where the held weight windows take effect i…
shengliangxu Oct 29, 2026
830d704
Merge main, with [1/2] merged as 6c475d716cf, into the keep-fused fold
shengliangxu Oct 29, 2026
fc6d998
Say how the per-expert split numbers the experts it is given
shengliangxu Oct 29, 2026
945271d
Refuse fused projections whose expert counts differ, and check the GP…
shengliangxu Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions modelopt/torch/export/hf_export_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from modelopt.torch.utils.distributed import distribution_plan

from .layer_utils import get_expert_linear_names, is_quantlinear, set_expert_quantizer_amax
from .moe_utils import _export_fused_experts
from .moe_utils import _export_fused_experts, _export_fused_experts_keep_fused
from .quant_format import QUANTIZATION_NONE
from .quant_utils import get_quantization_format
from .registry import ExportContext, ExportModuleRegistry, PrepareMoEInputsRegistry
Expand Down Expand Up @@ -226,13 +226,37 @@ def _export_moe_linear(name: str, module: nn.Module, ctx: ExportContext) -> None
def _export_fused_experts_module(name: str, module: nn.Module, ctx: ExportContext) -> None:
"""Split and quantize a fused-experts module with plural weight quantizers.
Under FSDP2 each rank holds only some experts, so it packs just those (kept fused) and the
split into per-expert keys is deferred until the gather brings all experts together.
Tied experts are packed independently and their duplicate keys are dropped by name
in postprocess_state_dict; no per-module dedup cache is used.
Distributed no-gather export (``ctx.keep_fused_experts``) folds the quantizers into the fused
weight IN PLACE, quantizing only this rank's local experts and keeping the result ``Shard(0)`` --
the per-expert split happens later on the sharded weight (``split_fused_experts_state_dict``).
Otherwise (single process, or gather export) it splits into full per-expert submodules here.
"""
_export_fused_experts(module, ctx.dtype)
attr = name.rpartition(".")[2]
if ctx.keep_fused_experts and attr != "experts":
# The split into per-expert keys finds a fused weight by its ``experts.<proj>`` key, as the
# writer does, so one named otherwise would be written fused and fail only at load.
raise NotImplementedError(
f"{name}: the keep-fused export splits fused experts into per-expert keys by their "
f"`experts` attribute name, and this module is named {attr!r}."
)
Comment on lines +237 to +244

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] The new refusal closes the gap between the handler's structural eligibility and the split's name-based lookup, but it is one character looser than the lookup it guards. name.rpartition(".")[2] yields "experts" both for model.layers.0.mlp.experts and for a bare top-level name == "experts" (rpartition on a dotless string returns ("", "", "experts")), while _fused_experts_prefixes requires a leading dot: key.endswith(f".experts.{proj}") (moe_utils.py:646). A root-level experts attribute therefore passes this check, gets folded, and is then silently skipped by the split — written fused, failing only at load, which is exactly the failure this check exists to prevent.

(2) why it matters: contrived for a real HF model, so low likelihood — but it reintroduces the silent-at-write / fail-at-load mode the check was added for, and the guard reads as if it were exhaustive. (3) fix: make the two agree, e.g. require "." in name here too, or relax _fused_experts_prefixes to key.endswith(f"experts.{proj}") with a .-or-start check. Matching on name.endswith(".experts") instead of rpartition is the smallest version.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this one: the handler only sees a bare experts name when the exported root module itself owns the fused experts. Every model export reaches it with a dotted path (model.layers.N.mlp.experts), and no caller exports an experts module as the root, so the tighter check would guard a case nothing reaches.

if ctx.keep_fused_experts and distribution_plan(ctx.model).is_fsdp_dense:
# FSDP2 owns the fused weights here: fold them inside a weight-update window, which hands
# FSDP2 the packed weights. Replaced behind its back, they would be overwritten with its
# original shards the next time the layer reshards, and exported unquantized. The window
# unshards FSDP2's params, so under FSDP2 alone each rank folds every expert of the unit, one
# unit at a time as a forward holds it; expert weights FSDP2 does not shard, as expert
# parallelism keeps them, stay sharded over the experts, and each rank folds its own.
with _fsdp2_weight_window(ctx, module):
Comment on lines +245 to +252

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] Which of the fold's two branches this path reaches is decided by code outside both: fsdp2_aware_weight_update calls root_module.unshard() on entry, so by the time _export_fused_experts_keep_fused reads getattr(module, proj).data the unit's FSDP2-managed params have been replaced with their unsharded form. Whether the fused expert weight is still a Shard(0) DTensor there (and the fold is shard-local, as its docstring describes) or a plain full tensor (and every rank folds every expert, via local_indices = list(range(n))) depends on whether FSDP2 kept it as a DTensor over the EP mesh — which is a torch-version-dependent 2D-parallel detail, not something this file states.

That matters because the shard-local fold is the PR's whole premise, and nothing pins it: test_fused_experts_fold_in_a_weight_window_when_the_plan_shards_the_dense_part monkeypatches both fsdp2_aware_weight_update and _export_fused_experts_keep_fused, so it only asserts the open / fold / close ordering, and the two-rank shard-local tests build the DTensor directly rather than going through this branch. Sibling test test_quant_linear_handlers_pack_an_fsdp2_unit_in_one_weight_window's own docstring says the linear path packs the unsharded weight, which suggests the plain-tensor branch is what this reaches under pure FSDP2.

Worth one assertion in the fold (or a sentence here) recording which branch the is_fsdp_dense path is expected to take, so a torch upgrade that flips it is a test failure rather than a silent return to all-experts-per-rank. The PR's own note that "GPU tests were not re-run on this revision" means CI will not catch it either.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the sentence at this branch: the window unshards FSDP2's params before the fold reads them, so under FSDP2 alone the fused experts arrive as the unit's whole unsharded weight and each rank folds every expert of the unit, one unit at a time as a forward holds it; expert weights FSDP2 does not shard, as expert parallelism keeps them, stay sharded over the experts and each rank folds its own. The fold's own comment no longer says an FSDP2 fused weight is always a sharded DTensor there. I did not add an assertion: both branches are correct for the weight they get, and which one runs follows from whether FSDP2 manages the expert weights, which the layout decides.

_export_fused_experts_keep_fused(module, ctx.dtype)
elif ctx.keep_fused_experts:
# No-gather distributed export: fold each rank's LOCAL experts and leave the weight
# fused + sharded; the per-expert split happens after consolidation.
_export_fused_experts_keep_fused(module, ctx.dtype)
else:
_export_fused_experts(module, ctx.dtype)


@ExportModuleRegistry.register(predicate=is_quantlinear)
Expand Down Expand Up @@ -286,9 +310,19 @@ def _export_quant_embedding(name: str, module: nn.Module, ctx: ExportContext) ->
def _export_bmm_experts(name: str, module: nn.Module, ctx: ExportContext) -> None:
"""Export fused BMM-style expert weights (Llama4 / GPT-OSS).
Its weight quantizer has one amax covering all experts, so under FSDP2 each rank can pack
just the experts it owns and produce identical bytes -- no gather or unshard needed.
Their weight quantizer has one amax covering all experts. The keep-fused export refuses them,
since they store their experts transposed.
"""
if ctx.keep_fused_experts:
# The keep-fused export leaves fused experts for the writer to split by their
# ``experts.<proj>`` key, as ``(num_experts, out_dim, in_dim)`` weights. These store theirs
# transposed (GPT-OSS also interleaves gate and up, and keeps biases), so the split would cut
# every expert wrong, quantized or not.
raise NotImplementedError(
f"{name}: the keep-fused export splits fused experts stored (num_experts, out_dim, "
f"in_dim), and {type(module).__name__} stores its experts transposed; export it through "
"the gather export instead."
)
if get_quantization_format(module) == QUANTIZATION_NONE:
return
# TODO: consolidate uncalibrated experts handling logic
Expand Down
Loading
Loading