@@ -377,11 +377,14 @@ def train(self):
377377 self ._save_checkpoint ()
378378 checkpoint_saved = True
379379 finally :
380+ fsdp2 = getattr (self ._algorithm , '_distributed_strategy' ,
381+ 'ddp' ) == 'fsdp2'
380382 if (self ._config .save_checkpoint_upon_crash
381- and not checkpoint_saved and self ._rank <= 0 ):
383+ and not checkpoint_saved and self ._rank <= 0
384+ and not fsdp2 ):
382385 self ._save_checkpoint ()
383386 elif (self ._config .confirm_checkpoint_upon_crash
384- and not checkpoint_saved and self ._rank <= 0 ):
387+ and not checkpoint_saved and self ._rank <= 0 and not fsdp2 ):
385388 # Prompts for checkpoint only when running single process
386389 # training (rank is -1) or master process of DDP training (rank
387390 # is 0).
@@ -516,12 +519,23 @@ def _save_checkpoint(self):
516519 # training (rank is -1) or the master process of DDP training (rank is
517520 # 0). Other DDP ranks only save their local replay buffers.
518521 global_step = alf .summary .get_global_counter ()
522+ from alf .utils .distributed import (fsdp2_full_state_dict ,
523+ is_fsdp2_module )
524+ fsdp2 = is_fsdp2_module (self ._algorithm )
525+ algorithm_state = None
526+ if fsdp2 :
527+ # Full FSDP2 state gathering is collective even though only rank 0
528+ # writes the resulting checkpoint.
529+ algorithm_state = fsdp2_full_state_dict (self ._algorithm )
519530 if self ._rank <= 0 :
520531 # Replay buffers are saved separately below as sharded per-rank
521532 # source files, so rank 0's full checkpoint only contains model,
522533 # optimizer, metrics, and trainer progress.
523- self ._checkpointer .save (global_step = global_step ,
524- including_replay_buffer = False )
534+ self ._checkpointer .save (
535+ global_step = global_step ,
536+ including_replay_buffer = False ,
537+ state_overrides = {'algorithm' : algorithm_state }
538+ if fsdp2 else None )
525539 # Every rank writes one sharded replay-buffer source file into the
526540 # checkpoint directory. Restore will redistribute these files across the
527541 # active worker count, which may differ from the save-time worker count.
@@ -588,7 +602,8 @@ class RLTrainer(Trainer):
588602 def __init__ (self ,
589603 config : TrainerConfig ,
590604 ddp_rank : int = - 1 ,
591- algorithm_wrapper_ctor : Callable = None ):
605+ algorithm_wrapper_ctor : Callable = None ,
606+ distributed_strategy : str = 'ddp' ):
592607 """
593608
594609 Args:
@@ -599,6 +614,7 @@ def __init__(self,
599614 process training.
600615 algorithm_wrapper_ctor: if not None, will be used to wrap
601616 ``self._algorithm_ctor`` before creating ``self._algorithm``.
617+ distributed_strategy: ``'ddp'`` or ``'fsdp2'``.
602618 """
603619 super ().__init__ (config , ddp_rank )
604620
@@ -669,7 +685,7 @@ def __init__(self,
669685 self ._algorithm .set_path ('' )
670686 if ddp_rank >= 0 :
671687 # Activate the DDP training
672- self ._algorithm .activate_ddp (ddp_rank )
688+ self ._algorithm .activate_ddp (ddp_rank , distributed_strategy )
673689 # Make sure the BN statistics of different processes are synced
674690 # https://pytorch.org/docs/stable/generated/torch.nn.SyncBatchNorm.html#torch.nn.SyncBatchNorm
675691 # This conversion needs to be performed before wrapping modules with DDP.
@@ -763,7 +779,24 @@ def _train(self):
763779 iter_num += 1
764780 self ._trainer_progress .update (iter_num , total_time_steps )
765781
766- if self ._need_to_evaluate (iter_num ):
782+ need_to_evaluate = self ._need_to_evaluate (iter_num )
783+ from alf .utils .distributed import is_fsdp2_module
784+ if is_fsdp2_module (self ._algorithm ):
785+ # Only rank 0 owns an evaluator, but FSDP2 parameter gathering
786+ # is collective. Broadcast rank 0's decision so every rank
787+ # enters the gather/reshard operations in the same order.
788+ evaluate_flag = torch .tensor (int (need_to_evaluate ),
789+ device = alf .get_default_device ())
790+ torch .distributed .broadcast (evaluate_flag , src = 0 )
791+ if bool (evaluate_flag ):
792+ performer = self ._algorithm ._fsdp2_performer
793+ performer .unshard ()
794+ if self ._rank == 0 :
795+ self ._eval ()
796+ self ._num_evals_performed += 1
797+ torch .distributed .barrier ()
798+ performer .reshard ()
799+ elif need_to_evaluate :
767800 self ._eval ()
768801 self ._num_evals_performed += 1
769802
@@ -837,6 +870,11 @@ def _check_dpp_paras_consistency(self, iter_num: int,
837870 if not training_started :
838871 return
839872
873+ # FSDP2 ranks intentionally hold different parameter shards, so the
874+ # replicated-parameter consistency check is not applicable.
875+ if getattr (self ._algorithm , '_distributed_strategy' , 'ddp' ) == 'fsdp2' :
876+ return
877+
840878 proc_cxt = PerProcessContext ()
841879 if not (proc_cxt .is_distributed
842880 and self ._config .ddp_paras_check_interval > 0
0 commit comments