Skip to content

Commit 908064e

Browse files
zhixianglipre-commit-ci[bot]deependujhaCopilot
authored
fix(FSDP): pass explicit CPU device to satisfy torch>=2.5 device_id contract (#21774)
* fix(strategy): pass CPU device to FSDP device_id on CPU Passing `device_id=None` (which is `root_device.index` for CPU) triggers a guard in PyTorch >= 2.5 ("FSDP needs a non-CPU accelerator device"). Passing the actual `torch.device("cpu")` avoids this guard and allows FSDP to run on CPU. - Update `FSDPStrategy` in both Fabric and PyTorch to pass `self.root_device` if it is a CPU device, otherwise `self.root_device.index`. - Add unit tests to verify `device_id` selection for CPU and GPU devices. CONV=516b4463-4a64-48ac-aabe-20fc589f838a TAG=agy * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore: update CHANGELOG * refactor(FSDP): address review feedback on device_id CPU fix - common out `device_id` into a local so the debug log reflects the device actually passed (was logging root_device.index = None on CPU) - add call-site comment noting CPU is not a supported FSDP path; the branch only honors the torch>=2.5 device_id contract - replace the parametrized CPU/GPU device_id tests with a single minimal CPU-only assertion per strategy: guards the device_id=None regression without codifying the GPU path as a tested contract - reword CHANGELOG to frame this as a narrow robustness fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * apply copilot suggestion --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Deependu <deependujha21@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent fe6b1cc commit 908064e

6 files changed

Lines changed: 104 additions & 5 deletions

File tree

src/lightning/fabric/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
2020

2121
### Fixed
2222

23+
- Fixed `FSDPStrategy` raising `RuntimeError` under PyTorch 2.5+ when `root_device` is CPU, by passing an explicit `torch.device("cpu")` instead of `device_id=None` (relevant only when the GPU-accelerator guard is bypassed) ([#21774](https://github.com/Lightning-AI/pytorch-lightning/pull/21774))
24+
2325
- Fixed inconsistent FLOPs reporting on NVIDIA H100/H200 GPUs by defaulting to dense FLOPs, with sparse FLOPs now requiring an explicit opt-in. ([#21743](https://github.com/Lightning-AI/pytorch-lightning/pull/21743))
2426

2527
- Fixed AccumulateGrad stream mismatch warning when using DDP with Fabric ([#21746](https://github.com/Lightning-AI/pytorch-lightning/pull/21746))

src/lightning/fabric/strategies/fsdp.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,15 @@ def setup_module(self, module: Module) -> Module:
296296
del self._fsdp_kwargs["auto_wrap_policy"]
297297
else:
298298
_warn_if_shared_params_across_fsdp_units(module, self._fsdp_kwargs.get("auto_wrap_policy"))
299+
# CPU is not a supported FSDP target; this branch only honors the torch>=2.5 contract,
300+
# which rejects device_id=None (root_device.index is None on CPU). The GPU path is unchanged.
301+
device_id = self.root_device if self.root_device.type == "cpu" else self.root_device.index
299302
module = FullyShardedDataParallel(
300303
module=module,
301304
cpu_offload=self.cpu_offload,
302305
mixed_precision=self.mixed_precision_config,
303306
sharding_strategy=self.sharding_strategy,
304-
device_id=self.root_device.index,
307+
device_id=device_id,
305308
**self._fsdp_kwargs,
306309
)
307310

@@ -354,12 +357,15 @@ def module_sharded_context(self) -> AbstractContextManager:
354357
from torch.distributed.fsdp.fully_sharded_data_parallel import FullyShardedDataParallel
355358
from torch.distributed.fsdp.wrap import enable_wrap
356359

360+
# CPU is not a supported FSDP target; this branch only honors the torch>=2.5 contract,
361+
# which rejects device_id=None (root_device.index is None on CPU). The GPU path is unchanged.
362+
device_id = self.root_device if self.root_device.type == "cpu" else self.root_device.index
357363
return enable_wrap(
358364
wrapper_cls=FullyShardedDataParallel,
359365
cpu_offload=self.cpu_offload,
360366
mixed_precision=self.mixed_precision_config,
361367
sharding_strategy=self.sharding_strategy,
362-
device_id=self.root_device.index,
368+
device_id=device_id,
363369
**self._fsdp_kwargs,
364370
)
365371

src/lightning/pytorch/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
2424

2525
### Fixed
2626

27+
- Fixed `FSDPStrategy` raising `RuntimeError` under PyTorch 2.5+ when `root_device` is CPU, by passing an explicit `torch.device("cpu")` instead of `device_id=None` (relevant only when the GPU-accelerator guard is bypassed) ([#21774](https://github.com/Lightning-AI/pytorch-lightning/pull/21774))
28+
2729
- Fixed non-zero process exits in `CombinedLoader.reset()` with large tensors and persistent spawned workers by avoiding explicit `_shutdown_workers()` calls and relying on iterator cleanup via `del` [#21708](https://github.com/Lightning-AI/pytorch-lightning/issues/21708)
2830

2931
- Fixed `SIGTERMException` producing a zero exit code instead of 143 (128 + SIGTERM) ([#21623](https://github.com/Lightning-AI/pytorch-lightning/issues/21623))

src/lightning/pytorch/strategies/fsdp.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,16 @@ def _setup_model(self, model: Module) -> Module:
308308
del self.kwargs["auto_wrap_policy"]
309309
else:
310310
_warn_if_shared_params_across_fsdp_units(model, self.kwargs.get("auto_wrap_policy"))
311-
log.debug(f"setting up FSDP model with device id: {self.root_device.index}, kwargs: {self.kwargs}")
311+
# CPU is not a supported FSDP target; this branch only honors the torch>=2.5 contract,
312+
# which rejects device_id=None (root_device.index is None on CPU). The GPU path is unchanged.
313+
device_id = self.root_device if self.root_device.type == "cpu" else self.root_device.index
314+
log.debug(f"setting up FSDP model with device id: {device_id}, kwargs: {self.kwargs}")
312315
model = FullyShardedDataParallel(
313316
module=model,
314317
cpu_offload=self.cpu_offload,
315318
mixed_precision=self.mixed_precision_config,
316319
sharding_strategy=self.sharding_strategy,
317-
device_id=self.root_device.index,
320+
device_id=device_id,
318321
**self.kwargs,
319322
)
320323

@@ -403,12 +406,15 @@ def model_sharded_context(self) -> Generator[None, None, None]:
403406
from torch.distributed.fsdp.fully_sharded_data_parallel import FullyShardedDataParallel
404407
from torch.distributed.fsdp.wrap import enable_wrap
405408

409+
# CPU is not a supported FSDP target; this branch only honors the torch>=2.5 contract,
410+
# which rejects device_id=None (root_device.index is None on CPU). The GPU path is unchanged.
411+
device_id = self.root_device if self.root_device.type == "cpu" else self.root_device.index
406412
with enable_wrap(
407413
wrapper_cls=FullyShardedDataParallel,
408414
cpu_offload=self.cpu_offload,
409415
mixed_precision=self.mixed_precision_config,
410416
sharding_strategy=self.sharding_strategy,
411-
device_id=self.root_device.index,
417+
device_id=device_id,
412418
**self.kwargs,
413419
):
414420
yield

tests/tests_fabric/strategies/test_fsdp.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,46 @@ def __init__(self):
188188
apply_mock.assert_called_with(wrapped, checkpoint_wrapper_fn=ANY, **strategy._activation_checkpointing_kwargs)
189189

190190

191+
def test_setup_module_device_id_cpu():
192+
"""``setup_module`` passes an explicit ``torch.device('cpu')`` (not ``device_id=None``) on CPU.
193+
194+
``root_device.index`` is ``None`` on CPU; ``device_id=None`` trips torch>=2.5's "FSDP needs a
195+
non-CPU accelerator device" guard. Only reachable when the GPU-accelerator guard is bypassed.
196+
197+
"""
198+
captured = {}
199+
200+
class FakeFSDP(nn.Module):
201+
def __init__(self, module, **kwargs):
202+
super().__init__()
203+
captured.update(kwargs)
204+
self.module = module
205+
206+
strategy = FSDPStrategy()
207+
strategy._parallel_devices = [torch.device("cpu")]
208+
with mock.patch("torch.distributed.fsdp.FullyShardedDataParallel", FakeFSDP):
209+
strategy.setup_module(nn.Linear(2, 2))
210+
assert captured["device_id"] == torch.device("cpu")
211+
212+
213+
def test_module_sharded_context_device_id_cpu():
214+
"""``module_sharded_context`` passes an explicit ``torch.device('cpu')`` (not ``device_id=None``) on CPU."""
215+
from contextlib import contextmanager
216+
217+
captured = {}
218+
219+
@contextmanager
220+
def fake_enable_wrap(*args, **kwargs):
221+
captured.update(kwargs)
222+
yield
223+
224+
strategy = FSDPStrategy()
225+
strategy._parallel_devices = [torch.device("cpu")]
226+
with mock.patch("torch.distributed.fsdp.wrap.enable_wrap", fake_enable_wrap), strategy.module_sharded_context():
227+
pass
228+
assert captured["device_id"] == torch.device("cpu")
229+
230+
191231
def test_forbidden_precision_raises():
192232
with pytest.raises(TypeError, match="can only work with the `FSDPPrecision"):
193233
FSDPStrategy(precision=HalfPrecision())

tests/tests_pytorch/strategies/test_fsdp.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,49 @@ def __init__(self):
446446
apply_mock.assert_called_with(wrapped, checkpoint_wrapper_fn=ANY, **strategy._activation_checkpointing_kwargs)
447447

448448

449+
def test_setup_model_device_id_cpu():
450+
"""``_setup_model`` passes an explicit ``torch.device('cpu')`` (not ``device_id=None``) on CPU.
451+
452+
``root_device.index`` is ``None`` on CPU; ``device_id=None`` trips torch>=2.5's "FSDP needs a
453+
non-CPU accelerator device" guard. Only reachable when the GPU-accelerator guard is bypassed.
454+
455+
"""
456+
captured = {}
457+
458+
class FakeFSDP(nn.Module):
459+
def __init__(self, module, **kwargs):
460+
super().__init__()
461+
captured.update(kwargs)
462+
self.module = module
463+
464+
strategy = FSDPStrategy()
465+
model = nn.Linear(2, 2)
466+
strategy._parallel_devices = [torch.device("cpu")]
467+
strategy._lightning_module = model
468+
strategy._process_group = Mock()
469+
with mock.patch("torch.distributed.fsdp.FullyShardedDataParallel", FakeFSDP):
470+
strategy._setup_model(model)
471+
assert captured["device_id"] == torch.device("cpu")
472+
473+
474+
def test_model_sharded_context_device_id_cpu():
475+
"""``model_sharded_context`` passes an explicit ``torch.device('cpu')`` (not ``device_id=None``) on CPU."""
476+
from contextlib import contextmanager
477+
478+
captured = {}
479+
480+
@contextmanager
481+
def fake_enable_wrap(*args, **kwargs):
482+
captured.update(kwargs)
483+
yield
484+
485+
strategy = FSDPStrategy()
486+
strategy._parallel_devices = [torch.device("cpu")]
487+
with mock.patch("torch.distributed.fsdp.wrap.enable_wrap", fake_enable_wrap), strategy.model_sharded_context():
488+
pass
489+
assert captured["device_id"] == torch.device("cpu")
490+
491+
449492
def test_strategy_cpu_offload():
450493
"""Test the different ways cpu offloading can be enabled."""
451494
# bool

0 commit comments

Comments
 (0)