Repository navigation
[OMNIML-6054] Decide how a model is distributed, from the model built on the meta device - #2737
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. |
Codecov Report❌ Patch coverage is
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
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:
|
f1e16c8 to
8b9651f
Compare
|
|
/ok to test 8b9651f |
2dcf64f to
a97136a
Compare
2ba6ef1 to
fe5b268
Compare
a97136a to
fc0ed3b
Compare
|
/ok to test fe5b268 |
fe5b268 to
74bbbf9
Compare
|
/ok to test 74bbbf9 |
74bbbf9 to
9074ecf
Compare
|
/ok to test 9074ecf |
c3f96b6 to
d023003
Compare
9074ecf to
00417b9
Compare
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>
|
/ok to test 2a6290b |
kevalmorabia97
left a comment
There was a problem hiding this comment.
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.
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
mainWhat 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 aDistributionPlan(#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.auto_mapregistersAutoModelForImageTextToTextbuilds 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 theGenerationMixinevery other rank's first load adds. Rank 0's Kimi-K3 lackedgeneratewhile the other ranks' had it, and an expert-parallel preview deadlocked.ep_size, orMODELOPT_EP_DEGREE).autotakes the largest degree that divides both the world and the routed-expert count.base_model_ep_plannames. They are found by the rule the expert-parallel loader ([OMNIML-6060] [3/3] Refuse an expert-parallel load that would calibrate the experts wrong #2741) uses, so the decision and the load cannot disagree.dense, orMODELOPT_FSDP_SIZE, which takesauto,1to replicate, orworldor the world size to shard).autoFSDP2-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.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.resolve_distribution_plantakesrevision,token,cache_dir,local_files_onlyandsubfolderasload_pretraineddoes, so the plan and its config are for the checkpoint the load reads.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.MODELOPT_EP_DEGREEorMODELOPT_FSDP_SIZEmust not reshape it. It holds the whole model, unless the caller's owndense="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_modellives in the hf_ptq example, which the library cannot import, and is a single-processdevice_maploader: it requiresarchitecturesand takes its class from the first entry, beside the example's own speculative, pack-quantized and MXFP4 branches. Accelerate'sinfer_auto_device_mapandcompute_module_sizessay 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 experts1/ep_sizeand keeps the rest whole, a single sum over the meta model's parameters (re-tied first, asinit_empty_weightsunties them).The class that builds a VLM with its head takes the checkpoint's remote class only under
trust_remote_code, aMODELOPT_EP_DEGREEthat is not a number is refused by name, and a single process parses its ownep_sizeas a distributed run does, refusing a bad one the same way.Usage
Testing
tests/unit/torch/utils/test_model_load_utils.py(the decision) andtests/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: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 arebroadcast_from's, tested in [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736.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.all_reduce, the experts' share left out of the dense fit, the Hub options not passed on, a single process'sep_sizenot parsed, and a trusted remote VLM's registered image-text class not taken.ep * dp == worldand no-EP-plan tests, which the decision tests cover, are dropped. Each drop is checked by mutation.MODELOPT_EP_DEGREEnames the knob, as a badep_sizein a single process names the argument.tests/uniton 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.MODELOPT_EP_DEGREEreads 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.MODELOPT_EP_DEGREE) or the argument the degree came from.AutoModel's andAutoModelForCausalLM'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.build_meta_causal_lm's docstring states the registrations its restore assumes, andresolve_distribution_plan's that a caller passes the local checkpoint directory.AutoModelForCausalLMrecognizes (Phi-4-multimodal, the only such config in transformers 5.5.4 or 5.18) builds through it, head included, rather than throughAutoModel. The new table row fails without the change.resolve_distribution_plan's docstring says each rank returns its own copy of the config, rank 0's deciding the plan._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.ep_size=1anddenseare 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 withdistribution=, which skips the decision._auto_classfalls back toAutoModelfor a remote VLM whoseAutoModelForCausalLMclass 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, whichAutoModelcannot build, is not supported on main.modelopt.torch.utils.distributedis imported at module top asdist; [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 drops its four function-local imports of it.cf4597dcbeb, on main1ca8bd1fc4d(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_lmkeeps this PR'sauto_classparameter, now defaulting toNone, 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 andtest_model_load_utils.pypass on transformers 5.18 (90) and 5.5.4, and pre-commit passes against main.bdc45368d10after [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.init_empty_weightsunties 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 bareT5ConfigorWhisperConfig) 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.pyandtest_moe_a2a.pypass on CPU with transformers 5.18 and 5.5.4.test_model_load_utils.py,moe.py's helpers' intotest_moe_a2a.py(atest_moe.pyintests/unitalready belongs tomodelopt/torch/models/moe.py, and pytest refuses two test modules of one basename in a run), and the plain-module MoE they share intotests/_test_utils/torch/moe_models.py;test_resolve_distribution_plan.pyis 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.build_meta_causal_lm's dead local import ofAutoModelForImageTextToTextis gone; routed experts the built model has but the config does not count (undernum_experts,num_local_experts,n_routed_expertsornum_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"
CONTRIBUTING.md: N/A1f9b0ee7e98; 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