Repository navigation
[OMNIML-6054] [2/2] Route load_pretrained by the distribution plan - #2779
shengliangxu wants to merge 99 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. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. ℹ️ Recent review info
Walkthrough
Merge Risk:
|
| Check name | Status | Explanation |
|---|---|---|
| Linked Issues check | The only linked issue, #2774, is closed and completed. The issue states it is historical context only. No active directly linked target remains, so no linked-issue coding requirements apply to this pu… |
|
| Out of Scope Changes check | The whole-PR summary (mergeBase 4e10244 to currentHead fb8bf38) shows changes that match the PR description. In modelopt/torch/utils/plugins/model_load_utils.py, load_pretrained gains a distribution… | |
| Security Anti-Patterns | No listed security anti-pattern is introduced by the pull request. In the changed modelopt Python files, torch.load uses weights_only=True; no added weights_only=False, allow_pickle=True, ha… |
|
| Title check | The title clearly and concisely describes the main change: routing load_pretrained based on the distribution plan. |
|
| Description Check | Check skipped - CodeRabbit’s high-level summary is enabled. |
Full details: Linked Issues check
-
Explanation
The only linked issue,
#2774, is closed and completed. The issue states it is historical context only. No active directly linked target remains, so no linked-issue coding requirements apply to this pull request. The prior PASS result still holds.
Full details: Out of Scope Changes check
-
Explanation
The whole-PR summary (mergeBase 4e10244 to currentHead fb8bf38) shows changes that match the PR description. In modelopt/torch/utils/plugins/model_load_utils.py, load_pretrained gains a distribution parameter, resolve_distribution_plan gains config and Hub access options, and fsdp2_wrap moves here. In modelopt/torch/utils/distributed.py, fsdp2_wrap forwards to the new location and its signature is unchanged. The Hub access options support the PR's Hub ID localization. The test changes cover the same functions: test_example_utils.py (FakeModel now a Module so the plan can be recorded), test_export_carry_over.py and test_model_load_utils.py (manual plan recording after fsdp2_wrap), test_distributed_run.py (forwarding), test_unplaced_source_keys.py (plan check), test_resolve_distribution_plan.py (Hub options), and the new unit and GPU test modules. The CHANGELOG.rst entry documents the same behavior. No file in the summary is shown to be unrelated. The assessment rests on the AI-generated summary; the source hunks were not read in this review.
Full details: Security Anti-Patterns
-
Explanation
No listed security anti-pattern is introduced by the pull request. In the changed
modeloptPython files,torch.loadusesweights_only=True; no addedweights_only=False,allow_pickle=True, hardcodedtrust_remote_code=True,eval()/exec()call, or# noseccomment exists. The pull request changes nopyproject.tomlorrequirements*.txtfile, so it adds no dependency requiring license review.
Finishing Touches
-
Generate docstrings
-
- Commit to this branch
- Create a new PR
Fix failing CI checks
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2779 +/- ##
===========================================
+ Coverage 71.55% 81.75% +10.19%
===========================================
Files 664 657 -7
Lines 76659 73928 -2731
===========================================
+ Hits 54856 60442 +5586
+ Misses 21803 13486 -8317
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:
|
|
/ok to test 3ac7c7b |
3ac7c7b to
df76b36
Compare
|
/ok to test df76b36 |
df76b36 to
aa3615e
Compare
|
|
/ok to test aa3615e |
00417b9 to
ff7f381
Compare
aa3615e to
168c90d
Compare
|
/ok to test 168c90d |
ff7f381 to
f61d317
Compare
168c90d to
537ff8d
Compare
|
/ok to test 537ff8d |
f61d317 to
a179de9
Compare
465f1eb to
aa2a065
Compare
|
/ok to test aa2a065 |
…e on one The docstring read as if every rank's whole model went onto its GPU; that default holds only when no device_map is passed, and device_map=None, from_pretrained's own, loads on the CPU for the caller to place, which a test row now pins. mp_policy or cpu_offload passed as None, their default, no longer refuses a whole-model load, which made the same call load or raise by rank 0's GPU memory. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…it lives The loaders import fsdp2_wrap, so a bare reference in their docstrings named no documented function, and the wrap's own reference to load_pretrained named none in its new module. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… wrap's docstring The wrap's module does not import load_pretrained, so its docstring names it in plain text, as the shorter line keeps the source budget. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The FSDP2 load refused a quantized checkpoint by its top-level quantization_config alone, but a VLM such as Kimi-K2.5 keeps its language model's under text_config, and its remote code builds it as a causal LM, so the load read the packed weights as stored and left the model's weights uninitialized. The check now looks under text_config and llm_config too. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…odel Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
fsdp2_wrap refused only a model recorded as replicated, so one load_pretrained had already sharded passed the check and reached FSDP2's own error at its first layer. It now refuses a model that, or any of whose submodules, records a plan, and names the plan and where it is recorded. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Such a rank loads the whole model where from_pretrained places it, on the CPU, while its peers load onto their GPUs. It now says so with a warning of its own, rather than one on rank 0, which may have a GPU and nothing to say. The load is otherwise unchanged. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…a decided plan The sharded load refused any checkpoint whose whole model is not built through AutoModelForCausalLM, but the FSDP2 load builds the text-only VLMs it supports (Qwen3.5) through their image-text class, as hf_ptq's --use_fsdp2 relies on. It now refuses a checkpoint only when that is not the class the load would build. Its refusals offered MODELOPT_FSDP_SIZE=1 as a way to load whole on every rank, which a plan passed as distribution= ignores. They offer it only for a plan the load decided, and a replicated plan in either case. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… texts load_pretrained now records a plan on every model it returns, a single process's too, and fsdp2_wrap refuses a model that records one, so wrapping a model load_pretrained returned no longer works; the changelog says so and what to do instead. The warning of a rank without a GPU of its own no longer claims its peers have one, and the FSDP2 loader's docstring says a direct caller records its own plan. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…p2_wrap alike fsdp2_wrap repeated set_distribution_plan's walk for a module that records a plan, read its private attribute, and raised a RuntimeError where set_distribution_plan raises a ValueError for the same record. Both now use _recorded_plan, and fsdp2_wrap raises a ValueError too, so a caller catches one type. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…acement in one place A whole-model load refused cpu_offload=False, the FSDP2 load's own default and what hf_ptq passes when --cpu_offload is not set, while it took cpu_offload=None. Each FSDP2-only option now asks nothing at its own default as well as at None, broadcast_chunk_size excepted. The refusal of placement options under a layout rank 0's GPU memory chose moved into _refuse_sharded_load_options, which already decides which options place a whole model, so the two can no longer disagree, and its way out is _whole_instead's. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…kpoint Under a layout rank 0's GPU memory chose, which nothing asked for, the FSDP2 load refused a quantized checkpoint, or one carrying an MTP head, with only "load it unsharded". Both refusals now name the way to load it whole, as the load's other refusals do, and the changelog lists them. The docstring says which FSDP2-only options a whole load takes as asking nothing, and the dtype check is written once. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_has_own_gpu() read a rank's GPU as the one its local rank numbers, so a launch that exposes one GPU to each process, each bound to cuda:0, read every rank but the first as having none: the FSDP2 load refused it, and a whole load left it on the CPU with a warning. A missing LOCAL_RANK on later nodes did the same. _own_gpu_index() takes the device the process has set up once CUDA is initialized, else the one its local rank numbers, and the device_map default and the FSDP2 load place onto it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…k_size=8 as nothing With MODELOPT_EP_DEGREE unset, the plan decision picked an expert-parallel degree for an MoE, which load_pretrained then refused, as no load here shards the routed experts yet: a multi-rank load of an MoE with no knob set failed. It now decides such a degree as 1 and loads; a degree MODELOPT_EP_DEGREE asks for, as a plan handed in, is still refused. A whole-model load also takes broadcast_chunk_size=8, the FSDP2 load's own default, as asking nothing, so a caller can forward every FSDP2 option at its default whatever the plan. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
MODELOPT_EP_DEGREE or MODELOPT_FSDP_SIZE set to whitespace only stripped to an empty value both parses refused. env.py now strips both knobs and reads a blank one as auto, as it reads an empty one, so the parses need no strip of their own. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…tself A sharded load supports a text-only VLM, which it builds through its image-text class, yet refuses any auto_class but AutoModelForCausalLM. The docstring now says so: leave auto_class out for such a VLM. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The docstring said a call that places the model loads only where it fits one GPU, which reads as covering device_map=None. An option at from_pretrained's own default places nothing: the whole load keeps device_map=None on the CPU, while a layout rank 0's GPU memory chose shards the model. Say so. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A launch without a group that MODELOPT_EP_DEGREE asks to shard was told to load inside distributed_run(), where the plan's EP degree is refused next, as no load here shards the routed experts: the refusal now names MODELOPT_EP_DEGREE=1 as well. A sharded load's refusal of an auto_class was the one with no way out: it now says to leave the class out, or to load the model whole with it, and both auto_class refusals say which class the load builds, a text-only VLM's image-text class included. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_own_gpu_index reads torch.cuda.is_initialized() and current_device(), but the fixtures, the standalone placement tests and the pooled refusal workers pinned only availability and the device count, so a host process that had set CUDA up would hand them cuda:0 where they expect a local rank's GPU. One helper, _pin_cuda, pins all four: unbound for the local-rank cases, bound for the bound-device ones. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A load the plan decision decides read MODELOPT_EP_DEGREE on every rank just before resolve_distribution_plan, outside any collective refusal: a rank whose knob did not parse raised alone while rank 0 went on into the decision's collectives and waited there. The knob is now parsed in the load-arguments check, whose refusal raises on every rank. A two-rank test with the knob malformed on rank 1 alone fails on the group's bound without this. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
CONTRIBUTING asks for brief docstrings. load_pretrained's keeps the routes, the recording contract and the refusal guarantees; the extended routing guidance moves to the PR description. Comments keep their reasons in fewer lines, the FSDP2-only options are named once, and the refusal of an unknown option drops a branch for a sharded load other than FSDP2, which has none. No behaviour changes. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…uses The refusal under a layout rank 0's GPU memory chose said the refused options place a whole model, but they are every keyword the sharded load does not take (use_safetensors, low_cpu_mem_usage, ...). It now says they do not apply to a sharded load, as the refusal beside it does, and names the way to load the model whole. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
On a distributed run the whole-model load defaults each rank's device_map to its own GPU. A loader that places the model itself, as init_quantized_weights does, refuses any device_map, so its caller passes device_map=None, which the default leaves alone. The docstring says so. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
init_quantized_weights quantizes and compresses the model in the AutoModelForCausalLM.from_pretrained it patches. Under a live multi-rank group a load the automatic layout shards, or a handed plan's, built the model through from_config instead and returned it unquantized. The context now marks the from_pretrained it installs, and a sharded load refuses while it is installed, on every rank, before anything loads, naming the way to load the model whole. A test forces the automatic layout to FSDP2 inside the real context, and hands a sharded plan, and fails without this. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… skips Hub options The routing guidance that moved out of load_pretrained's docstring is now a docs page, "Loading Hugging Face models across ranks", under the quantization guide, and the docstring links it. The docstring said the config and model loads both take the Hub options; the FSDP2 load builds the model with from_config, which takes none, so the docstring and the page now say so. set_distribution_plan's docstring is back to main's wording. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Inside init_quantized_weights a multi-rank load_pretrained failed whatever it decided: a sharded layout was refused, and a replicated one injected each rank's device_map, which the context's from_pretrained refuses, naming an argument the caller never passed. As for a model class the sharded loads refuse, the plan decision now gives every rank the whole model there when nothing asks to shard, without measuring it, and the whole-model load leaves the device map to the context, which computes it. A knob or a handed plan that asks to shard is still refused. A test loads inside the real context, with rank 0's GPU memory fitting the model or not, and reaches the context's from_pretrained past its device_map check; it fails without this. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
| - Bump minimum required PyTorch version to 2.10. | ||
| - Bump minimum required ``diffusers`` version to 0.37, ``peft`` to 0.18.1, ``accelerate`` to 1.6 and ``nltk`` to 3.9 for the ``[hf]`` extra. | ||
| - Drop support for Transformer Engine 1.x. Quantizing Transformer Engine modules now requires Transformer Engine 2.0 or later. | ||
| - ``load_pretrained`` called in a process group of several ranks now decides how to distribute the model rather than load it whole on every rank: a model too big for one GPU is FSDP2-sharded, a load that refuses placement arguments, quantized checkpoints, ones carrying an MTP head and a call inside ``init_quantized_weights``; an MoE loads without expert parallelism, and is refused when ``MODELOPT_EP_DEGREE`` asks for it, until the expert-parallel load lands; a model class other than a causal LM is refused when ``MODELOPT_FSDP_SIZE`` or ``MODELOPT_EP_DEGREE`` asks to shard; and a model loaded whole goes onto each rank's own GPU unless ``device_map`` places it. Set ``MODELOPT_FSDP_SIZE=1`` and ``MODELOPT_EP_DEGREE=1``, or pass ``distribution=DistributionPlan(<ranks>, dense="replicate")``, to load it whole on every rank. |
There was a problem hiding this comment.
[SUGGESTION] This entry's class-refusal clause is narrower than the code, and the entry is well past CONTRIBUTING's "one or two sentences … for external users".
Accuracy. "a model class other than a causal LM is refused when MODELOPT_FSDP_SIZE or MODELOPT_EP_DEGREE asks to shard" describes _decide_distribution's auto_class refusal (model_load_utils.py:1441-1448) only. A measured layout refuses a class too: with distribution=None, no knob set and auto_class left out, _decide_distribution builds on meta, dense="auto" picks "fsdp" for a checkpoint over 60% of rank 0's GPU, and _sharded_load_kwargs then raises NotImplementedError because _auto_class(hf_config) is not builds (:588-602) — e.g. a large Phi-4-multimodal-style config AutoModelForCausalLM accepts but _auto_class routes to AutoModelForImageTextToText. The refusal itself is fine (it ends with _whole_instead(decided)), but a reader of this entry concludes their knob-free multi-rank load of a non-causal checkpoint is unaffected.
Length. The first sentence is ~110 words carrying five distinct behaviour changes. docs/source/guides/_distributed_loading.rst is now the right home for the detail; the changelog needs what users must do.
Suggested replacement for the entry:
- ``load_pretrained`` called in a process group of several ranks now decides how to distribute the
model rather than load it whole on every rank: a model too big for one GPU is FSDP2-sharded, and
the sharded load refuses placement arguments, a ``dtype``, a quantized checkpoint, one carrying an
MTP head, a model class it does not build, and a call inside ``init_quantized_weights``; an MoE
loads without expert parallelism until the expert-parallel load lands. Set ``MODELOPT_FSDP_SIZE=1``
and ``MODELOPT_EP_DEGREE=1``, or pass ``distribution=DistributionPlan(<ranks>, dense="replicate")``,
to keep loading it whole on every rank; see :doc:`/guides/_distributed_loading`.(The device_map note and the "whole load goes onto each rank's own GPU" behaviour read better as their own short bullet than inside this one.)
There was a problem hiding this comment.
Taken in 0d3d0937c05, close to your wording, so the entry now lists every class the sharded load refuses and not only the knob case.
- The entry lists what the sharded load refuses: placement arguments, a
dtype, a quantized checkpoint, one carrying an MTP head, a model class it does not build, and a call insideinit_quantized_weights. The class refusal now covers a layout rank 0's GPU memory chose. - It says an MoE loads without expert parallelism, which
MODELOPT_EP_DEGREEcannot ask for until the expert-parallel load lands. - It names the settings that keep loading whole, and points to the new guide by its title. I used the title because the changelog has no
:doc:links and is also read on GitHub. - The per-rank GPU placement of a whole load is now a short bullet of its own.
|
/ok to test c78d2e5 |
The entry said only a knob refuses a model class other than a causal LM, but a layout rank 0's GPU memory chose refuses one too, and it ran to five behaviour changes in one sentence. It now lists what the sharded load refuses, says what to set to keep loading whole, and points to the new guide; the per-rank GPU placement of a whole load is a bullet of its own. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test 0d3d093 |
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 [6/21]. Rebased onto main 7c3c868 later the same day, after #2739 merged, and onto main f03651e that night.
[2/2] of a split (2026-10-30), as review asked to keep each PR under the source-line budget: #3140 ([1/2]) moves
fsdp2_wrapout ofmodelopt.torch.utils.distributedintomodelopt.torch.utils.fsdp2_utils, and this PR routesload_pretrainedby the distribution plan.Source lines: 496 against #3140, under the ~500 budget: trimmed on 2026-10-31 on the user's decision not to split.
Base: #3140 (
shengliangx/hfep-13a-move-fsdp2-wrap). The diff below is this PR's alone. Merge order: #3140 first, then this PR, which then retargets to main; until then GitHub runs no unit, GPU, example or regression tests on it (they run against main).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
load_pretrained(#2733) loads a model whole. A distributed load, such asparallel_load_and_prepare_fsdp2(main), is a separate entry point, and every caller picks one itself, ashf_ptq.py --use_fsdp2does. A caller that picks a loader also gets what that loader skips: a sharded load cannot run the model type's loading hook, so a Nemotron-H checkpoint's MTP head is not built and its tensors go to the export unquantized.This PR makes
load_pretrainedthe one loader, whole or distributed:distributionis aDistributionPlan; by default it is the planresolve_distribution_plan([OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737) decides.dense="fsdp":parallel_load_and_prepare_fsdp2over the process group. A single process launched withtorchrun --nproc_per_node=1has no group, so the load sets one up, of one rank, once nothing can refuse the load anymore; a process started without a launcher cannot set one up and is refused.distributed_run([OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736) destroys it with the run. One rank of a multi-rank launch whose group is not set up yet is refused too, told to set it up first withdistributed_run(), rather than shard over the launch's ranks under a one-rank plan; the plan-size refusal says the same when that is why the sizes differ.from_pretrained. On a distributed run every rank gets the whole model, onto its own GPU when it has one and nodevice_mapis passed.MODELOPT_EP_DEGREEasks for, or a handed plan withep_size > 1, is refused before anything loads, and the refusal namesMODELOPT_EP_DEGREE=1.resolve_distribution_plantakes the class the load builds with (auto_class, whichload_pretrainedpasses). With a class the sharded loads do not build and no knob asking to shard, it plans the whole model on every rank without a meta-device build, so an encoder-decoder loaded throughAutoModel, for example, loads multi-rank as before.set_distribution_plan, [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736), where calibration and export read it.load_pretrainedrecords it exactly once, at the end of every load; the loaders it calls record none.fsdp2_wraprecords nothing, as on main, so a caller that wraps or loads a model through it directly records the plan itself, as the error for a missing plan says.hf_ptq.py --use_fsdp2loads throughparallel_load_and_prepare_fsdp2directly and records no plan in this PR. Recording it there lands with [OMNIML-6083] Write a sharded model's HF checkpoint without gathering it #2752, the first PR whose export reads it on that path, and [OMNIML-6085] [3/3] Run expert-parallel PTQ from hf_ptq.py #2754 moves that load ontoload_pretrained.fsdp2_wrapmoved intomodelopt.torch.utils.fsdp2_utils, which imports no Hugging Face package and which the loaders import it from, in [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140 ([1/2]), which keeps its old path. This PR makes it refuse a model that, or any of whose submodules, records a plan, as a modelload_pretrainedreturned does: a plan is recorded once, so a model to shard yourself is one loaded withfrom_pretrained.auto_mapnames comes with the environment's Hub settings.broadcast_from, [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736) before it branches, and loads a plan rank 0 was handed just as it would one it decided. Ranks that each loaded the plan they were handed could build mismatched meshes and hang, and ranks passing different kinds would wait in each other's collectives. Rank 0 checks what it was handed before it sends it, a non-plan included, so a distribution it refuses raises on every rank instead of leaving the others waiting in the broadcast.all_reduce([OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736): a refusal on any rank raises on every rank, before the checkpoint's fetch for the arguments, after it for a rank that cannot see the checkpoint rank 0 fetched, and before the sharded load's collectives for what the config says, instead of leaving the other ranks waiting there.hf_ptq.py --use_fsdp2refuses Nemotron-H MTP on the same loader (validate_fsdp2_supported), and this applies its rule to every caller. The hook could wrap the FSDP2 load's meta-device build, but the head's calibration forward inside the root FSDP2 unit and its quantized export from FSDP2 shards have never run on that path: supporting MTP under FSDP2 is a change to that loader and to hf_ptq, validated on a real checkpoint, not part of this routing.from_pretrained's placement arguments (device_map,max_memory, ...) and anauto_classother thanAutoModelForCausalLM(leave it out: the load picks the class), rather than ignore them, and an explicitdtype, saying why it does not apply: the load keeps the checkpoint's dtype, and FSDP2'smp_policysets the dtype it computes in. An option passed asNone,from_pretrained's own default, asks nothing and is not refused.--use_fsdp2does, and any other model throughAutoModelForCausalLM. It refuses a config_auto_classwould not build through that class, as most VLMs (whichAutoModelForCausalLMrefuses deep in the loader, or builds as the language model alone), before it sets up a group or reads anything, and the refusal says how to load the model whole on every rank: its model class asauto_class, withMODELOPT_FSDP_SIZE=1(for a plan the load decided) or a replicated plan.hf_configwithout the plan it was resolved with is refused. A sharded load builds the model from it as given, so a plan decided here, from the checkpoint's own config, would not be the one it came with.config=is for a config the plan should be decided from.resolve_distribution_plan's docstring says to hand the config it returns toload_pretrainedonly with a plan that shards the model: a whole-model load reads the checkpoint's config itself and refuseshf_config.The FSDP2 route's refusals name their ways out (
MODELOPT_FSDP_SIZE=1for a plan the load decided, or a replicated plan), and a config handed with a plan is copied before the load builds from it. With a plan handed and no config, the FSDP2 route reads the checkpoint's config once and hands it to the load, rather than reading it twice. Deciding a plan builds the meta-device model from a copy of the config, so the config returned with the plan, which a sharded load builds from, is the same on every rank: the build writes its dtype and its attention and experts implementations, and only rank 0 builds.Routing details are in the docs page this PR adds,
docs/source/guides/_distributed_loading.rst("Loading Hugging Face models across ranks", under the quantization guide), whichload_pretrained's docstring links.Usage
Testing
Tests in
tests/unit/torch/utils/test_model_load_utils.py, beside the plan decision's (the module's test file, as CONTRIBUTING asks). They run on CPU, and stand-in loaders record their calls. They cover:test_a_load_refuses_what_it_cannot_loadhas a VLM row (a Llava config), which fails without the refusal;cpu_dist_workers), rank 0 handed a replicated plan and rank 1 an FSDP2 one, or none: both load whole and record rank 0's plan. A rank that loads the plan it was handed fails at once, on the placement refusal. Rank 0 handed a non-plan fails both ranks;dtypebefore the checkpoint is fetched or by a quantizedhf_configbefore the FSDP2 load: both ranks raise rank 1's refusal. The fetch and the load stand in as collectives on a group bounded to 10 s, so without the change the test fails fast (rank 0 gets a dropped connection) instead of hanging;With [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737's plan-decision tests: the config returned with the plan is the one every rank read, unchanged by rank 0's meta-device build (new
test_the_config_returned_with_the_plan_is_as_every_rank_read_it, which fails without its fix).A single process's load, placed as
from_pretrainedplaces it, with the caller's arguments and a single process's plan: [OMNIML-6080] Derive the unplaced source keys once, at load, through a core load_pretrained #2733's real CPU load intests/unit/torch/utils/test_unplaced_source_keys.py, which now also checks the plan.fsdp2_wrapleaving the plan to the load, and refusing a model recorded as replicated: CPU unit tests intests/unit/torch/utils/test_fsdp2_utils.py. [OMNIML-6080] Derive the unplaced source keys once, at load, through a core load_pretrained #2733's GPU tests that wrap or load through it directly (test_export_carry_over.py,test_model_load_utils.py) record the plan themselves. Its old path forwarding every argument: [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140'stest_fsdp2_wrap_keeps_its_old_path.Mutation-checked: 17 mutants, each failing a test. They drop the rank-GPU default, pin a single process's model, drop the config handoff or the world-size check, load an expert-parallel or FSDP2 plan whole, accept a string, let each rank load the plan it was handed, record no plan in the load or record one in
fsdp2_wraptoo, set no group up for a single process's FSDP2 load, set one up over a live group, or have the oldfsdp2_wrappath drop an argument or the result.New tests in
tests/gpu/torch/utils/test_model_load_utils.pyrunload_pretrainedleft to decide, on every GPU of the node:MODELOPT_FSDP_SIZE=worldFSDP2-sharded;tests/examples/hf_ptq/test_example_utils.py:get_model's stand-in model is a module now, sinceload_pretrainedrecords its plan on it.The load refusals are one parametrized test, and the single-process load test gives way to [OMNIML-6080] Derive the unplaced source keys once, at load, through a core load_pretrained #2733's real load. Each drop is checked by mutation.
A single process without a launcher is refused an FSDP2 load, without stubbing the setup, and a handed config is left as the caller passed it. A rank of a multi-rank launch whose process group is not set up is refused any load, before anything is fetched. The refusal table has an explicit
dtype.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/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.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: a rank of a multi-rank launch without its process group is refused on every route, before anything is fetched; a caller's model class with a knob asking to shard is refused before the model is built; a changelog entry for
fsdp2_wrap's move. 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). [OMNIML-6064] Load each rank's checkpoint shard straight into a meta-built model #2739's
FSDPModuleimport, now on main, and this PR'sCPUOffloadPolicy, fully_shardare one import line, andfsdp2_wrapsits ahead of the meta-DCP loader, as before.Review fixes, 2026-10-27 (Claude's review after the rebase): a sharded load takes
dtype=Noneandtorch_dtypeasfrom_pretraineddoes.Noneand"auto"keep the checkpoint's dtype under either name, and any other dtype is refused with the dtype message, wheretorch_dtypegot the placement one. The new cases fail without the change.Review suggestions, 2026-10-27 evening: a launch whose processes set no group up (hf_ptq under torchrun without
--use_fsdp2) loads the whole model on each process again, refusing there only a plan for several ranks and a sharded load; a handed plan is checked before the checkpoint is fetched; an FSDP2 layout decided from rank 0's GPU memory says so when it refuses placement options;fsdp2_wrapimports the layer detection where it uses it.Review fixes, 2026-10-27 night: a launch without its process group refuses
MODELOPT_EP_DEGREEabove 1 orMODELOPT_FSDP_SIZEnaming the world before anything is fetched, and the sharded-load refusal names the plan to pass. The refusal test's new rows fail without the change.Review suggestions, 2026-10-27, last round: on a launch without its group, a
MODELOPT_EP_DEGREEthat is not an EP degree is refused with the plan decision's message, and a padded value is read as its number;load_pretrained's docstring says the automatic layout weighs the model in the checkpoint's own dtype. The refusal test's new row fails without the change.Review fix, 2026-10-27, last round: a one-rank sharded plan in a launch without its group is refused before the checkpoint is fetched, and the advice for a model with no plan names
load_pretrainedagain, which records the plan from this PR on.Review fixes, 2026-10-27, last round:
hf_configwith a plan that loads the model whole is refused before the checkpoint is fetched, with the no-plan case's message, and the new pre-fetch test row fails without the change;fsdp2_wrap's docstring says the FSDP2 load wraps with it.Review suggestions, 2026-10-28: the refusals that depend only on a handed plan's arguments (
auto_class,dtype, options the sharded load does not take) run before the checkpoint is fetched, and the new test row fails without the change; a test docstring no longer saysfsdp2_wraploads.Review suggestions, 2026-10-28, fifth round: the handed-plan test pins the
auto_classanddtypearms of the pre-fetch refusal, each failing with its arm left out of the pre-fetch call; the four function-local imports ofmodelopt.torch.utils.distributedgo, now that [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737 imports it at module top.Declined review suggestion, 2026-10-28, fifth round: one helper for the kwargs view the pre-fetch refusal and
_load_shardedread; the two agree, and the handed-plan test pins each pre-fetch arm.Rebased on 2026-10-29 onto [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737's current head
2a6290b470a(main2b900c8fa41). This PR's own diff is unchanged, except that its test additions follow the module-mirroring test files: the plan-decision test goes totest_model_load_utils.py([OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737 deletedtest_resolve_distribution_plan.py), and thefsdp2_wrapold-path test totest_distributed.py(main renamedtest_distributed_run.py). One test fixture changed: main'sbroadcast_fromnow picks its sender bymodelopt.torch.utils.distributed.rank(), which the load-routing tests'process_groupfixture sets to 1 for the FSDP2 load, sorun_asnow runs the real broadcast with the deciding rank as its sender. No source change. Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq argument and example-utils tests pass on CPU with transformers 5.18 and 5.5.4.2026-10-29, at
59e83993426:tests/unit/torch/exportandtests/examples/hf_ptq/test_carry_over_layouts.pyalso pass on CPU with transformers 5.18 and 5.5.4, apart from 7test_export_diffusers.pycases that fail the same way on [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737's head in this environment; GitHub runs no unit tests on this PR until it targets main.GPU check, 2026-10-29, at
59e83993426, the first run of this PR's multi-GPU tests, on GB200 (torch 2.11.0a0 from NGC PyTorch 26.02, transformers 5.9.0): the newtest_load_pretrained_distribution.py, the two files whose tests now record the plan by hand (test_model_load_utils.py,test_export_carry_over.py), andtest_distributed.py, whosefsdp2_wraptests import it through its old path:59e83993426tests/gpu/torch/utils/test_load_pretrained_distribution.pytests/gpu/torch/utils/test_model_load_utils.pytests/gpu/torch/export/test_export_carry_over.pytests/gpu/torch/utils/test_distributed.pyRebased on 2026-10-29 onto main
70147bcbfa1, [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737's squash, after [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737 merged; this PR now targets main. Its own diff is unchanged. The one conflict was the changelog: main's Transformer Engine 1.x line and this PR'sfsdp2_wrapline are both kept. Pre-commit, mypy included, passes against main;tests/unit/torch/utils,tests/unit/torch/exportand the hf_ptq tests that load throughload_pretrainedpass on CPU with transformers 5.18 and 5.5.4, apart from the same 7test_export_diffusers.pycases as above. GitHub now runs the full unit, GPU, example and regression CI on it.Review fixes, 2026-10-29 (cjluo-nv's review): a refusal on any rank fails every rank, of the arguments before the checkpoint is fetched and of what the config says before the sharded load (the argument checks now run after the plan's broadcast; the sharded load's checks moved into
_sharded_load_kwargs, run inside theall_reduce). The new two-rank test fails on all three of its cases with the change reverted. The routing tests moved into the unit and GPUtest_model_load_utils.pyfiles, test bodies unchanged; the changelog entry is two sentences; the MTP refusal's reason is above. Pre-commit, mypy included, passes against main;tests/unit/torch/utils,tests/unit/torch/exportand the hf_ptq tests that load throughload_pretrainedpass on CPU with transformers 5.18 and 5.5.4, apart from the same 7test_export_diffusers.pycases as above.Review fix, 2026-10-29, cjluo-nv's second round: a rank that cannot see the checkpoint rank 0 fetched fails every rank, the fetch running inside the same
all_reduceas the other refusals. The two-rank visibility test (test_a_rank_that_cannot_see_the_checkpoint_fails_every_rank) also loads by a handed plan, on the test's own gloo group with the error caught on both ranks, and fails in about 8 s with the change reverted. Pre-commit, mypy included, passes against main;tests/unit/torch/utils,tests/unit/torch/exportand the hf_ptq tests that load throughload_pretrainedpass on CPU with transformers 5.18 and 5.5.4, apart from the same 7test_export_diffusers.pycases.Rebased on 2026-10-30 onto main
99c3834f62c(after [OMNIML-6081] [1/2] Pack sharded weights in FSDP2 weight-update windows, and NVFP4 DTensor shards #3099 and [OMNIML-6081] [2/2] Fold fused experts' quantizers into their weights shard-locally #2750 merged), without conflicts; this PR's own diff is unchanged. Pre-commit, mypy included, passes against main.Review fix, 2026-10-30 (cjluo-nv):
config=given as a path, asfrom_pretrainedtakes it, is read as the load reads it, and the plan decision, the loading hook and a sharded load use that config, so a weights-only checkpoint with its config in another directory loads again (it had failed on the decision's read of the checkpoint's own config.json).test_a_config_passed_for_the_load_needs_none_on_disknow also loads one throughLlamaForCausalLM, with and withoutmodel_type; both cases fail without the change. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Review suggestion, 2026-10-30 (Claude): the
config=path is read through the model class's ownconfig_class, asfrom_pretrainedreads it, and throughAutoConfigfor the auto classes; a new row loads a model class of the caller's own whose config classAutoConfigcannot resolve, and fails without the change. Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Review fix, 2026-10-30 (Claude): a process without a group, or in a group of one, takes
DistributionPlan()without deciding or reading a config, as before plans existed, so a model class of the caller's own with itsmodel_typegiven loads again whateverAutoConfigknows (test_a_model_class_of_the_callers_own_reads_its_config_itself, which fails without the change); a model class's own config class reads aconfig=path withouttrust_remote_code. Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Review fix, 2026-10-30 (Claude), the several-rank arm: the plan decision reads the checkpoint's config as the load does, through the model class's own
config_classor, for an auto class,AutoConfig, so a model class of the caller's own loads whole on every rank whateverAutoConfigknows;test_a_model_class_of_the_callers_own_loads_wholehas a two-rank row with such a config on disk, which fails without the change. The otherAutoConfigreads on the load path (amodel_typeleft to read, which main reads too; the FSDP2 route's, which only auto classes reach) refuse nothing main loaded. Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Review fixes, 2026-10-30 (Claude): on a plan that loads the model whole, the options only the FSDP2 load takes (
cpu_offload,mp_policy,broadcast_chunk_size) are refused with the other load checks, rather than reachfrom_pretrained;MODELOPT_EP_DEGREEhas one parse, the plan decision's, which a launch without its group now uses too and which takes surrounding spaces and refuses a degree below 1. New rows (an FSDP2 option on a replicated plan, a padded auto, a degree of -2) fail without the change. Source insertions were then 509, a few over the ~500 budget, for these fixes. Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Review suggestions, 2026-10-30 (Claude's approving review at
cb7fb1e8495): a distributed whole-model load defaults each rank'sdevice_mapto its GPU only where CUDA is available, so a run without GPUs (a gloo group on CPU) loads whole again; a model type left to read is read as the model class reads its config;MODELOPT_FSDP_SIZEtakes surrounding spaces asMODELOPT_EP_DEGREEdoes. New rows (two gloo ranks loading whole without adevice_map, a caller's model class whose model type is read, a paddedMODELOPT_FSDP_SIZE) fail without the change. Source insertions are 508. Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.2026-10-30, line budget (cjluo-nv): the routing's comments and docstrings are tightened, with no change in what a load does or refuses, bringing source insertions to 488; the automatic layout's placement refusal reads
MODELOPT_FSDP_SIZEthrough its one parse,_dense_from_env(Claude's last suggestion). Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Split on 2026-10-30 (cjluo-nv's review): the
fsdp2_wrapmove, its old path, its changelog line and its test went to [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140 ([1/2], 54 source lines); this PR is rebased onto it and keeps the rest (434 source lines). The two merged together give exactly the tree of this PR's previous heada51881aa6a8on maincb9ab4de069; the rebase dropped three commits whose whole change [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140 now carries, and resolved the commits that had moved or rewordedfsdp2_wrapto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140's version. Pre-commit, mypy included, passes against main at both heads;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4 at both heads.2026-10-30, after [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140's review fixes: rebased onto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140
6c44b40df74(it now also carriesfsdp2_wrap's dtype helpers, 114 source lines); this PR's own diff is unchanged. Review suggestions (Claude's approving review):load_pretrained's docstring says that a single process, or a group of one, reads no knob and takes the FSDP2 load only from a plan passed asdistribution, and that a launch without its group loads with nodevice_mapdefault. Source insertions: 435. Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Review fixes, 2026-10-30 (Claude): the whole-model load runs inside the same failure-carrying
all_reduceas the refusals, so a rank whose load fails alone (out of memory on its GPU, say) fails every rank rather than leave the others in the next collective;test_a_whole_load_that_fails_on_one_rank_fails_every_rank(two gloo ranks) fails without it.set_distribution_plan's docstring wording. Source insertions: 439. Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Rebased on 2026-10-30 onto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140
48891f691fb(its old path's narrower import error); this PR's own diff is unchanged. Pre-commit passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Review fix, 2026-10-30 (Claude), rebased onto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140
26bf59ace7c: a model FSDP2-sharded afterload_pretrainedrecorded a replicating plan is refused bydistribution_plan(), rather than read as replicated by the export; record-once stays, and theload_pretrainedandfsdp2_wrapdocstrings say to wrap a model loaded withfrom_pretrainedand record its plan after. The two-ranktest_a_model_with_no_plan_raises_in_a_run_of_several_rankscovers it and fails without the change. Source insertions: 453. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Review fixes, 2026-10-30 (cjluo-nv, Claude), rebased onto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140
a4bdab004de: the FSDP2 load is refused, with the other load checks and so on every rank, on a rank without a GPU, which in a group the caller set up used to fail inside the load while the others waited in its broadcasts (test_a_load_refused_on_one_rank_fails_every_rank's new two-rankno-gpurow, which fails without it);config,variantorhf_configpassed asNone, their default, is taken as not passed instead of refused (three new rows oftest_a_load_it_decides_reads_the_config_once, which fail without it). Source insertions: 466. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4.Review fixes, 2026-10-30 (Claude), rebased onto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140
a627d7d6477: a distributed whole-model load defaults a rank'sdevice_maponly to a GPU of its own (its local rank below the visible GPU count), so a host with fewer GPUs than ranks no longer hands a rank a device it lacks, and the FSDP2 load's refusal asks the same; the CHANGELOG says a model loaded whole on several ranks goes onto each rank's own GPU; the two-rank gloo load test masks CUDA, so it no longer depends on the host's GPUs; a model class given by name is refused by name; anep_sizethat is no whole number ([2],2.9) is refused rather than raise aTypeErroror be truncated. New rows (no_gpu_of_its_own,fewer-gpus-than-ranks, the by-name and whole-number tests) fail without their change. Source insertions: 478. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (339 passed, 35 skipped).Review fix, 2026-10-30 (Claude): the check that a model recorded as replicated was not sharded after its plan was recorded moved from
distribution_plan(), where it walked the model on every read of a replicated plan (the export reads it per weight), tofsdp2_wrap, which now refuses such a model;distribution_plan()is an attribute read again.test_fsdp2_wrap_refuses_a_model_recorded_as_replicatedfails without the guard. The single-process FSDP2 load's comment says the group it sets up outlives the load outsidedistributed_run. Source insertions: 478. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers.Review fixes, 2026-10-30 (Claude): a load option outside the sharded load's own set passed as
None, from_pretrained's default (device_map=Noneas hf_ptq passes it,max_memory,offload_folder,quantization_config), is taken as not passed instead of refused by the FSDP2 load or the automatic layout (newplacement-noneandnonerows, which fail without it); the CHANGELOG files what changes for a load in a group of several ranks under Backward Breaking Changes, withMODELOPT_FSDP_SIZE=1/MODELOPT_EP_DEGREE=1or a replicated plan to keep it whole; the refusal tests match the option, knob or value a refusal names rather than its wording (tests only). Source insertions: 487. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers.Review fixes, 2026-10-30 (Claude): the docstring says a rank's whole model goes onto its own GPU only when no
device_mapis passed,device_map=Nonekeepingfrom_pretrained's load on the CPU for the caller to place (a row now pins it);mp_policyorcpu_offloadpassed asNoneno longer refuses a whole-model load, which made the same call load or raise by rank 0's GPU memory (new row, which fails without it). Source insertions: 494. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers.Rebased on 2026-10-30 onto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140
ad004655ca9(cjluo-nv's review there:fsdp2_wraplives inmodelopt.torch.utils.fsdp2_utils, free of the Hugging Face packages). This PR's own changes to the wrap, its docstring and its refusal of a model recorded as replicated, moved with it, and their tests intotests/unit/torch/utils/test_fsdp2_utils.py; the docstrings' references tofsdp2_wrappoint there. Source insertions: 496. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (344 passed, 33 skipped).Review fix, 2026-10-30 (Claude): the FSDP2 load refuses a VLM whose language model is quantized, its
quantization_configundertext_configorllm_configas Kimi-K2.5 keeps it, rather than only a top-level one; its remote code builds it as a causal LM, so the load had read the packed weights as stored. The newquantized-language-modelrow fails without the change. Source insertions: 498. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers.Review suggestions taken on the user's decision, 2026-10-30 (Claude):
fsdp2_wraprefuses a model that, or any of whose submodules, records any distribution plan, not only a replicating one, naming the plan; a rank of a replicated load that has no GPU of its own, and nodevice_map, warns on that rank (not throughwarn_rank_0) that it loads wherefrom_pretrainedplaces it. New rows (a sharded plan, a plan on a submodule) andtest_a_rank_without_a_gpu_of_its_own_says_sofail without the change, the last also with the warning sent throughwarn_rank_0. Source insertions: 511, past the budget by the user's decision. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers.Review fixes, 2026-10-30 (Claude): the FSDP2 load refuses a checkpoint only when it would not build the class
_auto_classnames, so a Qwen3.5 VLM, which it builds through its image-text class as hf_ptq's--use_fsdp2does, reaches it (test_a_text_only_vlm_the_fsdp2_load_builds_reaches_it, which fails without the change); its refusals offerMODELOPT_FSDP_SIZE=1only for a plan the load decided (test_a_sharded_load_offers_the_knob_only_for_a_plan_it_decided); the changelog filesfsdp2_wrap's refusal of a modelload_pretrainedreturned as breaking; the no-GPU warning no longer claims a rank's peers have GPUs; andparallel_load_and_prepare_fsdp2's docstring says a direct caller records its own plan. Source insertions: 529. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (346 passed, 33 skipped).Review suggestion, 2026-10-30 (Claude's approving review at
c49fd54f6ee):set_distribution_planandfsdp2_wrapfind a recorded plan with one walk,distributed._recorded_plan, andfsdp2_wraprefuses with aValueError, asset_distribution_plandoes for the same record, rather than aRuntimeError; its test expects theValueError. Source insertions: 543. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (346 passed, 33 skipped).Review suggestions, 2026-10-30 (Claude's approving review at
4ffccd8088a): a whole-model load takescpu_offload=False, the FSDP2 load's own default and what hf_ptq passes without--cpu_offload, as asking nothing (newcpu_offload_at_its_defaultrow, which fails without the change); the refusal of placement options under a layout rank 0's GPU memory chose moved into_refuse_sharded_load_options, which already decides which options place a whole model, and uses_whole_insteadfor its way out (the refusals are unchanged). Source insertions: 538. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (346 passed, 33 skipped).Review fixes, 2026-10-30 (Claude): under a layout rank 0's GPU memory chose, the FSDP2 load's refusals of a quantized checkpoint and of one carrying an MTP head name the way to load it whole, as its other refusals do, and the changelog lists them (
test_a_measured_layout_names_the_way_back_when_it_refuses_a_checkpoint, which fails without the change); the docstring says which FSDP2-only options a whole load takes as asking nothing; the dtype check is written once. Source insertions: 540. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (346 passed, 33 skipped).Fix, 2026-10-30 (found on [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136 by meenchen's review, agreed with that PR): a rank's own GPU is the one its process is bound to once CUDA is initialized, as torch's DeviceMesh takes it, else the one its local rank numbers (
_own_gpu_index(), with_has_own_gpu()over it). A launch that exposes one GPU to each process, each bound to cuda:0, no longer reads every rank but the first as GPU-less: the FSDP2 load no longer refuses it, a whole load no longer leaves it on the CPU with a warning, and both place onto the bound device (test_a_rank_bound_to_its_one_visible_gpu_loads_onto_it, and the two-ranktest_one_visible_gpu_per_process_shards_on_every_rank, both failing without the change). Source insertions: 556. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (346 passed, 33 skipped).Review fixes, 2026-10-30 (Claude): an EP degree left to the plan decision is decided as 1, so a multi-rank load of an MoE with no knob set loads without expert parallelism instead of being refused, while a degree
MODELOPT_EP_DEGREEasks for is still refused (test_a_load_it_decides_keeps_the_experts_whole, whose left-to-decide row fails without the change); a whole-model load takesbroadcast_chunk_size=8, the FSDP2 load's own default, as asking nothing (new row). Source insertions: 563. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (346 passed, 33 skipped).Rebased on 2026-10-30 onto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140
74a636f02c0, itself on main059588b89a5, without conflicts; this PR's own diff is unchanged. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers.Review suggestions, 2026-10-30 (Claude, at
47ca6d1c422):MODELOPT_EP_DEGREEorMODELOPT_FSDP_SIZEset to whitespace only reads as unset (auto), as an empty one does, instead of being refused:env.pystrips both knobs, and the two parses no longer strip their own (newexported-blankrows, which fail without the change). The docstring says a sharded load builds a text-only VLM through its image-text class itself and takes noauto_classbutAutoModelForCausalLM, so leave it out for one. Source insertions: 567. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (345 passed, 35 skipped).Review suggestions, 2026-10-30 (Claude, at
e78cbe68469): the docstring says an option atfrom_pretrained's own default,device_map=Noneincluded, places nothing and does not keep a load whole. A launch without a group thatMODELOPT_EP_DEGREEasks to shard is told to setMODELOPT_EP_DEGREE=1as well, since no load here shards the routed experts. A sharded load's refusal of anauto_classnames its way out (leave it out, or load the model whole with it), and bothauto_classrefusals say which class the load builds. Theep-knobandmodel_classtest rows now expect the way out and fail without the change. Messages and docstring only. Source insertions: 577. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 and 5.5.4, andtests/unit/torch/utilswithout transformers (345 passed, 35 skipped).Rebased on 2026-10-30 onto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140
b4445d14a8a, itself on maind464d30b120, without conflicts; this PR's own diff is unchanged. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 (778 passed) and 5.5.4 (757 passed, 21 skipped), andtests/unit/torch/utilswithout transformers (346 passed, 36 skipped).Review fixes, 2026-10-31 (cjluo-nv): a load whose plan the load decides parses
MODELOPT_EP_DEGREEin the load-arguments check, so a knob one rank cannot parse fails every rank instead of leaving rank 0 in the plan decision's collectives (test_a_knob_one_rank_cannot_parse_fails_every_rank, two ranks, which fails on the group's bound without the change). The unit tests pin the CUDA state the GPU check reads (_pin_cuda), so a host process that set CUDA up no longer hands them cuda:0; with CUDA set up beforehand, five of them failed before.load_pretrained's docstring is shortened to the routes, the record and the refusal guarantees, its extended guidance moved to Routing details above, and the routing's comments are tightened, with no change in behaviour: source insertions 483 against [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 (779 passed) and 5.5.4 (758 passed, 21 skipped), andtests/unit/torch/utilswithout transformers (346 passed, 36 skipped).Review suggestions, 2026-10-31 (Claude, at
54ae57e1cfd): the refusal under a layout rank 0's GPU memory chose says the options it refuses do not apply to a sharded load, rather than that they place a whole model, as they are any keyword the sharded load does not take (itsplacedrow now matches the new wording and fails without the change). The docstring says to passdevice_map=Nonewhere the model is placed otherwise, as underinit_quantized_weights, which refuses the per-rankdevice_mapa distributed whole-model load defaults to. Source insertions: 484 against [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140. Pre-commit, mypy included, passes against main;tests/unit/torch/utilsand the hf_ptq example tests pass on CPU with transformers 5.18 (779 passed) and 5.5.4 (758 passed, 21 skipped), andtests/unit/torch/utilswithout transformers (346 passed, 36 skipped).Review fixes, 2026-10-31 (cjluo-nv, at
32179e5ad47): a sharded load is refused insideinit_quantized_weights, on every rank and before anything loads, since the FSDP2 load builds the model withfrom_configand would skip the quantization and compression the context'sfrom_pretrainedapplies (test_a_sharded_load_is_refused_inside_init_quantized_weights, with the automatic layout forced to FSDP2 inside the real context and with a handed sharded plan, fails without the change). The routing guidance is published as a docs page, whichload_pretrained's docstring links, and the docstring and the page say a sharded load'sfrom_configtakes no Hub options. Source insertions: 494 against [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140. Pre-commit, mypy included, passes against main; the docs build passes with warnings as errors;tests/unit/torch/utils, the hf_ptq example tests and the accelerate plugin tests pass on CPU with transformers 5.18 (784 passed) and 5.5.4 (763 passed, 21 skipped), andtests/unit/torch/utilswithout transformers (346 passed, 36 skipped).Review fix, 2026-10-31 (Claude, at
6ed77327b96): insideinit_quantized_weights, a multi-rank load whose plan the load decides gives every rank the whole model when nothing asks to shard, without measuring it, and leaves the device map to the context, which computes it and refuses one passed; before, it was refused when the layout sharded and failed on an injecteddevice_mapwhen it replicated. A knob or a handed plan that asks to shard is still refused.test_a_load_it_decides_inside_init_quantized_weights_is_whole_and_unplaced(rank 0's GPU memory fitting the model or not) fails without the change. Source insertions: 495 against [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140. Pre-commit, mypy included, passes against main;tests/unit/torch/utils, the hf_ptq example tests and the accelerate plugin tests pass on CPU with transformers 5.18 (786 passed) and 5.5.4 (765 passed, 21 skipped), andtests/unit/torch/utilswithout transformers (346 passed, 36 skipped).Rebased on 2026-10-31 onto [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140
30ab95bdb53, itself on maine352caca3c3, without conflicts; this PR's own diff is unchanged. Pre-commit, mypy included, passes against main;tests/unit/torch/utils, the hf_ptq example tests and the accelerate plugin tests pass on CPU with transformers 5.18 (786 passed) and 5.5.4 (765 passed, 21 skipped), andtests/unit/torch/utilswithout transformers (346 passed, 36 skipped).Review suggestion, 2026-10-31 (Claude, at
c78d2e50ed6): the changelog's backward-breaking entry lists what the sharded load refuses, the class refusal of a layout rank 0's GPU memory chose included, in two sentences, and the whole load's per-rank GPU placement is its own bullet. Text only. Source insertions: 496 against [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140. Pre-commit, mypy included, passes against main.Before your PR is "Ready for review"
load_pretrainednow decides how to distribute the model rather than load it whole on every rank (MODELOPT_FSDP_SIZE=1andMODELOPT_EP_DEGREE=1, or a replicated plan, keep it whole), and a model it loads whole goes onto each rank's own GPU unlessdevice_mapis passed.load_pretrainedrecords the plan on the model it returns, even in a single process, andfsdp2_wrapnow refuses a model that records one (wrap a model loaded withfrom_pretrained). Otherwise a single process loads as before unless a plan it is handed shards the model, anddistributionis new.fsdp2_wrap's old import path still works ([OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140).CONTRIBUTING.md: N/A1064e3d09ce, after the fetch fix; its suggestions are answered in their threads. Approved again at3160a0fb703after the config-path fix; its suggestion is taken in6c36e2f8a88; its reviews since are answered inf065849c295,c53b4863b21andcb7fb1e8495; approved again atcb7fb1e8495andd9d394b8f55, their suggestions taken in1b308a07425anda51881aa6a8, re-requested.Additional Information
Part 6 of 21 of expert-parallel PTQ for Hugging Face MoE models, now [2/2] after #3140; #2737, below it, merged on 2026-10-29. The expert-parallel half of this routing is #2780.
Replaces #2774, which carried this routing whole as [19/20]. Moving its branch down the stack made the branch part of #2774's old base, so GitHub closed #2774 as merged into that base; nothing reached main.
Summary by CodeRabbit
New Features
Documentation