1111# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212# See the License for the specific language governing permissions and
1313# limitations under the License.
14- import shutil
1514import warnings
1615from collections .abc import Generator
1716from contextlib import AbstractContextManager , ExitStack , nullcontext
4948 _Sharded ,
5049 _validate_keys_for_strict_loading ,
5150)
51+ from lightning .fabric .utilities .cloud_io import (
52+ _atomic_save ,
53+ _checkpoint_join ,
54+ _is_checkpoint_dir ,
55+ _is_local_file_protocol ,
56+ _load ,
57+ _prepare_directory_checkpoint ,
58+ _remove_checkpoint ,
59+ _resolve_path ,
60+ get_filesystem ,
61+ )
5262from lightning .fabric .utilities .distributed import (
5363 ReduceOp ,
5464 _distributed_is_initialized ,
@@ -439,8 +449,9 @@ def save_checkpoint(
439449 )
440450
441451 # broadcast the path from rank 0 to ensure all the states are saved in a common path
442- path = Path (self .broadcast (path ))
443- if path .is_dir () and self ._state_dict_type == "full" and not _is_sharded_checkpoint (path ):
452+ # remote URLs are kept as strings; only local paths are wrapped in `Path`
453+ path = _resolve_path (self .broadcast (path ))
454+ if self ._state_dict_type == "full" and _is_checkpoint_dir (path ) and not _is_sharded_checkpoint (path ):
444455 raise IsADirectoryError (f"The checkpoint path exists and is a directory: { path } " )
445456
446457 from torch .distributed .fsdp import FullyShardedDataParallel as FSDP
@@ -461,9 +472,7 @@ def save_checkpoint(
461472 module = modules [0 ]
462473
463474 if self ._state_dict_type == "sharded" :
464- if path .is_file ():
465- path .unlink ()
466- path .mkdir (parents = True , exist_ok = True )
475+ _prepare_directory_checkpoint (path )
467476
468477 state_dict_ctx = _get_sharded_state_dict_context (module )
469478
@@ -488,11 +497,11 @@ def save_checkpoint(
488497 _distributed_checkpoint_save (converted_state , path )
489498
490499 if self .global_rank == 0 :
491- torch . save (metadata , path / _METADATA_FILENAME )
500+ _atomic_save (metadata , _checkpoint_join ( path , _METADATA_FILENAME ) )
492501
493502 elif self ._state_dict_type == "full" :
494503 if _is_sharded_checkpoint (path ):
495- shutil . rmtree (path )
504+ _remove_checkpoint (path )
496505
497506 state_dict_ctx = _get_full_state_dict_context (module , world_size = self .world_size )
498507 full_state : dict [str , Any ] = {}
@@ -507,7 +516,7 @@ def save_checkpoint(
507516 _apply_filter (key , filter or {}, converted , full_state )
508517
509518 if self .global_rank == 0 :
510- torch . save (full_state , path )
519+ _atomic_save (full_state , path )
511520 else :
512521 raise ValueError (f"Unknown state_dict_type: { self ._state_dict_type } " )
513522
@@ -527,7 +536,7 @@ def load_checkpoint(
527536 " FSDPStrategy.load_checkpoint(..., state={'model': model, ...})"
528537 )
529538 # broadcast the path from rank 0 to ensure all the states are loaded from a common path
530- path = Path (self .broadcast (path ))
539+ path = _resolve_path (self .broadcast (path ))
531540
532541 if isinstance (state , Module ):
533542 from lightning .fabric .strategies .model_parallel import _load_raw_module_state_from_path
@@ -568,11 +577,9 @@ def load_checkpoint(
568577 module .load_state_dict (module_state [module_key ], strict = strict )
569578
570579 if optimizers :
571- from torch .distributed .checkpoint import FileSystemReader
572-
573580 # TODO: replace with newer APIs
574581 # https://github.com/pytorch/pytorch/issues/119800#issuecomment-1942156271
575- reader = FileSystemReader ( path = path )
582+ reader = _get_distributed_checkpoint_reader ( path )
576583 # the optimizer states must be loaded separately
577584 for optim_key , optim in optimizers .items ():
578585 optim_state = load_sharded_optimizer_state_dict (
@@ -588,7 +595,7 @@ def load_checkpoint(
588595 optim .load_state_dict (flattened_osd )
589596
590597 # Load metadata (anything not a module or optimizer)
591- metadata = torch . load ( path / _METADATA_FILENAME , weights_only = weights_only )
598+ metadata = _load ( _checkpoint_join ( path , _METADATA_FILENAME ) , weights_only = weights_only )
592599 requested_metadata_keys = state .keys () - modules .keys () - optimizers .keys ()
593600 _validate_keys_for_strict_loading (requested_metadata_keys , metadata .keys (), strict = strict )
594601 for key in requested_metadata_keys :
@@ -600,7 +607,7 @@ def load_checkpoint(
600607 return metadata
601608
602609 if _is_full_checkpoint (path ):
603- checkpoint = _lazy_load (path )
610+ checkpoint = _lazy_load (path ) if _is_local_file_protocol ( str ( path )) else _load ( path )
604611
605612 from lightning .fabric .strategies .model_parallel import (
606613 _load_raw_module_state ,
@@ -901,13 +908,19 @@ def _get_full_state_dict_context(
901908 return state_dict_type_context # type: ignore[return-value]
902909
903910
904- def _is_sharded_checkpoint (path : Path ) -> bool :
911+ def _is_sharded_checkpoint (path : _PATH ) -> bool :
905912 """A heuristic check to determine whether the path points to a directory with checkpoint shards."""
906- return path .is_dir () and (path / _METADATA_FILENAME ).is_file ()
913+ if _is_local_file_protocol (str (path )):
914+ path = Path (path )
915+ return path .is_dir () and (path / _METADATA_FILENAME ).is_file ()
916+ fs = get_filesystem (path )
917+ return fs .isfile (str (path ).rstrip ("/" ) + "/" + _METADATA_FILENAME )
907918
908919
909- def _is_full_checkpoint (path : Path ) -> bool :
910- return path .is_file ()
920+ def _is_full_checkpoint (path : _PATH ) -> bool :
921+ if _is_local_file_protocol (str (path )):
922+ return Path (path ).is_file ()
923+ return get_filesystem (path ).isfile (str (path ))
911924
912925
913926def _has_fsdp_modules (module : object ) -> TypeGuard [Module ]:
@@ -928,38 +941,54 @@ def _move_torchmetrics_to_device(module: torch.nn.Module, device: torch.device)
928941 metric .to (device ) # `.to()` is in-place
929942
930943
931- def _distributed_checkpoint_save (converted_state : dict [str , Any ], path : Path ) -> None :
944+ def _get_distributed_checkpoint_writer (path : _PATH ) -> Any :
945+ if _is_local_file_protocol (str (path )):
946+ from torch .distributed .checkpoint import FileSystemWriter
947+
948+ # FSDP's FileSystemWriter streams the tensors to disk to minimize memory peaks
949+ return FileSystemWriter (path = path , single_file_per_rank = True )
950+ from torch .distributed .checkpoint ._fsspec_filesystem import FsspecWriter
951+
952+ return FsspecWriter (path = str (path ), single_file_per_rank = True )
953+
954+
955+ def _get_distributed_checkpoint_reader (path : _PATH ) -> Any :
956+ if _is_local_file_protocol (str (path )):
957+ from torch .distributed .checkpoint import FileSystemReader
958+
959+ return FileSystemReader (path = path )
960+ from torch .distributed .checkpoint ._fsspec_filesystem import FsspecReader
961+
962+ return FsspecReader (path = str (path ))
963+
964+
965+ def _distributed_checkpoint_save (converted_state : dict [str , Any ], path : _PATH ) -> None :
932966 if _TORCH_GREATER_EQUAL_2_3 :
933967 from torch .distributed .checkpoint import save
934968
935- # let torch automatically infer the writer to use. This might also support fsspec paths in the future
936- # https://github.com/pytorch/pytorch/issues/118036
969+ # torch automatically infers the writer to use from the checkpoint_id, including
970+ # FsspecWriter for remote (fsspec) URLs. https://github.com/pytorch/pytorch/issues/118036
937971 save (converted_state , checkpoint_id = path )
938972 else : # deprecated
939- from torch .distributed .checkpoint import FileSystemWriter
940-
941973 if _TORCH_GREATER_EQUAL_2_2 :
942974 from torch .distributed .checkpoint import save
943975 else :
944976 from torch .distributed .checkpoint import save_state_dict as save
945- # FSDP's FileSystemWriter streams the tensors to disk to minimize memory peaks
946- writer = FileSystemWriter (path = path , single_file_per_rank = True )
977+ writer = _get_distributed_checkpoint_writer (path )
947978 save (converted_state , writer )
948979
949980
950- def _distributed_checkpoint_load (module_state : dict [str , Any ], path : Path ) -> None :
981+ def _distributed_checkpoint_load (module_state : dict [str , Any ], path : _PATH ) -> None :
951982 if _TORCH_GREATER_EQUAL_2_3 :
952983 from torch .distributed .checkpoint import load
953984
954- # let torch automatically infer the reader to use. This might also support fsspec paths in the future
955- # https://github.com/pytorch/pytorch/issues/118036
985+ # torch automatically infers the reader to use from the checkpoint_id, including
986+ # FsspecReader for remote (fsspec) URLs. https://github.com/pytorch/pytorch/issues/118036
956987 load (module_state , checkpoint_id = path )
957988 else : # deprecated
958- from torch .distributed .checkpoint import FileSystemReader
959-
960989 if _TORCH_GREATER_EQUAL_2_2 :
961990 from torch .distributed .checkpoint import load
962991 else :
963992 from torch .distributed .checkpoint import load_state_dict as load
964- reader = FileSystemReader ( path = path )
993+ reader = _get_distributed_checkpoint_reader ( path )
965994 load (module_state , reader )
0 commit comments