Repository navigation
[OMNIML-6081] [2/2] Fold fused experts' quantizers into their weights shard-locally #2750
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ef4d4b9
34d5415
f774bed
cc0384c
d69333e
4d2d40a
87a6386
ec602ef
f8657d5
eafde05
703f299
158cc7f
8502776
8492e0c
73570e5
390346a
d62f873
89504e4
efca07b
64b9c94
faa81c6
044460a
833d957
6c93a25
455fa15
aaf9c1e
da2c12f
3ed0377
b0e1038
62a0893
71b2564
906c355
eebbcbe
00804c5
d242e3c
d8f9a0d
9b06335
60c3d81
912e953
0a40404
b8be313
daed636
2982230
136c766
ebfb6a7
e95c664
d341421
3a86980
54f8632
a322d41
51f187f
0759477
af4c213
889d630
8dd7169
34b4f7c
2da1b3f
d0cef07
5dcb830
8674b73
c186322
8d1caf3
9f30d03
3c7c5d9
a48ce7a
cdd8d7c
26e9863
d568dac
6033bb8
fa69b30
ec2741c
46742ff
6f1d1da
830d704
fc6d998
945271d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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}." | ||
| ) | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: That matters because the shard-local fold is the PR's whole premise, and nothing pins it: Worth one assertion in the fold (or a sentence here) recording which branch the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
@@ -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 | ||
|
|
||
There was a problem hiding this comment.
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 formodel.layers.0.mlp.expertsand for a bare top-levelname == "experts"(rpartition on a dotless string returns("", "", "experts")), while_fused_experts_prefixesrequires a leading dot:key.endswith(f".experts.{proj}")(moe_utils.py:646). A root-levelexpertsattribute 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 namehere too, or relax_fused_experts_prefixestokey.endswith(f"experts.{proj}")with a.-or-start check. Matching onname.endswith(".experts")instead ofrpartitionis the smallest version.There was a problem hiding this comment.
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
expertsname 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.