已合并
Add distributed checkpoint support #9796
chuboning创建于 2024年2月28日
Add distributed checkpoint support #9796
已合并
从refs/pull/9796/head合入到master
共 6 个文件变更+626-31
| @@ -0,0 +1,387 @@ | |||
| 1 | +import os | ||
| 2 | +from typing import cast, List, Optional, Union | ||
| 3 | +import torch | ||
| 4 | +import torch.distributed as dist | ||
| 5 | +import torch.nn | ||
| 6 | +import torch.futures | ||
| 7 | +from torch.futures import Future | ||
| 8 | +from torch.distributed.checkpoint.storage import WriteResult | ||
| 9 | + | ||
| 10 | +from torch.distributed.checkpoint import ( | ||
| 11 | + StorageReader, | ||
| 12 | + StorageWriter, | ||
| 13 | + CheckpointException, | ||
| 14 | + load_state_dict, | ||
| 15 | + save_state_dict, | ||
| 16 | +) | ||
| 17 | +from torch.distributed._shard import sharded_tensor | ||
| 18 | + | ||
| 19 | +from torch.distributed.checkpoint.default_planner import ( | ||
| 20 | + _create_default_local_metadata, | ||
| 21 | +) | ||
| 22 | + | ||
| 23 | +from torch.distributed.checkpoint.metadata import ( | ||
| 24 | + BytesStorageMetadata, | ||
| 25 | + Metadata, | ||
| 26 | + TensorStorageMetadata, | ||
| 27 | +) | ||
| 28 | + | ||
| 29 | +from torch.distributed.checkpoint.planner import ( | ||
| 30 | + SavePlan, | ||
| 31 | + SavePlanner, | ||
| 32 | + LoadPlan, | ||
| 33 | + LoadPlanner, | ||
| 34 | +) | ||
| 35 | + | ||
| 36 | +from torch.distributed._shard.sharded_tensor import ( | ||
| 37 | + state_dict_hook, | ||
| 38 | + ShardedTensor, | ||
| 39 | +) | ||
| 40 | +from torch.distributed._shard.sharding_spec import ChunkShardingSpec | ||
| 41 | +from torch.testing._internal.distributed._shard.sharded_tensor import ( | ||
| 42 | + ShardedTensorTestBase, | ||
| 43 | +) | ||
| 44 | + | ||
| 45 | +import torch_npu | ||
| 46 | +from torch_npu.testing.common_distributed import with_comms, skipIfUnsupportMultiNPU | ||
| 47 | +from torch_npu.testing.testcase import run_tests | ||
| 48 | + | ||
| 49 | + | ||
| 50 | +class TestModule(torch.nn.Module): | ||
| 51 | + def __init__(self) -> None: | ||
| 52 | + super().__init__() | ||
| 53 | + self.sharded: ShardedTensor = sharded_tensor.zeros(self.spec(), 4, 4) | ||
| 54 | + self.regular = torch.nn.Parameter(torch.ones(4, 4)) | ||
| 55 | + self.extra_sharded: Optional[ShardedTensor] = None | ||
| 56 | + self.extra_param: Optional[torch.nn.Parameter] = None | ||
| 57 | + self._register_state_dict_hook(state_dict_hook) | ||
| 58 | + | ||
| 59 | + def spec(self) -> ChunkShardingSpec: | ||
| 60 | + # pyre-fixme [28]: Unexpected keyword argument `dim` to call `dist._sharding_spec.api.ChunkShardingSpec.__init__`. | ||
| 61 | + return ChunkShardingSpec( | ||
| 62 | + dim=0, | ||
| 63 | + placements=[ | ||
| 64 | + "rank:0/npu:0", | ||
| 65 | + "rank:1/npu:1", | ||
| 66 | + ], | ||
| 67 | + ) | ||
| 68 | + | ||
| 69 | + | ||
| 70 | +class TestDistributedCheckpointing(ShardedTensorTestBase): | ||
| 71 | + def destroy_pg(self) -> None: | ||
| 72 | + dist.barrier() | ||
| 73 | + dist.destroy_process_group() | ||
| 74 | + | ||
| 75 | + | ||
| 76 | + def world_size(self) -> int: | ||
| 77 | + return 2 | ||
| 78 | + | ||
| 79 | + | ||
| 80 | + | ||
| 81 | + def test_tensor_metadata_with_missing_rank_spec(self) -> None: | ||
| 82 | + spec = ChunkShardingSpec( | ||
| 83 | + dim=0, | ||
| 84 | + placements=[ | ||
| 85 | + "rank:1/npu:1", | ||
| 86 | + ], | ||
| 87 | + ) | ||
| 88 | + | ||
| 89 | + st = sharded_tensor.zeros(spec, 4, 4, dtype=torch.float64) | ||
| 90 | + mapping = {} | ||
| 91 | + | ||
| 92 | + md = _create_default_local_metadata({"st": st}) | ||
| 93 | + | ||
| 94 | + st_md = md.state_dict_metadata["st"] | ||
| 95 | + self.assertEqual(1, len(st_md.chunks)) | ||
| 96 | + | ||
| 97 | + | ||
| 98 | + | ||
| 99 | + def test_default_metadata(self) -> None: | ||
| 100 | + device = f"npu:{dist.get_rank()}" | ||
| 101 | + spec = ChunkShardingSpec( | ||
| 102 | + dim=0, | ||
| 103 | + placements=[ | ||
| 104 | + "rank:0/npu:0", | ||
| 105 | + "rank:1/npu:1", | ||
| 106 | + ], | ||
| 107 | + ) | ||
| 108 | + | ||
| 109 | + state_dict = { | ||
| 110 | + "sharded": sharded_tensor.rand( | ||
| 111 | + spec, | ||
| 112 | + ( | ||
| 113 | + 10, | ||
| 114 | + 10, | ||
| 115 | + ), | ||
| 116 | + ), | ||
| 117 | + "replicated": torch.rand(4, device=device), | ||
| 118 | + "bytes": [1, 2, 3, 4], | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + metadata = _create_default_local_metadata(state_dict) | ||
| 122 | + self.assertTrue("bytes" in metadata.state_dict_metadata) | ||
| 123 | + self.assertIsInstance( | ||
| 124 | + metadata.state_dict_metadata["bytes"], BytesStorageMetadata | ||
| 125 | + ) | ||
| 126 | + | ||
| 127 | + self.assertTrue("replicated" in metadata.state_dict_metadata) | ||
| 128 | + self.assertIsInstance( | ||
| 129 | + metadata.state_dict_metadata["replicated"], TensorStorageMetadata | ||
| 130 | + ) | ||
| 131 | + md = metadata.state_dict_metadata["replicated"] | ||
| 132 | + self.assertEqual(md.size, state_dict["replicated"].size()) | ||
| 133 | + self.assertEqual(md.properties.dtype, torch.float32) | ||
| 134 | + self.assertEqual(1, len(md.chunks)) | ||
| 135 | + | ||
| 136 | + self.assertTrue("sharded" in metadata.state_dict_metadata) | ||
| 137 | + self.assertIsInstance( | ||
| 138 | + metadata.state_dict_metadata["sharded"], TensorStorageMetadata | ||
| 139 | + ) | ||
| 140 | + md = metadata.state_dict_metadata["sharded"] | ||
| 141 | + self.assertEqual(md.properties.dtype, torch.float32) | ||
| 142 | + self.assertEqual(md.size, state_dict["sharded"].size()) | ||
| 143 | + self.assertEqual(2, len(md.chunks)) | ||
| 144 | + | ||
| 145 | + | ||
| 146 | +class TestStorageBase: | ||
| 147 | + def __init__(self, fail_conf): | ||
| 148 | + self.fail_conf = fail_conf | ||
| 149 | + self.rank = 0 if not dist.is_initialized() else dist.get_rank() | ||
| 150 | + | ||
| 151 | + def _get_ranks(self, name): | ||
| 152 | + return self.fail_conf[name] if name in self.fail_conf else None | ||
| 153 | + | ||
| 154 | + def _fail_rank(self, name): | ||
| 155 | + ranks = self._get_ranks(name) | ||
| 156 | + if ranks is not None and self.rank in ranks: | ||
| 157 | + raise ValueError(f"rank fail {self.rank} for {name}") | ||
| 158 | + | ||
| 159 | + def _fail_rank_async(self, name, result=None): | ||
| 160 | + ranks = self._get_ranks(name) | ||
| 161 | + fut = Future() | ||
| 162 | + if ranks is not None and self.rank in ranks: | ||
| 163 | + fut.set_exception(ValueError(f"async rank fail {self.rank} for {name}")) | ||
| 164 | + else: | ||
| 165 | + fut.set_result(result) | ||
| 166 | + return fut | ||
| 167 | + | ||
| 168 | + | ||
| 169 | +class FaultyStorageWriter(TestStorageBase, StorageWriter): | ||
| 170 | + def __init__(self, fail_conf): | ||
| 171 | + super().__init__(fail_conf) | ||
| 172 | + | ||
| 173 | + def reset(self, checkpoint_id: Union[str, os.PathLike, None] = None) -> None: | ||
| 174 | + return | ||
| 175 | + | ||
| 176 | + def set_up_storage_writer(self, is_coordinator: bool) -> None: | ||
| 177 | + self._fail_rank("fail_set_up_storage_writer") | ||
| 178 | + | ||
| 179 | + def prepare_local_plan(self, plan: SavePlan) -> SavePlan: | ||
| 180 | + self._fail_rank("fail_prepare_local_plan") | ||
| 181 | + return plan | ||
| 182 | + | ||
| 183 | + def prepare_global_plan(self, plans: List[SavePlan]) -> List[SavePlan]: | ||
| 184 | + self._fail_rank("fail_prepare_global_plan") | ||
| 185 | + return plans | ||
| 186 | + | ||
| 187 | + def write_data( | ||
| 188 | + self, plan: SavePlan, planner: SavePlanner | ||
| 189 | + ) -> Future[List[WriteResult]]: | ||
| 190 | + self._fail_rank("fail_write_data") | ||
| 191 | + return self._fail_rank_async("fail_write_data_async", []) | ||
| 192 | + | ||
| 193 | + def finish(self, metadata: Metadata, results: List[List[WriteResult]]) -> None: | ||
| 194 | + self._fail_rank("fail_finish") | ||
| 195 | + | ||
| 196 | + | ||
| 197 | + def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool: | ||
| 198 | + return True | ||
| 199 | + | ||
| 200 | + | ||
| 201 | +class FaultyStorageReader(TestStorageBase, StorageReader): | ||
| 202 | + def __init__(self, metadata, fail_conf): | ||
| 203 | + super().__init__(fail_conf) | ||
| 204 | + self.metadata = metadata | ||
| 205 | + | ||
| 206 | + def reset(self, checkpoint_id: Union[str, os.PathLike, None] = None) -> None: | ||
| 207 | + return | ||
| 208 | + | ||
| 209 | + def set_up_storage_reader(self, metadata: Metadata, is_coordinator: bool) -> None: | ||
| 210 | + self._fail_rank("fail_set_up_storage_reader") | ||
| 211 | + | ||
| 212 | + def prepare_local_plan(self, plan: LoadPlan) -> LoadPlan: | ||
| 213 | + self._fail_rank("fail_prepare_local_plan") | ||
| 214 | + return plan | ||
| 215 | + | ||
| 216 | + def prepare_global_plan(self, plans: List[LoadPlan]) -> List[LoadPlan]: | ||
| 217 | + self._fail_rank("fail_prepare_global_plan") | ||
| 218 | + return plans | ||
| 219 | + | ||
| 220 | + def read_data(self, plan: LoadPlan, planner: LoadPlanner) -> Future[None]: | ||
| 221 | + self._fail_rank("fail_read_data") | ||
| 222 | + return self._fail_rank_async("fail_read_data_async") | ||
| 223 | + | ||
| 224 | + def read_metadata(self) -> Metadata: | ||
| 225 | + self._fail_rank("fail_read_metadata") | ||
| 226 | + return self.metadata | ||
| 227 | + | ||
| 228 | + | ||
| 229 | + def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool: | ||
| 230 | + return True | ||
| 231 | + | ||
| 232 | + | ||
| 233 | +class TestDistributedFailure(ShardedTensorTestBase): | ||
| 234 | + def destroy_pg(self) -> None: | ||
| 235 | + dist.barrier() | ||
| 236 | + dist.destroy_process_group() | ||
| 237 | + | ||
| 238 | + def get_spec(self): | ||
| 239 | + return ChunkShardingSpec( | ||
| 240 | + dim=0, | ||
| 241 | + placements=[ | ||
| 242 | + f"rank:{r}/npu:{r}" for r in range(dist.get_world_size()) | ||
| 243 | + ], | ||
| 244 | + ) | ||
| 245 | + | ||
| 246 | + | ||
| 247 | + | ||
| 248 | + def test_dummy_writer_works(self) -> None: | ||
| 249 | + state_dict = { | ||
| 250 | + "sharded": sharded_tensor.rand(self.get_spec(), 20, 20), | ||
| 251 | + "replicated": torch.rand(10, 10), | ||
| 252 | + "bytes": [1, 2, 3, 4], | ||
| 253 | + } | ||
| 254 | + | ||
| 255 | + save_state_dict(state_dict, FaultyStorageWriter({})) | ||
| 256 | + | ||
| 257 | + | ||
| 258 | + | ||
| 259 | + def test_dummy_reader_works(self) -> None: | ||
| 260 | + state_dict = { | ||
| 261 | + "sharded": sharded_tensor.rand(self.get_spec(), 20, 20), | ||
| 262 | + "replicated": torch.rand(10, 10), | ||
| 263 | + "bytes": [1, 2, 3, 4], | ||
| 264 | + } | ||
| 265 | + metadata = _create_default_local_metadata(state_dict) | ||
| 266 | + | ||
| 267 | + load_state_dict(state_dict, FaultyStorageReader(metadata, {})) | ||
| 268 | + | ||
| 269 | + def _test_dist_failure(self, callback, kwargs): | ||
| 270 | + bad_ranks = next(iter(kwargs.values())) if len(kwargs) > 0 else [] | ||
| 271 | + | ||
| 272 | + # Empty bad_ranks means it must work | ||
| 273 | + if len(bad_ranks) == 0: | ||
| 274 | + callback() | ||
| 275 | + else: | ||
| 276 | + with self.assertRaises(CheckpointException) as cm: | ||
| 277 | + callback() | ||
| 278 | + e = cast(CheckpointException, cm.exception) | ||
| 279 | + for rank, wrapped_ex in e.failures.items(): | ||
| 280 | + ex = wrapped_ex[0] | ||
| 281 | + self.assertTrue(rank in bad_ranks, msg=f"{rank} did not fail") | ||
| 282 | + if not kwargs.get("ignore_exception_type", False): | ||
| 283 | + self.assertEqual(ValueError, type(ex), str(ex)) | ||
| 284 | + | ||
| 285 | + failed_ranks = e.failures.keys() | ||
| 286 | + for rank in bad_ranks: | ||
| 287 | + self.assertTrue( | ||
| 288 | + rank in failed_ranks, | ||
| 289 | + msg=f"{rank} was supposed to fail was fine", | ||
| 290 | + ) | ||
| 291 | + | ||
| 292 | + def _test_save(self, state_dict, coordinator=0, **kwargs): | ||
| 293 | + no_dist = not dist.is_initialized() | ||
| 294 | + | ||
| 295 | + def _save(): | ||
| 296 | + save_state_dict( | ||
| 297 | + state_dict, | ||
| 298 | + storage_writer=FaultyStorageWriter(kwargs), | ||
| 299 | + coordinator_rank=coordinator, | ||
| 300 | + no_dist=no_dist, | ||
| 301 | + ) | ||
| 302 | + | ||
| 303 | + self._test_dist_failure(_save, kwargs) | ||
| 304 | + | ||
| 305 | + def _test_load(self, state_dict, coordinator=0, **kwargs): | ||
| 306 | + no_dist = not dist.is_initialized() | ||
| 307 | + | ||
| 308 | + def _load(): | ||
| 309 | + metadata = _create_default_local_metadata(state_dict) | ||
| 310 | + load_state_dict( | ||
| 311 | + state_dict, | ||
| 312 | + storage_reader=FaultyStorageReader(metadata, kwargs), | ||
| 313 | + coordinator_rank=coordinator, | ||
| 314 | + no_dist=no_dist, | ||
| 315 | + ) | ||
| 316 | + | ||
| 317 | + self._test_dist_failure(_load, kwargs) | ||
| 318 | + | ||
| 319 | + | ||
| 320 | + | ||
| 321 | + def test_save_error_handling(self) -> None: | ||
| 322 | + state_dict = { | ||
| 323 | + "sharded": sharded_tensor.rand(self.get_spec(), 20, 20), | ||
| 324 | + "replicated": torch.rand(10, 10), | ||
| 325 | + "bytes": [1, 2, 3, 4], | ||
| 326 | + } | ||
| 327 | + | ||
| 328 | + self._test_save(state_dict, fail_set_up_storage_writer=[0]) | ||
| 329 | + self._test_save(state_dict, fail_finish=[0]) | ||
| 330 | + self._test_save(state_dict, fail_prepare_global_plan=[0]) | ||
| 331 | + | ||
| 332 | + self._test_save(state_dict, fail_prepare_local_plan=[0]) | ||
| 333 | + self._test_save(state_dict, fail_write_data=[2]) | ||
| 334 | + self._test_save(state_dict, fail_write_data_async=[3]) | ||
| 335 | + | ||
| 336 | + self._test_save(state_dict, coordinator=1, fail_set_up_storage_writer=[1]) | ||
| 337 | + self._test_save(state_dict, coordinator=1, fail_finish=[1]) | ||
| 338 | + | ||
| 339 | + def test_save_error_handling_no_dist(self) -> None: | ||
| 340 | + state_dict = {"replicated": torch.rand(10, 10), "bytes": [1, 2, 3, 4]} | ||
| 341 | + | ||
| 342 | + self.assertFalse(dist.is_initialized()) | ||
| 343 | + | ||
| 344 | + self._test_save(state_dict, fail_set_up_storage_writer=[0]) | ||
| 345 | + self._test_save(state_dict, fail_finish=[0]) | ||
| 346 | + self._test_save(state_dict, fail_prepare_global_plan=[0]) | ||
| 347 | + | ||
| 348 | + self._test_save(state_dict, fail_prepare_local_plan=[0]) | ||
| 349 | + self._test_save(state_dict, fail_write_data=[0]) | ||
| 350 | + self._test_save(state_dict, fail_write_data_async=[0]) | ||
| 351 | + | ||
| 352 | + | ||
| 353 | + | ||
| 354 | + def test_load_error_handling(self) -> None: | ||
| 355 | + state_dict = { | ||
| 356 | + "sharded": sharded_tensor.rand(self.get_spec(), 20, 20), | ||
| 357 | + "replicated": torch.rand(10, 10), | ||
| 358 | + "bytes": [1, 2, 3, 4], | ||
| 359 | + } | ||
| 360 | + | ||
| 361 | + self._test_load(state_dict) | ||
| 362 | + self._test_load(state_dict, fail_set_up_storage_reader=[0]) | ||
| 363 | + self._test_load(state_dict, fail_prepare_global_plan=[0]) | ||
| 364 | + self._test_load(state_dict, fail_read_metadata=[0]) | ||
| 365 | + self._test_load(state_dict, fail_prepare_local_plan=[1]) | ||
| 366 | + self._test_load(state_dict, fail_read_data=[3]) | ||
| 367 | + self._test_load(state_dict, fail_read_data_async=[1]) | ||
| 368 | + | ||
| 369 | + self._test_load(state_dict, coordinator=3, fail_set_up_storage_reader=[0]) | ||
| 370 | + self._test_load(state_dict, coordinator=1, fail_read_metadata=[3]) | ||
| 371 | + self._test_load(state_dict, coordinator=2, fail_read_data=[0]) | ||
| 372 | + self._test_load(state_dict, coordinator=3, fail_read_data_async=[2]) | ||
| 373 | + self._test_load(state_dict, coordinator=1, fail_prepare_global_plan=[1]) | ||
| 374 | + | ||
| 375 | + def test_load_error_handling_no_dist(self) -> None: | ||
| 376 | + state_dict = {"replicated": torch.rand(10, 10), "bytes": [1, 2, 3, 4]} | ||
| 377 | + self._test_load(state_dict) | ||
| 378 | + self._test_load(state_dict, fail_set_up_storage_reader=[0]) | ||
| 379 | + self._test_load(state_dict, fail_read_metadata=[0]) | ||
| 380 | + self._test_load(state_dict, fail_prepare_local_plan=[0]) | ||
| 381 | + self._test_load(state_dict, fail_prepare_global_plan=[0]) | ||
| 382 | + self._test_load(state_dict, fail_read_data=[0]) | ||
| 383 | + self._test_load(state_dict, fail_read_data_async=[0]) | ||
| 384 | + | ||
| 385 | + | ||
| 386 | +if __name__ == "__main__": | ||
| 387 | + run_tests() | ||
| @@ -0,0 +1,97 @@ | |||
| 1 | +import torch | ||
| 2 | + | ||
| 3 | +import torch.distributed.checkpoint as dist_cp | ||
| 4 | +import torch.distributed as dist | ||
| 5 | + | ||
| 6 | +from torch.distributed.fsdp import FullyShardedDataParallel as FSDP | ||
| 7 | +from torch.distributed.fsdp.fully_sharded_data_parallel import StateDictType | ||
| 8 | +from torch.distributed.checkpoint.default_planner import ( | ||
| 9 | + DefaultSavePlanner, | ||
| 10 | + DefaultLoadPlanner, | ||
| 11 | +) | ||
| 12 | + | ||
| 13 | +from torch.testing._internal.distributed._tensor.common_dtensor import ( | ||
| 14 | + DTensorTestBase, | ||
| 15 | +) | ||
| 16 | +from torch.testing._internal.distributed.checkpoint_utils import with_temp_dir | ||
| 17 | + | ||
| 18 | +import torch_npu | ||
| 19 | +from torch_npu.testing.common_distributed import with_comms, skipIfUnsupportMultiNPU | ||
| 20 | +from torch_npu.testing.testcase import run_tests | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +class FsdpModelStateCheckpoint(DTensorTestBase): | ||
| 24 | + def _test_fsdp_model_state(self, process_group) -> None: | ||
| 25 | + CHECKPOINT_DIR = self.temp_dir | ||
| 26 | + | ||
| 27 | + model = FSDP(torch.nn.Linear(8, 8, device="npu")) | ||
| 28 | + model(torch.rand(8, 8, device="npu")).sum().backward() | ||
| 29 | + | ||
| 30 | + with FSDP.state_dict_type(model, StateDictType.SHARDED_STATE_DICT): | ||
| 31 | + state_dict = { | ||
| 32 | + "model": model.state_dict(), | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + dist_cp.save_state_dict( | ||
| 36 | + state_dict=state_dict, | ||
| 37 | + storage_writer=dist_cp.FileSystemWriter(CHECKPOINT_DIR), | ||
| 38 | + planner=DefaultSavePlanner(), | ||
| 39 | + ) | ||
| 40 | + | ||
| 41 | + model_2 = FSDP( | ||
| 42 | + torch.nn.Linear(8, 8, device="npu"), process_group=process_group | ||
| 43 | + ) | ||
| 44 | + | ||
| 45 | + with FSDP.summon_full_params(model): | ||
| 46 | + with FSDP.summon_full_params(model_2): | ||
| 47 | + self.assertNotEqual(model.weight, model_2.weight) | ||
| 48 | + self.assertNotEqual(model.bias, model_2.bias) | ||
| 49 | + | ||
| 50 | + # now load the model and ensure the values are the same | ||
| 51 | + with FSDP.state_dict_type(model_2, StateDictType.SHARDED_STATE_DICT): | ||
| 52 | + state_dict = { | ||
| 53 | + "model": model_2.state_dict(), | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + dist_cp.load_state_dict( | ||
| 57 | + state_dict=state_dict, | ||
| 58 | + storage_reader=dist_cp.FileSystemReader(CHECKPOINT_DIR), | ||
| 59 | + planner=DefaultLoadPlanner(), | ||
| 60 | + ) | ||
| 61 | + model_2.load_state_dict(state_dict["model"]) | ||
| 62 | + | ||
| 63 | + with FSDP.summon_full_params(model): | ||
| 64 | + with FSDP.summon_full_params(model_2): | ||
| 65 | + self.assertEqual(model.weight, model_2.weight) | ||
| 66 | + self.assertEqual(model.bias, model_2.bias) | ||
| 67 | + | ||
| 68 | + | ||
| 69 | + | ||
| 70 | + | ||
| 71 | + def test_fsdp_model_state_no_resharding(self): | ||
| 72 | + self._test_fsdp_model_state(process_group=None) | ||
| 73 | + | ||
| 74 | + def _create_new_dist_group(self): | ||
| 75 | + world_size = dist.get_world_size() | ||
| 76 | + group1 = [i for i in range(world_size) if i % 2 == 0] | ||
| 77 | + group2 = [i for i in range(world_size) if i % 2 != 0] | ||
| 78 | + | ||
| 79 | + # create new fsdp group for resharding | ||
| 80 | + fsdp_0 = dist.new_group(ranks=group1) | ||
| 81 | + fsdp_1 = dist.new_group(ranks=group2) | ||
| 82 | + if dist.get_rank() % 2 == 0: | ||
| 83 | + my_fsdp = fsdp_0 | ||
| 84 | + else: | ||
| 85 | + my_fsdp = fsdp_1 | ||
| 86 | + | ||
| 87 | + return my_fsdp | ||
| 88 | + | ||
| 89 | + | ||
| 90 | + | ||
| 91 | + | ||
| 92 | + def test_fsdp_model_state_with_resharding(self): | ||
| 93 | + self._test_fsdp_model_state(process_group=self._create_new_dist_group()) | ||
| 94 | + | ||
| 95 | + | ||
| 96 | +if __name__ == "__main__": | ||
| 97 | + run_tests() | ||
| @@ -0,0 +1,102 @@ | |||
| 1 | +import torch | ||
| 2 | + | ||
| 3 | +import torch.distributed.checkpoint as dist_cp | ||
| 4 | +import torch.distributed as dist | ||
| 5 | + | ||
| 6 | +from torch.distributed.fsdp import FullyShardedDataParallel as FSDP | ||
| 7 | +from torch.distributed.fsdp.fully_sharded_data_parallel import StateDictType | ||
| 8 | +from torch.distributed.checkpoint.default_planner import ( | ||
| 9 | + DefaultSavePlanner, | ||
| 10 | + DefaultLoadPlanner, | ||
| 11 | +) | ||
| 12 | + | ||
| 13 | +from torch.testing._internal.distributed._tensor.common_dtensor import ( | ||
| 14 | + DTensorTestBase, | ||
| 15 | +) | ||
| 16 | +from torch.testing._internal.distributed.checkpoint_utils import with_temp_dir | ||
| 17 | + | ||
| 18 | +import torch_npu | ||
| 19 | +from torch_npu.testing.common_distributed import with_comms, skipIfUnsupportMultiNPU | ||
| 20 | +from torch_npu.testing.testcase import run_tests | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +class FsdpOptimStateCheckpoint(DTensorTestBase): | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + def test_distributed_tensor_planner(self) -> None: | ||
| 28 | + CHECKPOINT_DIR = self.temp_dir | ||
| 29 | + | ||
| 30 | + model = FSDP(torch.nn.Linear(8, 8, device="npu")) | ||
| 31 | + optim = torch.optim.Adam(model.parameters(), lr=0.1) | ||
| 32 | + | ||
| 33 | + model(torch.rand(8, 8, device="npu")).sum().backward() | ||
| 34 | + optim.step() | ||
| 35 | + | ||
| 36 | + with FSDP.state_dict_type(model, StateDictType.SHARDED_STATE_DICT): | ||
| 37 | + state_dict = { | ||
| 38 | + "model": model.state_dict(), | ||
| 39 | + "optim": FSDP.optim_state_dict(model, optim), | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + dist_cp.save_state_dict( | ||
| 43 | + state_dict=state_dict, | ||
| 44 | + storage_writer=dist_cp.FileSystemWriter(CHECKPOINT_DIR), | ||
| 45 | + planner=DefaultSavePlanner(), | ||
| 46 | + ) | ||
| 47 | + | ||
| 48 | + # now load the model and ensure the values are the same | ||
| 49 | + model_2 = FSDP(torch.nn.Linear(8, 8, device="npu")) | ||
| 50 | + optim_2 = torch.optim.Adam(model_2.parameters(), lr=0.1) | ||
| 51 | + | ||
| 52 | + with FSDP.summon_full_params(model): | ||
| 53 | + with FSDP.summon_full_params(model_2): | ||
| 54 | + self.assertNotEqual(model.weight, model_2.weight) | ||
| 55 | + self.assertNotEqual(model.bias, model_2.bias) | ||
| 56 | + | ||
| 57 | + # Adam lazily creates its state | ||
| 58 | + self.assertEqual(0, len(optim_2.state)) | ||
| 59 | + | ||
| 60 | + with FSDP.state_dict_type(model_2, StateDictType.SHARDED_STATE_DICT): | ||
| 61 | + state_dict = { | ||
| 62 | + "model": model_2.state_dict(), | ||
| 63 | + # cannot load the optimizer together with the model | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + dist_cp.load_state_dict( | ||
| 67 | + state_dict=state_dict, | ||
| 68 | + storage_reader=dist_cp.FileSystemReader(CHECKPOINT_DIR), | ||
| 69 | + planner=DefaultLoadPlanner(), | ||
| 70 | + ) | ||
| 71 | + model_2.load_state_dict(state_dict["model"]) | ||
| 72 | + | ||
| 73 | + optim_state = torch.distributed.checkpoint.optimizer.load_sharded_optimizer_state_dict( | ||
| 74 | + model_state_dict=state_dict["model"], | ||
| 75 | + optimizer_key="optim", | ||
| 76 | + storage_reader=dist_cp.FileSystemReader(CHECKPOINT_DIR), | ||
| 77 | + ) | ||
| 78 | + | ||
| 79 | + flattened_osd = FSDP.optim_state_dict_to_load( | ||
| 80 | + model_2, optim_2, optim_state["optim"] | ||
| 81 | + ) | ||
| 82 | + optim_2.load_state_dict(flattened_osd) | ||
| 83 | + | ||
| 84 | + with FSDP.summon_full_params(model): | ||
| 85 | + with FSDP.summon_full_params(model_2): | ||
| 86 | + self.assertEqual(model.weight, model_2.weight) | ||
| 87 | + self.assertEqual(model.bias, model_2.bias) | ||
| 88 | + | ||
| 89 | + def opt_at(opt, idx): | ||
| 90 | + return list(iter(opt.state.values()))[idx] | ||
| 91 | + | ||
| 92 | + # Adam lazily creates its state | ||
| 93 | + self.assertEqual( | ||
| 94 | + opt_at(optim, 0)["exp_avg"], opt_at(optim_2, 0)["exp_avg"] | ||
| 95 | + ) | ||
| 96 | + self.assertEqual( | ||
| 97 | + opt_at(optim, 0)["exp_avg_sq"], opt_at(optim_2, 0)["exp_avg_sq"] | ||
| 98 | + ) | ||
| 99 | + | ||
| 100 | + | ||
| 101 | +if __name__ == "__main__": | ||
| 102 | + run_tests() | ||
| @@ -59,46 +59,53 @@ static inline at::Tensor to_impl_npu( | |||
| 59 | } | 59 | } |
| 60 | 60 | ||
| 61 | at::Tensor NPUNativeFunctions::to( | 61 | at::Tensor NPUNativeFunctions::to( |
| 62 | - const at::Tensor& self, | 62 | + const at::Tensor &self, |
| 63 | c10::optional<at::ScalarType> dtype, | 63 | c10::optional<at::ScalarType> dtype, |
| 64 | c10::optional<c10::Layout> layout, | 64 | c10::optional<c10::Layout> layout, |
| 65 | c10::optional<c10::Device> device, | 65 | c10::optional<c10::Device> device, |
| 66 | c10::optional<bool> pin_memory, | 66 | c10::optional<bool> pin_memory, |
| 67 | bool non_blocking, | 67 | bool non_blocking, |
| 68 | bool copy, | 68 | bool copy, |
| 69 | - c10::optional<c10::MemoryFormat> optional_memory_format) { | 69 | + c10::optional<c10::MemoryFormat> optional_memory_format) |
| 70 | - TORCH_CHECK( | 70 | +{ |
| 71 | - !optional_memory_format.has_value(), | 71 | + if (device.has_value() && device.value().is_cpu() && optional_memory_format.has_value()) { |
| 72 | - "NPU not support specify memory_format."); | 72 | + TORCH_CHECK( |
| 73 | - c10::TensorOptions options_ = c10::TensorOptions().dtype(dtype) | 73 | + optional_memory_format.value() == c10::MemoryFormat::Preserve || |
| 74 | - .layout(layout) | 74 | + optional_memory_format.value() == c10::MemoryFormat::Contiguous, |
| 75 | - .device(device); | 75 | + "Only contiguous_format or preserve_format is supported."); |
| 76 | - TORCH_CHECK( | 76 | + } else { |
| 77 | - !(options_.has_memory_format() && optional_memory_format.has_value()), | 77 | + TORCH_CHECK( |
| 78 | - "Cannot set memory_format both in c10::TensorOptions and explicit argument; please delete " | 78 | + !optional_memory_format.has_value(), |
| 79 | - "the redundant setter."); | 79 | + "NPU not support specify memory_format."); |
| 80 | - auto options = | 80 | + } |
| 81 | - options_.merge_in(c10::TensorOptions().memory_format(optional_memory_format)); | ||
| 82 | 81 | ||
| 83 | - TORCH_CHECK( | 82 | + c10::TensorOptions options_ = c10::TensorOptions().dtype(dtype).layout(layout).device(device); |
| 84 | - options.requires_grad_opt() == c10::nullopt, | 83 | + TORCH_CHECK( |
| 85 | - "to(options) expects unset requires_grad flag, but got " | 84 | + !(options_.has_memory_format() && optional_memory_format.has_value()), |
| 86 | - "options.requires_grad set as ", | 85 | + "Cannot set memory_format both in c10::TensorOptions and explicit argument; please delete " |
| 87 | - options.requires_grad()); | 86 | + "the redundant setter."); |
| 87 | + auto options = | ||
| 88 | + options_.merge_in(c10::TensorOptions().memory_format(optional_memory_format)); | ||
| 88 | 89 | ||
| 89 | - TORCH_CHECK( | 90 | + TORCH_CHECK( |
| 90 | - !options.has_layout() || self.layout() == options.layout(), | 91 | + options.requires_grad_opt() == c10::nullopt, |
| 91 | - "to(options) doesn't support converting to a different layout, " | 92 | + "to(options) expects unset requires_grad flag, but got " |
| 92 | - "but got self.layout being ", | 93 | + "options.requires_grad set as ", |
| 93 | - self.layout(), | 94 | + options.requires_grad()); |
| 94 | - " and options.layout set as ", | ||
| 95 | - options.layout()); | ||
| 96 | 95 | ||
| 97 | - if (options.has_device()) { | 96 | + TORCH_CHECK( |
| 98 | - options = options.device(ensure_has_index(options.device())); | 97 | + !options.has_layout() || self.layout() == options.layout(), |
| 99 | - } | 98 | + "to(options) doesn't support converting to a different layout, " |
| 100 | - auto specified_options = self.options().merge_in(options); | 99 | + "but got self.layout being ", |
| 101 | - return to_impl_npu(self, specified_options, non_blocking, copy); | 100 | + self.layout(), |
| 101 | + " and options.layout set as ", | ||
| 102 | + options.layout()); | ||
| 103 | + | ||
| 104 | + if (options.has_device()) { | ||
| 105 | + options = options.device(ensure_has_index(options.device())); | ||
| 106 | + } | ||
| 107 | + auto specified_options = self.options().merge_in(options); | ||
| 108 | + return to_impl_npu(self, specified_options, non_blocking, copy); | ||
| 102 | } | 109 | } |
| 103 | 110 | ||
| 104 | at::Tensor NPUNativeFunctions::to( | 111 | at::Tensor NPUNativeFunctions::to( |
| @@ -76,6 +76,7 @@ def init_pg(backend: str = "hccl", world_size=1, rank=0, file_name="file://") -> | |||
| 76 | raise RuntimeError(f"Backend {backend} not supported!") | 76 | raise RuntimeError(f"Backend {backend} not supported!") |
| 77 | 77 | ||
| 78 | dist.init_process_group( | 78 | dist.init_process_group( |
| 79 | + backend=backend, | ||
| 79 | world_size=world_size, | 80 | world_size=world_size, |
| 80 | rank=rank, # pyre-ignore[16] | 81 | rank=rank, # pyre-ignore[16] |
| 81 | init_method=f"file://{file_name}", # pyre-ignore[16] | 82 | init_method=f"file://{file_name}", # pyre-ignore[16] |
| @@ -58,3 +58,4 @@ def add_storage_methods(): | |||
| 58 | torch.storage.UntypedStorage.cpu = _cpu | 58 | torch.storage.UntypedStorage.cpu = _cpu |
| 59 | torch.storage.TypedStorage._deepcopy = _deepcopy | 59 | torch.storage.TypedStorage._deepcopy = _deepcopy |
| 60 | torch.storage.TypedStorage.resize_ = _resize | 60 | torch.storage.TypedStorage.resize_ = _resize |
| 61 | + torch.storage.TypedStorage._resize_ = _resize | ||