Skip to content

[OMNIML-6054] Decide how a model is distributed, from the model built on the meta device - #2737

Merged
shengliangxu merged 44 commits into
mainfrom
shengliangx/hfep-2-plan-decision
Oct 29, 2026
Merged

shengliangxu merged 44 commits into
mainfrom
shengliangx/hfep-2-plan-decision

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Note

The expert-parallel PTQ series is now a tree instead of one stack (re-cut 2026-10-27, from main 1521053), so PRs that do not depend on each other can be reviewed and merged in parallel. This PR was [5/21]. Rebased onto main 7c3c868 later the same day, after #2739 merged, and onto main f03651e that night.

Base: main, now that #2736 has merged (2026-10-28). The diff below is this PR's alone.

Re-cut: this PR's diff is unchanged; only its base moved. Review fixes since are listed at the end of Testing.

Series layout

What does this PR do?

Type of change: New feature

resolve_distribution_plan(ckpt_path) decides how a run spreads a model over its ranks. It returns a DistributionPlan (#2736) for every model, MoE or dense, and for a single process too. It is the one place a run decides this: #2779 loads the model by the plan and records the plan on it, and every later step reads it from there.

  • From the model, not estimates. Rank 0 builds the model on the meta device, using the class that loads it whole: a causal LM, or for a VLM the class that carries its LM head. A trusted remote VLM whose auto_map registers AutoModelForImageTextToText builds through that class, with its LM head. It decides from the model's parameters and then discards it, and leaves the auto class it built through as it was: building a remote-code model through an auto class registers its class there, as it is, and rank 0's later load through that class would then build it without the GenerationMixin every other rank's first load adds. Rank 0's Kimi-K3 lacked generate while the other ranks' had it, and an expert-parallel preview deadlocked.
  • EP degree (ep_size, or MODELOPT_EP_DEGREE).
  • Dense layout (dense, or MODELOPT_FSDP_SIZE, which takes auto, 1 to replicate, or world or the world size to shard).
    • auto FSDP2-shards the dense part when a whole copy of it plus the rank's share of the routed experts would take more than 60% of one GPU's memory.
    • Sharding over part of the world is refused.
  • Every rank reads the checkpoint first. Each rank makes the checkpoint local and reads its config inside the failure-carrying all_reduce ([OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736), before the plan's broadcast. A rank that cannot see the checkpoint fails every rank with its own error, instead of leaving rank 0 waiting in the broadcast.
  • The checkpoint the load reads. resolve_distribution_plan takes revision, token, cache_dir, local_files_only and subfolder as load_pretrained does, so the plan and its config are for the checkpoint the load reads.
  • Rank 0 decides, and every rank gets its plan (broadcast_from, [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736). The inputs are read locally: the knobs, this GPU's memory, the files on this node. Ranks deciding for themselves could disagree, and ranks loading different plans hang. If rank 0 fails, every rank raises.
  • A single process decides without building anything, and reads no knob: a shell still exporting a multi-GPU run's MODELOPT_EP_DEGREE or MODELOPT_FSDP_SIZE must not reshape it. It holds the whole model, unless the caller's own dense="fsdp" asks for FSDP2 on one rank.

Measuring the meta model avoids the gaps of estimating from config fields, which this PR's earlier revision did: it ignored GQA and tied embeddings, assumed bf16, and treated a config missing the fields it read as one that fits a GPU.

Why not hf_ptq's get_model, or Accelerate's sizing. The decision builds through the library's own meta build, build_meta_causal_lm, which main's FSDP2 load uses too. get_model lives in the hf_ptq example, which the library cannot import, and is a single-process device_map loader: it requires architectures and takes its class from the first entry, beside the example's own speculative, pack-quantized and MXFP4 branches. Accelerate's infer_auto_device_map and compute_module_sizes say which device holds each module of such a load. The plan needs one number instead: what one rank holds when expert parallelism splits the routed experts 1/ep_size and keeps the rest whole, a single sum over the meta model's parameters (re-tied first, as init_empty_weights unties them).

The class that builds a VLM with its head takes the checkpoint's remote class only under trust_remote_code, a MODELOPT_EP_DEGREE that is not a number is refused by name, and a single process parses its own ep_size as a distributed run does, refusing a bad one the same way.

Usage

# torchrun --nproc_per_node 8 ...
from modelopt.torch.utils.plugins.model_load_utils import resolve_distribution_plan

plan, hf_config = resolve_distribution_plan("Qwen/Qwen3-30B-A3B")
# DistributionPlan(world_size=8, ep_size=8, dense='replicate')

Testing

  • Tests in tests/unit/torch/utils/test_model_load_utils.py (the decision) and tests/unit/torch/utils/test_moe_a2a.py (the MoE helpers), the files of the modules they cover. They run on CPU, build tiny real configs on the meta device, and answer rank and size as torchrun would. They cover:
    • the knobs, read at call time; expert counting; EP-degree selection;
    • routed experts found under any model prefix, named by their style or as their grouped-GEMM parameters' parent;
    • the class that builds a whole model, a VLM's with its LM head;
    • the decision:
      • a single process, whatever knobs its shell exports; an EP-capable MoE; an MoE without an EP plan;
      • the dense fit from exact parameter bytes, counting what expert parallelism leaves whole and the rank's share of the routed experts, so an expert-dominated MoE shards its dense part;
      • knob and argument overrides;
      • refused partial sharding and indivisible EP degrees;
      • a VLM decided from its whole model;
      • a model that cannot be built on meta, named in the error;
      • the meta build leaving the auto class's registrations as it found them (fails without the fix);
    • on two real gloo ranks (cpu_dist_workers): both take rank 0's plan, though rank 1 exports a different dense layout, and rank 1 decides nothing; a rank that cannot see the checkpoint fails both ranks with its error, before the broadcast. Rank 0's own failures are broadcast_from's, tested in [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736.
  • On 2 GPUs with transformers 5.18, a tiny Kimi-K3 at EP 2 with previews on: before this fix rank 0's model alone had no generate, so rank 0 moved on to calibration while rank 1 decoded in lockstep, and the run deadlocked (TORCH_DISTRIBUTED_DEBUG=DETAIL: an all-reduce on one rank against an all-to-all on the other); with it, both ranks preview, calibrate and export.
  • Mutation-checked: 30 mutants, each failing a test; and, since, a rank deciding for itself, the plan sent from the wrong rank, a single process reading the knobs, the checkpoint read outside the all_reduce, the experts' share left out of the dense fit, the Hub options not passed on, a single process's ep_size not parsed, and a trusted remote VLM's registered image-text class not taken.
  • Near-duplicate tests are merged into one test per behavior, and the env-knob, ep * dp == world and no-EP-plan tests, which the decision tests cover, are dropped. Each drop is checked by mutation.
  • The auto-class table has an untrusted remote VLM and a trusted remote image-text VLM, and a non-numeric MODELOPT_EP_DEGREE names the knob, as a bad ep_size in a single process names the argument.
  • The Hub options locate the checkpoint against an offline Hugging Face cache with two revisions and a subfolder.
  • Locally on this revision, the stack rebased on main c6fa613: pre-commit over the whole PR against main, at every PR of the stack; and at the stack's tip the full tests/unit on transformers 5.18 and 5.5 and on torch 2.8, and the hf_ptq example tests, each with the same failures as main in that environment. CI runs the full unit suite.
  • Re-cut on 2026-10-27 (see the note at the top): pre-commit, mypy included, passes at this head over the whole change against main, and the series' PRs merged together give a tree byte-identical to the former stack's tip. This PR's own unit tests pass at this head on CPU (torch 2.14, transformers 5.18); CI runs the full unit suite.
  • Review fixes, 2026-10-27: an encoder-decoder model builds through its seq2seq class; MODELOPT_EP_DEGREE reads auto in any case; a hybrid plan names its routed experts by both styles; the expert count is read from the config holding the plan; a docstring note on a rank 0 without a GPU. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.
  • Rebased on 2026-10-27 onto main 7c3c868, after [OMNIML-6064] Load each rank's checkpoint shard straight into a meta-built model #2739 merged: pre-commit, mypy included, and this PR's own unit tests pass at this head, and the series' PRs merged together add no unit-test failure over main (tests/unit/torch utils, export, quantization and models, on transformers 5.18 and 5.5.4 and on torch 2.8).
  • Review suggestions, 2026-10-27 evening: the refusal of an EP degree that cannot shard the experts names the knob (MODELOPT_EP_DEGREE) or the argument the degree came from.
  • Review fixes, 2026-10-27 night: the meta build leaves AutoModel's and AutoModelForCausalLM's registrations as it found them; a routed-expert config whose expert-parallel plan names no module of the model is refused unless the degree is 1; an explicit degree that does not divide the world names the knob or argument it came from. Each new test fails without its change.
  • Review suggestions, 2026-10-27, last round: the routed-expert match computes each module name's wildcard form (every layer number replaced) once instead of once per key, with the same matches; build_meta_causal_lm's docstring states the registrations its restore assumes, and resolve_distribution_plan's that a caller passes the local checkpoint directory.
  • Review fix, 2026-10-27, last round: a VLM-shaped config that AutoModelForCausalLM recognizes (Phi-4-multimodal, the only such config in transformers 5.5.4 or 5.18) builds through it, head included, rather than through AutoModel. The new table row fails without the change.
  • Review suggestion, 2026-10-27, last round: resolve_distribution_plan's docstring says each rank returns its own copy of the config, rank 0's deciding the plan.
  • Review suggestion, 2026-10-28: _choose_ep_degree's docstring says why the automatic EP degree is the largest: it holds the fewest experts per rank, leaving the most memory for activations, and every degree above 1 runs the same two all-to-alls per MoE layer.
  • Declined review findings, 2026-10-27:
    • The plan decision builds the model on meta even when ep_size=1 and dense are both given. Rank 0 takes 0.54-0.65 s and 8-208 MB of host memory on 232B-1T configs, the FSDP2 load builds the same meta model anyway, and a model that cannot be built on meta loads with distribution=, which skips the decision.
    • _auto_class falls back to AutoModel for a remote VLM whose AutoModelForCausalLM class is named *ForCausalLM. None of the remote checkpoints the series supports is one: Kimi-VL, Kimi-K2.5, Kimi-K3 and Step-3.7-Flash register *ForConditionalGeneration, and DeepSeek-VL2 registers no auto class. Qwen3-Omni, which AutoModel cannot build, is not supported on main.
  • Review suggestion, 2026-10-28, second round: the plan decision's MoE helpers and the three auto classes are imported at module top, nothing having deferred them.
  • Review suggestion, 2026-10-28, fifth round: modelopt.torch.utils.distributed is imported at module top as dist; [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 drops its four function-local imports of it.
  • Rebased on 2026-10-28, late night, onto [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736 at cf4597dcbeb, on main 1ca8bd1fc4d (after [OMNIML-6073] [1/3] Gather only FSDP2's own parameters in its weight-access window #3088 and [OMNIML-6073] [2/3] Quantize fused experts under grouped_mm; leave transposed layouts unquantized #2745 merged). The one conflict was with main's [OMNIML-5987] [3/3] Support Qwen text-only FSDP2 AutoQuant loading and export #2676: build_meta_causal_lm keeps this PR's auto_class parameter, now defaulting to None, so a call without one still gets main's text-only-VLM factory. The plan decision passes _auto_class(...) as before, and the registry restore covers whichever class builds. This PR's tests and test_model_load_utils.py pass on transformers 5.18 (90) and 5.5.4, and pre-commit passes against main.
  • Rebased on 2026-10-28, late night, onto main bdc45368d10 after [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736 merged; [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736's squash has the tree of its last head, so this PR's tree is unchanged.
  • Review fixes, 2026-10-29: the plan decision re-ties the meta model before sizing it, so a tied embedding and LM head count once (init_empty_weights unties them); the dense-fit test is sized from a normally built model, tied and untied, on either side of the 60% share. An encoder-decoder config that names no architecture (a bare T5Config or WhisperConfig) builds through its seq2seq class, checked through the public resolver too. Each fails its test without the change (mutation-checked). The routed-expert match's local transformers import names its reason (the optional [hf] extra). Pre-commit passes against main; this PR's tests, test_model_load_utils.py and test_moe_a2a.py pass on CPU with transformers 5.18 and 5.5.4.
  • Review fix, 2026-10-29: the tests moved into the files of the modules they cover (CONTRIBUTING.md): the plan decision's into test_model_load_utils.py, moe.py's helpers' into test_moe_a2a.py (a test_moe.py in tests/unit already belongs to modelopt/torch/models/moe.py, and pytest refuses two test modules of one basename in a run), and the plain-module MoE they share into tests/_test_utils/torch/moe_models.py; test_resolve_distribution_plan.py is gone. Test bodies are unchanged apart from the helpers' names. Pre-commit passes against main; the moved tests pass on CPU with transformers 5.18 and 5.5.4.
  • Review suggestions, 2026-10-29, Claude's approving round: build_meta_causal_lm's dead local import of AutoModelForImageTextToText is gone; routed experts the built model has but the config does not count (under num_experts, num_local_experts, n_routed_experts or num_routed_experts) are refused unless the degree is 1, as a plan naming no module of the model is, instead of being replicated on every rank silently. The new test fails without the refusal (mutation-checked). Pre-commit passes against main; this PR's tests pass on CPU with transformers 5.18 and 5.5.4.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Additive.
  • 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?: N/A. The feature's entry lands with [OMNIML-6085] [3/3] Run expert-parallel PTQ from hf_ptq.py #2754.
  • Did you get Claude approval on this PR?: ✅ Approved 2026-10-29 at 1f9b0ee7e98; its two suggestions are taken since.

Additional Information

Part 5 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after #2736.

Summary by CodeRabbit

  • New Features
    • Added automatic model distribution planning, including expert- and data-parallel degree selection and dense-parameter sharding based on GPU memory.
    • Added support for environment-configured parallelism settings and model selection and sizing for vision-language models.
    • Added validation for model configurations and parallelism choices.
  • Bug Fixes
    • Distribution plans are now consistent across ranks, and planning failures are propagated to all ranks.

@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

The change adds model and routed-expert discovery, selects EP and dense-layout settings, and resolves a distribution plan across ranks. Unit tests cover discovery, planning decisions, model-class selection, broadcasts, and checkpoint visibility errors.

Changes: Model distribution planning

Layer / File(s) Summary
Model and routed-expert discovery
modelopt/torch/utils/moe.py, modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_resolve_distribution_plan.py
Adds expert-count and EP-degree helpers, matches routed-expert keys to model modules, and selects the Transformers model class for meta-model construction. Tests cover config discovery, routed-expert matching, and model-class selection.
EP and dense-layout decisions
modelopt/torch/utils/env.py, modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_resolve_distribution_plan.py
Adds environment accessors and decision logic for EP degree and dense replication or FSDP sharding. Tests cover defaults, overrides, divisibility, memory thresholds, and invalid settings.
Plan resolution and rank coordination
modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_resolve_distribution_plan.py
Adds checkpoint config loading and rank-0 plan computation and broadcast. Tests cover broadcast behavior and checkpoint-visibility errors.

Priority: Normal

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant resolve_distribution_plan
  participant Rank0
  participant MetaModel
  participant OtherRanks
  Caller->>resolve_distribution_plan: checkpoint path and planning options
  resolve_distribution_plan->>Rank0: config and planning request
  Rank0->>MetaModel: build meta model and discover routed experts
  Rank0->>resolve_distribution_plan: distribution plan or error
  resolve_distribution_plan->>OtherRanks: broadcast plan
  OtherRanks->>resolve_distribution_plan: adopt plan
Loading
Merge Risk: Moderate · up to 7c4fe

For some composite or remote vision-language checkpoints, the distribution plan can pick a wrong expert-parallel degree or a wrong dense layout. Fix both selection paths before merging.

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 three modelopt Python modules and one test file. The changed code has no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), eval/exec on externa…
Title check Passed The title clearly describes the main change: determining model distribution from a model built on the meta device.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

PASS. The PR changes only three modelopt Python modules and one test file. The changed code has no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), eval/exec on external input, or # nosec. Transformers calls pass the caller-controlled trust_remote_code value, and the public resolver defaults it to False; no hardcoded trust_remote_code=True was added. No pyproject.toml or requirements.txt dependency changes are present.

✨ Finishing Touches: 💡 2
📝 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

Comment @coderabbitai help to get the list of available commands.

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.27536% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 80.44%. Comparing base (2b900c8) to head (2a6290b).

Files with missing lines Patch % Lines
modelopt/torch/utils/plugins/model_load_utils.py 98.97% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2737      +/-   ##
==========================================
+ Coverage   78.09%   80.44%   +2.35%     
==========================================
  Files         662      651      -11     
  Lines       76114    73117    -2997     
==========================================
- Hits        59439    58821     -618     
+ Misses      16675    14296    -2379     
Flag Coverage Δ
examples ?
examples-diffusers 19.88% <0.00%> (-0.04%) ⬇️
examples-gpt-oss 13.46% <0.00%> (-0.02%) ⬇️
examples-hf_ptq 23.48% <15.94%> (-0.02%) ⬇️
examples-llm_distill 13.53% <0.00%> (-0.02%) ⬇️
examples-llm_eval 17.26% <15.94%> (+0.02%) ⬆️
examples-llm_qat 17.38% <0.00%> (-0.06%) ⬇️
examples-llm_sparsity 16.77% <15.94%> (+0.02%) ⬆️
examples-megatron_bridge 26.47% <0.00%> (-0.29%) ⬇️
examples-specdec_bench 13.24% <0.00%> (-0.02%) ⬇️
examples-speculative_decoding 17.50% <15.94%> (-0.04%) ⬇️
examples-torch_onnx 21.17% <0.00%> (-0.05%) ⬇️
examples-torch_trt 15.19% <0.00%> (-0.02%) ⬇️
examples-vllm_serve 17.48% <15.94%> (+<0.01%) ⬆️
gpu 55.03% <23.91%> (+2.11%) ⬆️
regression 15.02% <0.00%> (-0.02%) ⬇️
unit 62.01% <99.27%> (+0.07%) ⬆️

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
shengliangxu force-pushed the shengliangx/hfep-2-plan-decision branch from f1e16c8 to 8b9651f Compare October 12, 2026 02:24
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-29 19:06 UTC

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 8b9651f

@shengliangxu shengliangxu changed the title [2/19] Decide the expert-parallel plan from the model and the knobs [OMNIML-6054] [2/19] Decide the expert-parallel plan from the model and the knobs Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-1-session branch from 2dcf64f to a97136a Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-2-plan-decision branch 2 times, most recently from 2ba6ef1 to fe5b268 Compare October 13, 2026 01:32
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-1-session branch from a97136a to fc0ed3b Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test fe5b268

@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-6054] [2/19] Decide the expert-parallel plan from the model and the knobs [OMNIML-6054] [2/20] Decide the expert-parallel plan from the model and the knobs Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-2-plan-decision branch from fe5b268 to 74bbbf9 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-6054] [2/20] Decide the expert-parallel plan from the model and the knobs [OMNIML-6054] [2/21] Decide the expert-parallel plan from the model and the knobs Oct 13, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 74bbbf9

@shengliangxu
shengliangxu removed this pull request from stack #2781 October 14, 2026 00:13
@shengliangxu
shengliangxu changed the base branch from shengliangx/hfep-1-session to shengliangx/hfep-1b-session-facts October 14, 2026 00:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-2-plan-decision branch from 74bbbf9 to 9074ecf Compare October 14, 2026 00:14
@shengliangxu
shengliangxu added this pull request to stack #2786 October 14, 2026 00:14
@shengliangxu shengliangxu changed the title [OMNIML-6054] [2/21] Decide the expert-parallel plan from the model and the knobs [OMNIML-6054] [3/22] Decide the expert-parallel plan from the model and the knobs Oct 14, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 9074ecf

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-1b-session-facts branch from c3f96b6 to d023003 Compare October 14, 2026 01:47
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-2-plan-decision branch from 9074ecf to 00417b9 Compare October 14, 2026 01:47
build_meta_causal_lm read transformers' private _model_mapping._extra_content through getattr
defaults, so a release that moved it would have turned the restore into a silent no-op, and with it
brought back rank 0's model without generate and the deadlock it caused. It now reads the attribute
directly: a move fails the build at once.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The grouped-GEMM parameters' parents counted only when no container style appeared in the plan, so a
plan styling some layers' expert containers and only the grouped-GEMM parameters of others lost the
second kind, both in the decision and in the expert-parallel load. Every transformers 5.18 plan with
both styles names the same containers both ways, so the union changes nothing for them.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Its *ForConditionalGeneration architecture reads like a VLM's, so T5, BART, Whisper and Qwen2-Audio
fell through to AutoModel, which leaves the LM head out of the parameters the dense fit counts. The
seq2seq auto classes now claim their configs first.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
MODELOPT_FSDP_SIZE=Auto or WORLD worked, but MODELOPT_EP_DEGREE=AUTO was refused as neither auto nor
a degree.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
For a VLM whose nested text config holds the MoE, an expert-count field on the root config was read
first, so an automatic ep_size could be chosen against a count the plan does not describe.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal always said ep_size=, also for a degree read from
MODELOPT_EP_DEGREE, so a user who set the variable was told to change an
argument they never passed. It now names what the degree came from.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
It is the module's rank-0 printer, already used for the load's other
notice, and flushes as the explicit print did.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The meta build kept only the auto class it built through. A remote
VLM's __init__ can build its text model through AutoModelForCausalLM
while the whole model builds through another auto class, and that
registration stayed: rank 0's later load through AutoModelForCausalLM
would then build the registered class, without the generate its peers'
load adds. The build now also keeps AutoModel's and
AutoModelForCausalLM's registrations as it found them.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A config with routed experts whose base_model_ep_plan names containers
the built model has none of (a renamed submodule, a hand-written plan)
left the decision with no experts to count, so the automatic degree
became 1 and every expert was replicated on every rank without a word;
the expert-parallel load finds the experts the same way, so it could
not have sharded them either. That is refused now, naming the
containers, unless the degree is set to 1.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
An explicit EP degree that does not divide the world size was refused
by DistributionPlan, as ep_size=..., also when MODELOPT_EP_DEGREE set
it. The decision now refuses it first and names the knob or argument
the degree came from, as its refusal for the experts does.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_auto_class fell back to the headless AutoModel for any multimodal config that neither the
image-text nor a seq2seq class recognizes, also when AutoModelForCausalLM builds it whole, as it
does Phi-4-multimodal (the one such config in transformers 5.5.4 and 5.18). The decision then
weighed the model without its LM head. AutoModelForImageTextToText exists in every supported
release, so its import guard goes.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… decides the plan

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The largest degree holds the fewest experts per rank, leaving the most memory
for activations, which calibration batches fill; any degree above 1 pays the
same all-to-alls per MoE layer.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Nothing deferred them: modelopt.torch.utils.moe does not import this module, and
transformers is imported at the top already. The refusal's second import of
_routed_expert_keys goes with them.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The plan decision imported modelopt.torch.utils.distributed inside the
function, with nothing left to defer it: the module already imports names
from it at the top. It is bound as a module alias, which the tests patch.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
init_empty_weights re-wraps every parameter it registers, which unties what
from_config tied, so the plan decision counted a tied embedding and LM head
twice. It now re-ties the meta model before summing its bytes, as the loaded
model holds them. The dense-fit test is sized from a normally built model, tied
and untied, at either side of the 60% share.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…s seq2seq class

The plan decision took a config with no nested text or vision config and no
*ForConditionalGeneration architecture for a causal LM, so a bare T5Config or
WhisperConfig failed its meta build. is_encoder_decoder now sends it to the
seq2seq dispatch.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
transformers is the optional [hf] extra, and modelopt.torch.utils.moe must
import without it (test_moe_a2a imports it in the torch-only install).

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
CONTRIBUTING.md names a test file after the production module it covers. The
tests of resolve_distribution_plan, _auto_class and build_meta_causal_lm move
into test_model_load_utils.py, and those of moe.py's expert counting, EP
degree and routed-expert match into test_moe_a2a.py, that module's test file:
tests/unit already has a test_moe.py (modelopt/torch/models/moe.py's), and
pytest refuses two test modules of one basename in a run. The plain-module MoE
the two files share is in _test_utils/torch/moe_models.py. Test bodies are
unchanged apart from the helpers' names, and the routed-expert test skips
without transformers, as test_moe_a2a.py imports without it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The module imports it at its top since this PR, and the repo's transformers floor
(5.5) has it, so the local import and its comment were dead.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
When the plan's expert containers are in the built model but the config spells
its expert count under none of the fields the decision reads, the automatic
degree came out 1 and every expert stayed on every rank without a word. It is
now refused, naming the fields and the knob or argument to set to 1, as the
refusal of a plan naming no module of the model is.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 2a6290b

@kevalmorabia97 kevalmorabia97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The plan decision and the collectives are correct, and the tests cover the logic that matters (107 passed). Non-blocking: build the meta model from copy.deepcopy(hf_config), since the meta build sets _attn_implementation_internal and _experts_implementation_internal on rank 0 only, and resolve_distribution_plan returns that same config.

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.

3 participants