Skip to content

prepare_model: don't move DTensor-sharded models to the device - #4181

Merged
SunMarc merged 5 commits into
mainfrom
fix-prepare-model-dtensor-to
Sep 3, 2026
Merged

SunMarc merged 5 commits into
mainfrom
fix-prepare-model-dtensor-to

Conversation

@qgallouedec

Copy link
Copy Markdown
Member

A model sharded at load time (e.g. transformers' DistributedConfig with FSDP2, optionally CPU-offloaded) manages its own parameter placement, but prepare_model unconditionally calls model = model.to(self.device) when device_placement is set. .to() recurses _apply through FSDP2-managed parameters and raises:

RuntimeError: Attempted to set the storage of a tensor on device "cpu" to a storage on different device "cuda:0".
RuntimeError: _apply(): Couldn't swap FSDPLinear.weight

model_has_dtensor already exists and is consulted a few lines below for the DDP case; use it to skip the device move too. Hit while fine-tuning GLM-5.2 (753B) with fsdp_cpu_offload=True through Trainer (huggingface/transformers#48204); on-device sharded models are unaffected because the move was a same-device no-op for them.

Reproduction

/!\ A standalone toy does not trigger this: plain FSDP2 (fresh or lazy-initialized, with or without CPU offload) tolerates the .to().

The failing _apply swap needs the parameter arrangement that transformers' sharded loading produces (DTensor expert weights on a tp mesh combined with FSDP2-managed dense parameters, CPU-offloaded). With the transformers side installed (huggingface/transformers#48204):

# torchrun --nproc_per_node 4 repro.py
import torch
from transformers import AutoModelForCausalLM, Trainer, TrainingArguments
from transformers.distributed import DistributedConfig

model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-30B-A3B", dtype=torch.bfloat16,
    distributed_config=DistributedConfig(tp_size=2, fsdp_size=2, enable_expert_parallel=True, fsdp_cpu_offload=True),
)
Trainer(model=model, args=TrainingArguments(output_dir="/tmp/x")).accelerator.prepare(model)
# RuntimeError: Attempted to set the storage of a tensor on device "cpu" to a storage on
# different device "cuda:0"  /  _apply(): Couldn't swap FSDPLinear.weight

The guard is correct independently of the trigger's rarity: a model holding DTensor parameters manages its own placement, and model_has_dtensor is already the codebase's test for exactly that condition a few lines below.

A model sharded at load time (e.g. transformers DistributedConfig with FSDP2,
optionally CPU-offloaded) manages its own parameter placement, and .to() on
FSDP2-managed parameters raises RuntimeError: _apply(): Couldn't swap
FSDPLinear.weight. model_has_dtensor already exists and is checked a few lines
below for the DDP case; use it to skip the device move too.
@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@qgallouedec
qgallouedec requested a review from SunMarc August 27, 2026 22:45

@SunMarc SunMarc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Indeed, thanks !

Comment thread src/accelerate/accelerator.py Outdated
Comment on lines +1874 to +1875
elif device_placement and not self.verify_device_map(model) and not model_has_dtensor(model):
# DTensor-sharded models manage their own placement; `.to()` on FSDP2-managed or CPU-offloaded params raises `_apply(): Couldn't swap ...`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe put the check in device_placement ? we already check for fsdp there

@qgallouedec qgallouedec Sep 2, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in f74c62f, moved into the device_placement resolution next to the FSDP check.

Note an explicit device_placement=True still attempts the move (same as FSDP today): the resolution is a default, not a guard, which I think is the right semantics.

Comment thread src/accelerate/accelerator.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

here

@SunMarc

SunMarc commented Sep 3, 2026

Copy link
Copy Markdown
Member

@bot /style

@github-actions

github-actions Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Style bot fixed some files and pushed the changes.

@SunMarc
SunMarc merged commit f7cc8e9 into main Sep 3, 2026
27 of 29 checks passed
@SunMarc
SunMarc deleted the fix-prepare-model-dtensor-to branch September 3, 2026 13:54
SunMarc added a commit that referenced this pull request Sep 25, 2026
…uted build (#4250)

* fix(dtensor): do not import torch.distributed on a non-distributed build 🧩

Fixes #4249.

model_has_dtensor() imports torch.distributed.tensor unconditionally. On a
PyTorch build compiled without a distributed backend that import raises rather
than returning False, and since v1.15.0 every prepare_model() call reaches it:

  accelerator.py:1802   device_placement = (... and not model_has_dtensor(model))
  utils/other.py:243    from torch.distributed.tensor import DTensor
  ...
  torch/distributed/distributed_c10d.py:25
  ModuleNotFoundError: No module named 'torch._C._distributed_c10d'

AMD's official Windows ROCm wheels are exactly that build: torch.distributed
imports, torch.distributed.is_available() is False, and torch._C._distributed_c10d
does not exist. So all training on AMD GPUs on Windows dies at trainer start.

In 1.14.0 the sole call site (accelerator.py:1878) sat behind
`if self.multi_device and not (...)`, so a single-GPU run never reached it. The
second call site added in #4181 is unguarded, which is what turned a
multi-device-only path into one every run executes.

Guarding model_has_dtensor itself rather than restoring the guard on the new
call site, because the invariant belongs to the function: DTensor lives under
torch.distributed, so a build without a distributed backend cannot hold DTensor
parameters at all. That also covers any future call site.

is_torch_distributed_available() is the existing idiom for this. It is already
imported in this same file and already used as a guard 70 lines further down,
and accelerator.py:2958 already computes
`is_dtensor_available = torch.distributed.is_available() and ...`, so the
"DTensor requires a distributed build" relationship is established.

2 tests added. The first reproduces the reported failure by making the import
raise the way it does on those wheels, so it fails against unpatched sources
with the bug report's own error:

  ModuleNotFoundError: No module named 'torch._C._distributed_c10d';
  'torch._C' is not a package

The second pins that the guard does not short-circuit the real check on a
normal build.

tests/test_utils.py: 48 passed, 4 skipped. ruff check and ruff format clean.

* Update src/accelerate/utils/other.py

---------

Co-authored-by: Marc Sun <57196510+SunMarc@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants