Repository navigation
[OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel - #2746
shengliangxu wants to merge 27 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. |
6e6ac9c to
bad2de9
Compare
2142693 to
2e134ec
Compare
|
|
/ok to test 2e134ec |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## shengliangx/hfep-9b-ep-load-routing #2746 +/- ##
======================================================================
Coverage ? 61.91%
======================================================================
Files ? 655
Lines ? 73164
Branches ? 0
======================================================================
Hits ? 45301
Misses ? 27863
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:
|
2e134ec to
fdd217a
Compare
|
/ok to test fdd217a |
16e5c17 to
5c0d5fa
Compare
fdd217a to
0a3e056
Compare
5c0d5fa to
698f459
Compare
0a3e056 to
67f1c8f
Compare
|
/ok to test 67f1c8f |
698f459 to
eaa41c2
Compare
67f1c8f to
2f18815
Compare
|
/ok to test 2f18815 |
2f18815 to
d68e7ff
Compare
d68e7ff to
da76af0
Compare
The decision and the load imported apply_distributed_support and check_distributed_support inside the functions that call them, with no circular import or optional dependency to justify it: model_load_utils already imports from moe at module scope. The tests that stand in for the applier now patch it where the load looks it up. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The ways out of an MoE left whole on every rank offered FSDP2 for any unquantized causal LM, but both FSDP2 loads refuse a Nemotron-H checkpoint carrying MTP weights. Name that as well. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The GPU test of an expert-parallel load_pretrained covered Qwen3-MoE, whose config ships its own plan. It now also loads DeepSeek-V3 saved with no plan of its own, so the plan comes from its model spec, as on a transformers before 5.13. For both, each rank's batch runs through the sharded model and must give the whole model's logits, so the test checks the all-to-all forward as well as each rank's expert slices. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
load_and_prepare_ep put the config the model keeps back to the plan the checkpoint declares, which it inferred from the plan being an attribute of the config it was handed. Since the decision and the load apply the model spec, a plan the spec attached is such an attribute too, so the kept config held ModelOpt's plan and an export would write it into config.json: for Nemotron-H a style only ModelOpt registers, and so for DeepSeek-V3 below transformers 5.13 or with a plan pinned to null. The load now reads the declared plan from the checkpoint's config.json, where the expert-parallel config sits in it, and the kept config gets exactly that, or no plan of its own. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel load reads the plan the checkpoint declares from its config.json. For a handed config whose plan sits on a nested config, a config.json that is no JSON object made the nested lookup raise on that rank alone, outside the load's collective refusal, so the other ranks went on into the load and waited for it. A file a rank cannot read as a JSON object now declares no plan, and the read runs inside the expert-parallel load's refusal block, so anything else it raises reaches every rank. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A config.json the expert-parallel load cannot read, or that is no JSON object, declares no plan to keep, so the loaded model keeps none and an export writes none, whatever the checkpoint declared. That loss is now a warning instead of silence. The call-site comment says the read no longer raises on the file, and why it stays in the collective block. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Left whole on every rank, a model whose spec marks expert parallelism unsupported was refused with a RuntimeError, while the load refuses the same spec with check_distributed_support's NotImplementedError. Raise the same type on both, which is still a RuntimeError. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The warning for a config.json the load cannot read went through warn_rank_0, so a rank other than 0 failing alone dropped the plan from its model's config with no warning anywhere. Each rank now warns for itself and names its rank. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Below the release a model's expert parallelism needs, an MoE left whole on every rank was refused with "Upgrade transformers" and no release, though the spec's own refusal names it. Both now read the release from one place, moe._ep_floor: the expert-parallel load's own, or the spec's when that is later. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Below the release a model's expert parallelism needs, an MoE left whole on every rank was told to upgrade transformers to shard its experts, even when no EP degree divides both its experts and the world size, or its config names no count: there an upgrade shards nothing, and the next run meets the refusal that names the real obstacle. Offer the upgrade only where a degree exists, and let the other cases name what stands in the way. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Below the release a model's expert parallelism needs, the left-whole refusal tells the caller to upgrade transformers to shard the experts. With the EP degree set to 1 by the caller, an upgrade alone leaves it at 1, and the next run meets the refusal again. Name raising the degree as well there, with the knob the caller set it by. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The left-whole refusal test checked for a ValueError by the word "divid" in the expected pattern, which the pattern for an EP degree to raise beside an upgrade contains too, though that refusal stays a RuntimeError. Check by the pattern of the decision's own refusal. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With no ranks to shard over, the plan holds the whole model whatever the model is, but the EP degree argument was still parsed with the model type, which consults the model's spec and its transformers floor for a result that cannot change. Parse it without. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
resolve_distribution_plan applied the model's spec whenever the world and the EP degree left room for expert parallelism, whatever class the load builds the model with. A class the sharded loads refuse is planned whole on every rank, or refused, so it is never expert-parallel, yet every rank ran the spec's prepare first: a prepare that raises failed a load that loaded the whole model on every rank before, and one that patches the process did so for a load that never uses it. Skip the spec for such a class, as the decision does. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With an expert-parallel plan handed in, the expert-parallel load prepares its copy of a config= passed for the load once its own checks pass, the rank's-own-GPU refusal among them, so a rank that cannot load runs no spec prepare, which can import the checkpoint's code. A row of test_a_sharded_load_refuses_what_it_cannot_honor holds it: its spec applier fails if a rank without a GPU prepares anything. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel load keeps on the loaded model's config the plan the checkpoint's config.json declares, read from the file. A plan set on a config passed for the load, and not declared there, is not kept, so an export writes out what the checkpoint declares; the docstring said only the spec's and the config class's plans were left out. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
check_distributed_support and the refusal of an MoE left whole on every rank both offered FSDP2 with the same caveat, what the FSDP2 load takes, in two wordings that each round had to keep in step. Both now take it from one helper in moe, with the knob spelled as the caller set it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…allel The plan decision applies the model's spec, prepare included, before it knows the plan, wherever the plan could still be expert-parallel, so a run whose auto EP degree comes out at 1 has run prepare too. That is now the documented contract of EPSpec.prepare: it must leave a whole-model or FSDP2 load as it would be without it, and may run more than once. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_sharded_load_kwargs took an hf_config as prepared by the model's spec unless told otherwise, so a caller that left prepared out would skip the spec for a config that needs it: DeepSeek-V3 below transformers 5.13 would load sharding nothing. Left out, it is now taken as not prepared; preparing again is harmless by EPSpec.prepare's contract, skipping it is not. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The plan decision now gives an EP degree it chose way to FSDP2 for the whole model when the dense part is FSDP2-sharded, so the routing test's case for the refusal of an expert-parallel plan that FSDP2-shards the dense part asks for the EP degree, as a caller meeting that refusal has. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The plan decision's refusal of an MoE its spec rules out read the spec's unsupported text to decide, then left it out, so a run that set nothing, the one most users reach as auto resolves to 1 for these models, learned only that the model cannot be expert-parallel. It now says why, as the load's own refusal of the same spec does. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With the model spec applied, an EP degree left to auto resolves to 1 for an MoE below the transformers floor its spec states, and the plan decision refuses to leave it whole, naming the upgrade that would shard it, before the load's give-way could plan it whole. The give-way test's below-the-floor case expects that refusal here. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The plan decision asks the model spec (_spec_refuses_ep) before the expert-parallel load's refusal: auto resolves to an EP degree of 1 for a model whose spec refuses expert parallelism or needs a later transformers, and the left-whole refusal says why. The load refusal's own spec and floor checks could no longer change a plan, so they are gone, with the floor they spelled out beside _ep_floor's. The give-way test's model-spec case patched a spec lookup the decision no longer reads; GPT-OSS's left-whole case covers that refusal, now with the MXFP4 checkpoint format its releases have, which the load refuses as well. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
and floor checks this PR leaves to the spec-aware decision. A plan handed to the load reaches it without that decision, so the load asks the spec itself, as load_pretrained's expert-parallel route does, and refuses with the same NotImplementedError or RuntimeError. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
transformers writes config.json as UTF-8. Read in the locale's encoding, a non-ASCII byte under a non-UTF-8 locale raised UnicodeDecodeError, which the plan-provenance read took for an unreadable file and dropped the plan the checkpoint declares. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The load asked the spec only past its format check and its no-plan guard, so a direct call for a model whose plan only its spec supplies, or whose MXFP4 checkpoint the load refuses as well (GPT-OSS's), reported those instead of the spec's reason, whose remedy is the one that loads. It now asks the spec as soon as it has the config, as load_pretrained does. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test d8059b3 |
|
/claude review |
| "spec", | ||
| lambda: ( | ||
| world_size > 1 | ||
| and auto_class in (None, AutoModelForCausalLM) |
There was a problem hiding this comment.
[SUGGESTION] The gate rules the spec out for an auto_class the sharded loads refuse, but not for _init_quantized_weights(), which _decide_distribution treats identically one line apart:
if (
(auto_class not in (None, AutoModelForCausalLM) or _init_quantized_weights())
and ep_size in ("auto", 1)
and dense in ("auto", "replicate")
):
# The sharded loads refuse this class and init_quantized_weights: load it whole, unbuilt.
return DistributionPlan(world_size)So on a multi-rank run with init_quantized_weights active and the EP degree left to auto, the gate is true (auto_class is None, _ep_degree(...) is "auto"), every rank applies the spec — prepare included — and the decision then returns the whole-model plan anyway. The comment just above states the rationale for excluding such a route: "the decision then plans the whole model on every rank, or refuses, without the spec, whose prepare could fail, or patch the process, for a load that never shards." That reasoning covers _init_quantized_weights() too, and today the cost is a config deepcopy plus an extra collective on a path that cannot shard; once #2748's specs set prepare, it becomes a process-level patch applied for a load that never uses it.
No spec in this PR sets prepare, and the returned config is still the unprepared one for a non-EP plan, so nothing is wrong at this head — but the gate reads as if it enumerates "expert parallelism is ruled out" exhaustively and misses one case the decision itself handles. Suggest making the two agree:
lambda: (
world_size > 1
and auto_class in (None, AutoModelForCausalLM)
and not _init_quantized_weights()
and _ep_degree(ep_size, hf_config.model_type) != 1
),If instead the asymmetry is deliberate — init_quantized_weights only forces the whole-model plan while dense resolves to "auto"/"replicate", whereas the auto_class refusal is unconditional — a half-line in the comment saying so would stop the next reader re-deriving it.
There was a problem hiding this comment.
The asymmetry is deliberate, and the suggested gate would regress the one case it leaves out.
A foreign auto_class rules the spec out whatever the knobs, because the decision then either loads the model whole or refuses the class, without building it. init_quantized_weights forces the whole-model plan only while nothing asks to shard.
With MODELOPT_EP_DEGREE=2 (or MODELOPT_FSDP_SIZE=world) inside the context, the decision still builds the model. For an MoE whose plan only its spec supplies (DeepSeek-V3 before transformers 5.13), it can find the experts only through the spec.
I checked this on CPU: a 2-rank stand-in of that model, MODELOPT_EP_DEGREE=2, inside init_quantized_weights(mtq.INT8_DEFAULT_CFG).
- With the gate as it is, the load refuses with the in-context message:
The expert-parallel load builds the model without from_pretrained, so it would skip init_quantized_weights' quantization; .... - With
not _init_quantized_weights()added, the decision raisesMODELOPT_EP_DEGREE=2 must divide the routed experts deepseek_v3 shards (0).instead.
You're right that with the EP degree left to auto, the spec is applied for a load that ends whole. That costs a config copy, one collective, and prepare. The EPSpec.prepare contract already requires prepare to be safe for a load that ends dense, and an auto EP degree that gives way runs it the same way.
Ruling the spec out inside the context only where the decision itself goes whole would avoid it: the EP degree auto, and the dense layout auto or replicate. I can make that change, with the half-line of comment you suggest, if you think it's worth it here.
There was a problem hiding this comment.
Approving: the spec now reaches the decision and the expert-parallel load on every rank through collectives, and the one new commit's reordering is correct.
No action needed:
- ✔️ Prior rounds' concerns stay closed:
_declared_ep_planstill reads the checkpoint'sconfig.json(so a spec-attached or class-default plan cannot reach an export), its read sits inside the EP load's collective refusal block, and_ep_floor/_fsdp2_way_outremain single-sourced inmoe.py. - The new commit's
check_distributed_supportinload_and_prepare_epsits inside the collective checks block, ahead of the format check and the no-plan guard, so a spec-refused MoE reports its own reason instead of "declare base_model_ep_plan". Dropping those checks from_ep_load_refusalis safe:_ep_degreereturns 1 for a spec-refusing model, so auto never needs the give-way, and the new left-whole arms raise with matching types. - One non-blocking SUGGESTION inline (
model_load_utils.py:1903): the spec gate excludes a refusedauto_classbut not_init_quantized_weights(). - Baseline:
.agents/review-guidelines/code-review.mdat basec4cc146. Reviewed headd8059b3; full scope, all 6 changed files opened (+236 added source lines, under budget). No tests or CI run by me; no injection attempts in the diff.
Note
Slice [3/3] of this PR's split (2026-10-30), so the model-spec work is reviewed and merged in pieces under the 500-line budget:
EPSpecand every model's data);apply_distributed_supportand its checks);Base:
shengliangx/hfep-base-9-specs, a branch (not a PR) that joins #2780 and #3135, the two PRs this one builds on. The diff below is this PR's alone. It merges after #2780 and #3135, then sits onmain.Re-cut: split from the former whole PR (head 2489583, pinned). Also adapted to #2779's collective load checks; see the end of Testing.
Series layout
mainrequire_transformers, and a VLM built without its LM headhfep-base-6a-ep-loader([OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 + [OMNIML-6073] Quantize Nemotron-H's experts under grouped_mm, with their wrapper in its model folder #3067 + [OMNIML-6071] Calibrate a distributed model on per-rank data #2740 + [OMNIML-6060] [1/3] Add require_transformers, and build a VLM without its LM head #3131)hfep-base-9-specs([OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 + [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135)hfep-base-11b-kimi([OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel #2746 + [OMNIML-6063] [3/3] Load a checkpoint its model spec decompresses through the expert-parallel loader #2747 + their lower slices) → [OMNIML-6078] [2/2] Add the Kimi-K2.5 expert-parallel adapter #2748, [OMNIML-6079] [2/2] Add the Kimi-K3 expert-parallel adapter #2749hfep-base-14-hf-ptq([OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel #2746 + [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742 + [OMNIML-6084] Export an expert-parallel model through the no-gather writer #2753) → [OMNIML-6085] [3/3] Run expert-parallel PTQ from hf_ptq.py #2754What does this PR do?
Type of change: New feature
The plan decision and the expert-parallel load apply the model's spec (#3134), through the applier (#3135). Without them, an MoE that only its spec makes expert-parallel, such as DeepSeek-V3 before transformers 5.13, is decided as dense.
resolve_distribution_planapplies the model's spec to a copy of the config before rank 0 builds the model on meta._spec_refuses_ep).MODELOPT_FSDP_SIZE=worldalone plans FSDP2 for GPT-OSS, and for Qwen3-MoE on transformers 5.5.prepareattaches), without applying anything.check_distributed_support).config=passed for the load with a plan handed in is the caller's own, so an expert-parallel load prepares a copy of it, once.prepareruns before deciding here, and where it goes in [OMNIML-6078] [2/2] Add the Kimi-K2.5 expert-parallel adapter #2748. The decision builds the model on the meta device and finds and counts its routed experts through the plan. For a model whose plan only its spec supplies, the spec must therefore be applied first. This PR applies the whole spec there,prepareincluded, and documents the interim contract onEPSpec.prepare: it must leave a whole-model or FSDP2 load as it would be without it, and may run more than once. No spec in this PR setsprepare.EPSpec.make_importable) and attaches the spec's stated plan (prepared_ep_plan) with nopreparerun.preparethen runs only once the plan is decided expert-parallel: in a collective step after the decision, or in the load after its own checks for a plan handed in.preparewheremodels/hf.py'sprepare_for_loadinghooks already run: on the route that loads the model it prepares, never on one that does not. The hooks are scoped to the load as a context;prepareonly runs on that route.Usage
Testing
tests/unit/torch/utils/test_model_load_utils.py(the decision's and the loads' tests) gains the spec's cases:MODELOPT_EP_DEGREE=1;ep_size=2meets the refusal;config=with a handed plan; GPT-OSS is refused with its config handed in, without preparing it.Mutation-checked (earlier rounds): skipping the spec or applying it without expert parallelism, ignoring the EP knob, letting each rank decide the spec for itself, preparing a handed config twice or not at all, ignoring the spec's plan at an EP degree of 1, letting auto pick an EP degree for a model whose spec refuses expert parallelism or below a floor, and offering an EP degree below the floor each fail a test.
Checked locally, at the stack's tip: DeepSeek-V3, Nemotron-H and Nemotron-H-Omni configs prepared by their specs and swapped to the
torch_a2astyles are written by the expert-parallel export with nobase_model_ep_plan, as their sources have none. There the export writer itself puts back the sourceconfig.json's values ([OMNIML-6084] Export an expert-parallel model through the no-gather writer #2753). The loaded model's own config holds no spec plan only since b51123a (below).On GPUs (1 node, EP4, an earlier revision): gpt-oss-20b under
torchrunstopped at the plan decision, before any weights load.2026-10-30, split and re-parented:
prepareitem is [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135's, the declined version-claims item [OMNIML-6076] [1/3] Describe expert parallelism in the model specs #3134's).test_load_pretrained_distribution.pymoved intotest_model_load_utils.py, where [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 moved the load tests.Pre-commit, mypy included, passes against main. CPU tests (
test_model_load_utils.py,test_ep_loader.py,test_moe_a2a.py,tests/unit/torch/models; torch 2.14):hfep-base-9-specs([OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 1bec6da + [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 fe16583); no conflicts, this PR's own diff unchanged. Pre-commit passes against main;test_model_load_utils.py,test_ep_loader.py,test_moe_a2a.pyandtests/unit/torch/modelspass on transformers 5.18 (467) and 5.5.4 (411, 56 skipped).moeinstead._EP_MIN_TRANSFORMERScomes in with the module-scope import frommoe.test_an_expert_parallel_moe_loads_with_its_experts_shardedintests/gpu/torch/utils/test_model_load_utils.pyruns for two models.its-own-planis Qwen3-MoE.its-spec-planis DeepSeek-V3 saved with no plan of its own, so the plan comes only from its spec. Both run the realload_pretrainedwith the plan left to decide, and check each rank's expert slices and its own batch's logits against the whole model. The test is gated on 2 GPUs and transformers >= 5.7.DistributionPlan(2, ep_size=2)with the spec's plan. The whole model builds and runs.test_model_load_utils.py,test_ep_loader.py,test_moe_a2a.pyandtests/unit/torch/modelspass on transformers 5.18 (467) and 5.5.4 (411, 56 skipped).load_pretrainedtests intests/gpu/torch/utils/test_model_load_utils.pypassed: both expert-parallel rows (its-spec-plan,its-own-plan) and the two dense tests. NVIDIA GB200, torch 2.11.0a0, transformers 5.9.0, reruns disabled:Review round, 2026-10-30 (b51123a): the expert-parallel load put a plan the model spec attached back on the loaded model's config, where an export would write it into
config.json. That affected Nemotron-H, Nemotron-H-Omni, DeepSeek-V3 below transformers 5.13, and anyconfig.jsonpinning the plan tonull. The load now reads the declared plan from the checkpoint'sconfig.json. The kept config gets exactly that, or no plan of its own.test_the_model_keeps_the_plan_its_checkpoint_declares(test_ep_loader.py) is parametrized over a plan declared by the checkpoint, a class default, and one attached by the spec. It asserts onvars(model.config)and on the writtenconfig.json. The spec-attached case fails without the fix.test_model_load_utils.py,test_ep_loader.py,test_moe_a2a.pyandtests/unit/torch/modelspass on transformers 5.18 (468) and 5.5.4 (412, 56 skipped).model.tp_plan, copied at build.Review round, 2026-10-30 (fa2d2f5): a
config.jsonthat one rank cannot read as a JSON object now declares no plan to keep, instead of raising in the nested lookup on that rank alone. The read of the declared plan runs inside the expert-parallel load's collective refusal block.test_a_load_failing_on_one_rank_fails_every_rankgainsunreadable-plan-provenance, on two gloo ranks: a handed config with the plan ontext_config, and rank 1 alone reading aconfig.jsonthat parses to[]. Both ranks reach the load, stood in by a bounded barrier. The previous code, and the move without the type check, each fail it in under 20 s.test_model_load_utils.py,test_ep_loader.py,test_moe_a2a.pyandtests/unit/torch/modelspass on transformers 5.18 (469) and 5.5.4 (412, 57 skipped).Review round, 2026-10-30 (2205949):
config.jsonthe load cannot read as a JSON object still declares no plan to keep, but now with a warning, since an export then writes none (3f1b031). A row intest_the_model_keeps_the_plan_its_checkpoint_declaresasserts the warning.NotImplementedError, as the load side does (2205949). It is still aRuntimeError, andtest_an_ep_capable_moe_left_whole_on_every_rank_is_an_errorasserts the type.Rebased on 2026-10-30 (d7e5fde) onto the rebuilt
hfep-base-9-specs(52b6cf9 = [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 d78aaf8 + [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 da3aeca), after [OMNIML-6060] [1/3] Add require_transformers, and build a VLM without its LM head #3131 merged to main (62a5c48). What moved underneath conflicted in 7 hunks (3 source, 4 test): [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779's newer round under [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136, and [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136's two latest commits. Each is resolved by applying this PR's change to the new text:_ep_degreekeeps [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779's whitespace strip. Itsmodel_typeis now optional, because [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 reads the knob through it before any config is read (a launch without a process group), where auto stays auto.ep_askedstrips the knob too, so every reading of_EP_AUTOagrees.load_pretrained's docstring says aconfigpath is prepared too.test_model_load_utils.py,test_ep_loader.py,test_moe_a2a.pyandtests/unit/torch/modelspass on transformers 5.18 (532) and 5.5.4 (462, 59 skipped).Review round, 2026-10-30 (ffb58cf), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 50e64f0 (7ca9934):
config.jsona rank cannot read is per rank and names the rank (89f8845). Theunreadable-plan-provenancerow asserts rank 1 alone warns.moe._ep_floor, whichcheck_distributed_supportnow uses too (ffb58cf)._NEEDS_LATERasserts it.Review round, 2026-10-30 (bfd26e8), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 3c353af (7146e4f):
test_an_ep_capable_moe_left_whole_on_every_rank_is_an_errorgainsbelow_the_ep_floor_no_degree, which fails without the change.Review round, 2026-10-30 (7041b61), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 b1f5d8c (ef0cb85):
{knob},{replicate}and the FSDP2 setting as the caller set them. The restack resolved the conflicts that way, along with [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780's new test row.below_the_ep_floor_explicit_degree_onerow fails without it.Review round, 2026-10-30 (2f90895), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 4a1e020 (ce39f86):
_sharded_load_kwargs(..., prepared=False)) rather than reading it again. This still happens after its option, MTP and GPU checks.test_a_load_prepares_its_config_once[planned]asserts one read.ValueErrorwith [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780. The unsupported arm keepsNotImplementedErrorand the below-the-floor armRuntimeError, and the test asserts each type.Review round, 2026-10-30 (6342161), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 4040ea3 (2a26182):
test_a_single_process_holds_the_whole_modelasserts it.freezedefault beside this PR's spec step in the expert-parallel branch.Restack and review round, 2026-10-30 (03f7985), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 8f9c8d9 (f306d9f):
preparethat raised failed a load that used to work.test_a_model_class_of_the_callers_own_loads_wholenow makes the spec applier fail if it is called. All three of its cases fail without the change, on transformers 5.18 and 5.5.4._EP_MIN_TRANSFORMERSto that module-scope import directly, and the commit that moved it there is dropped. The other restack conflicts were in import lists and in the one-rank-failure test, which now keeps both [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136'sno-gpurow and this PR's plan-provenance row.Review round, 2026-10-30 (d3a058e), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 d466147 (c312093). The restack had no conflicts, and the blocker fix above is now efe745e.
config=, the expert-parallel load now prepares its copy of that config after its own checks. This puts the spec step after the rank's-own-GPU refusal and runs it once (c6fd67f). A newno_gpu_config_passedrow fails without the change._declared_ep_plannow says that a plan set on a config passed to the load is not kept either (681fbc3).moe._fsdp2_way_out, and bothcheck_distributed_supportand the left-whole refusal use it (d3a058e).Restack round, 2026-10-30 (0eadf4c), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 b692242 and [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 85885c3 (9992be8), on main 059588b:
config=passed for a sharded load before reading the model type. The first commit therefore takes the final design directly: the expert-parallel load prepares that copy once its own checks pass. The commit that previously made that change now holds only its test row.preparemay run for a plan that ends up not expert-parallel.EPSpec.preparenow documents this as its contract (0eadf4c).Review round, 2026-10-30 (4ed85b3), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 1692141 (5aed81c):
_sharded_load_kwargsnow treats anhf_configpassed withoutpreparedas not prepared, so the load applies the spec to it (6cec3dd).Restack round, 2026-10-30 (9267a0b), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 093c652 and [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 8d2bd3c (8c36e1e), on main d464d30:
_ep_degreereads the knob unstripped, as [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779's environment parsing now strips it.Review round, 2026-10-31 (77c4117), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 1960174 and [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 41b8a6a (e887676):
Restacked on 2026-10-31 (c39129d) on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 fac2c56 and [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 7c0be5b (3abc574). [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136 moved
test_ep_loader.pyintotest_model_load_utils.py, so this PR's four commits that changed that file now make the same changes in the moved section; their imports go into that file's import block. Its own diff is otherwise unchanged. Pre-commit passes against main. The same CPU tests pass on transformers 5.18 (573) and 5.5.4 (486, 76 skipped).Review round, 2026-10-31 (c7619f8), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 3fc02b8 and [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 7c0be5b (604c3f5):
_ep_load_refusalno longer checks the spec and the transformers floor, which the decision asks the spec about first, and the give-way test drops the case that patched a spec lookup the decision no longer reads (c7619f8). GPT-OSS's left-whole case now has the MXFP4 format, and it fails without the spec clause.Review round, 2026-10-31 (3b10775), on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 3c40b57 and [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 7c0be5b (c76a6b8):
check_distributed_supportitself, asload_pretrained's expert-parallel route does. A plan handed to it is refused for a model its spec refuses, now that_ep_load_refusalleaves the spec to the decision (85ff59b). [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780's GPT-OSS case in the load test expects thatNotImplementedErrorhere, and fails without the call.config.jsonis read as UTF-8 (3b10775). Checked by hand underLC_ALL=Cwith a non-ASCII field; there is no unit test for it.Restacked on 2026-10-31 (d8059b3) on the helper rebuilt from [OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 c2d18e6 and [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 2b3dfc6 (c4cc146), on main e352cac:
confignote in theload_pretraineddocstring moved to the loading guide's Configs section, as [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 shortened the docstring.Before your PR is "Ready for review"
CONTRIBUTING.md: N/AAdditional Information
Part of expert-parallel PTQ for Hugging Face MoE models; merges after #2780 and #3135. The former whole PR's history (Llama-4 and MiniMax-M3 adapters added and then removed, review rounds) is pinned with its last head; the slices carry the net change.