diff --git a/src/lightning/fabric/strategies/fsdp.py b/src/lightning/fabric/strategies/fsdp.py index 95e44b0dd553c..2edcd47b13ce9 100644 --- a/src/lightning/fabric/strategies/fsdp.py +++ b/src/lightning/fabric/strategies/fsdp.py @@ -895,8 +895,11 @@ def _get_full_state_dict_context( from torch.distributed.fsdp import FullyShardedDataParallel as FSDP from torch.distributed.fsdp.api import FullOptimStateDictConfig - state_dict_config = FullStateDictConfig(offload_to_cpu=True, rank0_only=rank0_only) - optim_state_dict_config = FullOptimStateDictConfig(offload_to_cpu=True, rank0_only=rank0_only) + # Offloading to CPU when FSDP is on CPU triggers a use-after-free in PyTorch's FlatParamHandle.to_cpu(). + param = next(module.parameters(), None) + offload_to_cpu = param is None or param.device.type != "cpu" + state_dict_config = FullStateDictConfig(offload_to_cpu=offload_to_cpu, rank0_only=rank0_only) + optim_state_dict_config = FullOptimStateDictConfig(offload_to_cpu=offload_to_cpu, rank0_only=rank0_only) state_dict_type_context = FSDP.state_dict_type( module=module, state_dict_type=StateDictType.FULL_STATE_DICT, diff --git a/tests/tests_fabric/strategies/test_fsdp.py b/tests/tests_fabric/strategies/test_fsdp.py index 990fdf4845344..37c844a882c6b 100644 --- a/tests/tests_fabric/strategies/test_fsdp.py +++ b/tests/tests_fabric/strategies/test_fsdp.py @@ -433,17 +433,24 @@ def test_set_timeout(init_process_group_mock): @mock.patch("torch.distributed.fsdp.fully_sharded_data_parallel.FullyShardedDataParallel.set_state_dict_type") def test_get_full_state_dict_context_offload(set_type_mock, monkeypatch): - """Test that the state dict context manager handles CPU offloading.""" - - with _get_full_state_dict_context(module=Mock(spec=FullyShardedDataParallel), world_size=1): + """Test that the state dict context manager only offloads to CPU when the shards live on an accelerator.""" + # Shards on accelerator: offload to CPU. + accelerator_param = Mock() + accelerator_param.device = torch.device("cuda", 0) + module = Mock(spec=FullyShardedDataParallel) + module.parameters = Mock(return_value=iter([accelerator_param])) + with _get_full_state_dict_context(module=module, world_size=4): assert set_type_mock.call_args_list[0][0][2].offload_to_cpu # model config assert set_type_mock.call_args_list[0][0][3].offload_to_cpu # optim config set_type_mock.reset_mock() - with _get_full_state_dict_context(module=Mock(spec=FullyShardedDataParallel), world_size=4): - assert set_type_mock.call_args_list[0][0][2].offload_to_cpu # model config - assert set_type_mock.call_args_list[0][0][3].offload_to_cpu # optim config + # Shards on CPU: do not offload (prevents PyTorch use-after-free). + module = Mock(spec=FullyShardedDataParallel) + module.parameters = Mock(return_value=iter([torch.nn.Parameter(torch.zeros(1))])) + with _get_full_state_dict_context(module=module, world_size=4): + assert not set_type_mock.call_args_list[0][0][2].offload_to_cpu # model config + assert not set_type_mock.call_args_list[0][0][3].offload_to_cpu # optim config def test_device_mesh_type_annotation():