Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dd3885a
Route load_pretrained by the distribution plan
shengliangxu Oct 13, 2026
7a1c3cf
Load the plan rank 0 was handed on every rank
shengliangxu Oct 13, 2026
5ae6375
Take a plan the caller hands load_pretrained through the plan handle
shengliangxu Oct 14, 2026
383b532
Hand every rank rank 0's plan through the renamed broadcast_from
shengliangxu Oct 14, 2026
4e41b36
Route load_pretrained by the live process group instead of the session
shengliangxu Oct 14, 2026
0ba3164
Load by a distribution plan only, and record it on the loaded model
shengliangxu Oct 14, 2026
2714a15
Record the plan where FSDP2 wraps a model, in fsdp2_wrap
shengliangxu Oct 14, 2026
2555520
Give get_model's tests a module for load_pretrained to record its pla…
shengliangxu Oct 14, 2026
49a5589
Set up a single process's FSDP2 group in the load that needs it
shengliangxu Oct 14, 2026
153594d
Move fsdp2_wrap to the model loaders, keeping its old path
shengliangxu Oct 14, 2026
3c5f078
Record a load's plan in load_pretrained alone
shengliangxu Oct 15, 2026
3ede1c0
File the distributed load_pretrained entry under 0.49.0 in the changelog
shengliangxu Oct 15, 2026
a0d517e
Answer the checkpoint-read gather in the load-routing tests' launches
shengliangxu Oct 16, 2026
aa3689d
Test on two real ranks that every rank loads the plan rank 0 was handed
shengliangxu Oct 16, 2026
afbd944
Trim the load routing's tests
shengliangxu Oct 16, 2026
376587b
Import LayerActivationCollector at the load-routing tests' module scope
shengliangxu Oct 16, 2026
77f40ac
Pass the Hub options on to the plan's checkpoint read
shengliangxu Oct 20, 2026
7d681c8
Test that the plan's checkpoint read takes the Hub options
shengliangxu Oct 20, 2026
1b3296a
Read a sharded load's config with the Hub options too
shengliangxu Oct 20, 2026
410ca6a
Decide the plan from a config passed for the load
shengliangxu Oct 22, 2026
cc95eba
Test a passed config with a plan handed to the load too
shengliangxu Oct 22, 2026
f1e23aa
Refuse hf_config without the plan it was resolved with
shengliangxu Oct 22, 2026
8fe1734
Say in load_pretrained that the FSDP2 load refuses a quantized checkp…
shengliangxu Oct 22, 2026
172c114
Tell a launcher-less FSDP2 load to use torchrun, and copy a handed co…
shengliangxu Oct 24, 2026
6ecf3f3
Refuse an FSDP2 load in a multi-rank launch whose group is not up, an…
shengliangxu Oct 24, 2026
abd3dd9
Drop the mocked Hub-options test the plan's cached-revision test covers
shengliangxu Oct 24, 2026
17d6195
Take rank 0's distribution on every rank; load a caller's class whole
shengliangxu Oct 25, 2026
8e4aa77
Decide from a copy of the config; read an FSDP2 load's config once
shengliangxu Oct 25, 2026
30a58b9
Refuse an FSDP2 load of a VLM before it builds one as a causal LM
shengliangxu Oct 25, 2026
0c5596a
Refuse a load on a rank of a launch whose process group is not set up
shengliangxu Oct 27, 2026
a0bf4ba
Refuse a caller's model class before building the model a sharded pla…
shengliangxu Oct 27, 2026
4be027e
Note in the changelog that fsdp2_wrap moved with the loaders
shengliangxu Oct 27, 2026
49949f1
Wrap two lines of the launch refusal as ruff formats them
shengliangxu Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ Changelog
*Misc*

- The ``evaluation`` agent skill can stop looping ("doom loop") reasoning early: ask it to avoid doom loops and it adds vLLM's ``repetition_detection`` request field (vLLM 0.17.0 or later) to every task, ending a request once its output repeats. Use the same setting for baseline and candidate runs.
- ``modelopt.torch.utils.plugins.model_load_utils.load_pretrained`` now also loads a model distributed. Under ``torchrun`` it FSDP2-shards the model or gives every rank a whole copy, by the plan the new ``resolve_distribution_plan`` decides from the model and the ``MODELOPT_FSDP_SIZE`` variable, or by a plan passed as its new ``distribution`` argument. The loaded model records the plan (``modelopt.torch.utils.distributed.distribution_plan``). Until the expert-parallel load lands, an MoE planned expert-parallel, as the default ``MODELOPT_EP_DEGREE=auto`` plans one whose config has a ``base_model_ep_plan``, is refused before it loads: set ``MODELOPT_EP_DEGREE=1`` (with ``MODELOPT_FSDP_SIZE=world`` to FSDP2-shard it) or pass a plan with ``ep_size=1``.

**Backward Breaking Changes**

- ``export_mcore_gpt_to_hf`` given a Hugging Face Hub model ID no longer writes the tokenizer, processor, generation config or remote-code files. Pass a local directory instead, e.g. from ``modelopt.torch.export.ensure_local_checkpoint``.
- ``modelopt.torch.utils.distributed.fsdp2_wrap`` moved to ``modelopt.torch.utils.plugins.model_load_utils``, with the other loaders. The old import path still works, but calling it now needs the ``hf`` extra.
- Diffusion HF exports now omit attention quantizer buffers by default, except for FLUX.1, which retains enabled Q/K/V ``_amax`` keys for runtime compatibility; other consumers of those keys can opt in with ``export_attention_quantizers=True`` (CLI: ``--extra-param export_attention_quantizers=true``). Disabled attention quantizers remain excluded, and diffusion components, including LTX-2, no longer emit KV-cache quantization metadata.

**Deprecations**
Expand Down
48 changes: 7 additions & 41 deletions modelopt/torch/utils/distributed.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

import torch
import torch.distributed
from torch.distributed.fsdp import CPUOffloadPolicy, FSDPModule, fully_shard
from torch.distributed.fsdp import FSDPModule
from torch.distributed.tensor import DTensor

__all__ = [
Expand Down Expand Up @@ -734,49 +734,15 @@ def _move_to_fsdp_device(model, params: set[torch.nn.Parameter]) -> None:


def fsdp2_wrap(model, shard_root=True, mp_policy=None, cpu_offload: bool = False):
"""Auto-detect a HF causal-LM's decoder layers and FSDP2 ``fully_shard`` each one.
"""FSDP2-shard a HF causal LM's decoder layers, and the root unless ``shard_root=False``.

By default (``shard_root=True``) the root module is wrapped too, so embed/lm_head/norm are
sharded instead of replicated per rank; pass ``shard_root=False`` to leave the root replicated
(only decoder layers sharded). Returns the detected decoder layers so callers can reuse the
detection result.

Parameters whose dtype differs from the model's dominant one are excluded from the wrap (see
:func:`_off_dtype_params`), since FSDP2 rejects a shard group that mixes dtypes. They stay
replicated and are moved onto the shards' device, which ``fully_shard`` does not do for the
params it ignores.
It loads a model, so it lives with the loaders now:
:func:`modelopt.torch.utils.plugins.model_load_utils.fsdp2_wrap`. This keeps the old path, which
now needs the ``hf`` extra, as the loaders do.
"""
# Lazy import: layerwise_calib imports this module at top level (circular).
from modelopt.torch.quantization.utils.layerwise_calib import LayerActivationCollector

decoder_layers = LayerActivationCollector.get_decoder_layers(model)
if decoder_layers is None:
raise RuntimeError(
"Could not auto-detect decoder layers; FSDP2 wrap requires a standard HF causal-LM layout."
)
from modelopt.torch.utils.plugins.model_load_utils import fsdp2_wrap as wrap

fsdp_kwargs: dict[str, Any] = {"reshard_after_forward": True}
if mp_policy is not None:
fsdp_kwargs["mp_policy"] = mp_policy
if cpu_offload:
fsdp_kwargs["offload_policy"] = CPUOffloadPolicy()
ignored_params = _off_dtype_params(model)
if ignored_params:
fsdp_kwargs["ignored_params"] = ignored_params

# Snapshot/restore config.architectures: some HF builders mutate it during fully_shard.
config = getattr(model, "config", None)
architectures = list(getattr(config, "architectures", []) or [])
for layer in decoder_layers:
fully_shard(layer, **fsdp_kwargs)
if shard_root:
fully_shard(model, **fsdp_kwargs)
if ignored_params:
_move_to_fsdp_device(model, ignored_params)
if config is not None and architectures:
config.architectures = architectures

return decoder_layers
return wrap(model, shard_root=shard_root, mp_policy=mp_policy, cpu_offload=cpu_offload)
Comment thread
shengliangxu marked this conversation as resolved.
Comment on lines 736 to +745

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[SUGGESTION] fsdp2_wrap is a public __all__ export of modelopt.torch.utils.distributed, and after the move calling it requires the hf extra (model_load_utils imports transformers, accelerate and huggingface_hub at module level) where on main it needed only torch. Importing the module still works, since the forward is lazy, so the break is call-time only.

Impact is small in practice — the function only works on "a standard HF causal-LM layout", so any real caller already has transformers — and the docstring does say so. But per the repo's changelog rule ("backward breaking changes" get an entry), the CHANGELOG.rst line added here covers only load_pretrained; a clause noting that modelopt.torch.utils.distributed.fsdp2_wrap moved to modelopt.torch.utils.plugins.model_load_utils and that the old path now needs the hf extra would let a user hitting the ImportError find the reason.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added a Backward Breaking Changes entry: fsdp2_wrap moved to modelopt.torch.utils.plugins.model_load_utils with the other loaders, and calling it through the old path now needs the hf extra.



def broadcast_state_dict(
Expand Down
Loading
Loading