Skip to content

Commit 0a9ede1

Browse files
author
Le
committed
Add FSDP2 functionality
1 parent 19ac3a5 commit 0a9ede1

9 files changed

Lines changed: 878 additions & 35 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ To launch single-node multi-gpu training, set the 'multi-gpu' argument
173173
python -m alf.bin.train --conf=CONF_FILE --root_dir=LOG_DIR --distributed multi-gpu
174174
```
175175

176+
DistributedDataParallel (DDP) is used by default. To shard model parameters,
177+
gradients, and optimizer state with PyTorch FSDP2, add
178+
`--distributed_strategy fsdp2`:
179+
```bash
180+
python -m alf.bin.train --conf=CONF_FILE --root_dir=LOG_DIR \
181+
--distributed multi-gpu --distributed_strategy fsdp2
182+
```
183+
176184
To launch multi-node multi-gpu training, we use torch distributed launch module. The 'local_rank' for each process can be obtained from 'PerProcessContext' class, which can be used to assign gpu for your environment if you wish. For details on how PyTorch assign 'local_rank' and 'ddp_rank', please refer to the [documentation](https://github.com/pytorch/pytorch/blob/main/torch/distributed/launch.py). To start training, run the following command on the host machine:
177185
```bash
178186
export NCCL_SOCKET_IFNAME=SOCKET # find in ifconfig

alf/algorithms/algorithm.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ def __init__(self,
153153
self._replay_buffer = None
154154

155155
self._ddp_activated_rank = -1
156+
self._distributed_strategy = 'ddp'
156157

157158
# These 3 parameters are only set when ``set_replay_buffer()`` is called.
158159
self._replay_buffer_num_envs = None
@@ -333,16 +334,20 @@ def use_rollout_state(self):
333334
"""
334335
return self._use_rollout_state
335336

336-
def activate_ddp(self, rank: int):
337+
def activate_ddp(self, rank: int, strategy: str = 'ddp'):
337338
"""Prepare the Algorithm with DistributedDataParallel wrapper
338339
339340
Note that Algorithm does not need to remember the rank of the device.
340341
341342
Args:
342343
rank (int): DDP wrapper needs to know on which GPU device this
343344
module's parameters and buffers are supposed to be.
345+
strategy: distributed implementation, ``'ddp'`` or ``'fsdp2'``.
344346
"""
347+
if strategy not in ('ddp', 'fsdp2'):
348+
raise ValueError("Unknown distributed strategy: %s" % strategy)
345349
self._ddp_activated_rank = rank
350+
self._distributed_strategy = strategy
346351

347352
@use_rollout_state.setter
348353
def use_rollout_state(self, flag):
@@ -951,7 +956,11 @@ def state_dict(self, destination=None, prefix='', visited=None, **kwargs):
951956
return destination
952957

953958
@common.add_method(nn.Module)
954-
def load_state_dict(self, state_dict, strict=True, skip_preloded=True):
959+
def load_state_dict(self,
960+
state_dict,
961+
strict=True,
962+
skip_preloded=True,
963+
assign=False):
955964
"""Load state dictionary for the algorithm.
956965
957966
Args:
@@ -964,6 +973,8 @@ def load_state_dict(self, state_dict, strict=True, skip_preloded=True):
964973
skip_preloded (bool): whether to skip the modules that support
965974
pre-loading and have been pre-loaded. Currently only Algorithm
966975
and its derivatives support pre-loading. (Default: ``True``)
976+
assign (bool): accepted for compatibility with PyTorch's module
977+
state-dict APIs. ALF preserves its existing copy semantics.
967978
Returns:
968979
namedtuple:
969980
- missing_keys: a list of str containing the missing keys.

alf/algorithms/rl_algorithm.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
BasicRLInfo)
3232
from alf.utils import common, dist_utils, summary_utils
3333
from alf.utils.summary_utils import record_time
34-
from alf.utils.distributed import data_distributed_when, make_ddp_performer
34+
from alf.utils.distributed import (data_distributed_when,
35+
make_distributed_performer)
3536
from alf.tensor_specs import TensorSpec
3637
from .config import TrainerConfig
3738

@@ -786,14 +787,21 @@ def _train_iter_on_policy(self):
786787
return steps
787788

788789
def _unroll(self, unroll_length: int):
790+
if self._distributed_strategy == 'fsdp2' and self._ddp_activated_rank != -1:
791+
# FSDP parameters are sharded between calls, so every off-policy
792+
# unroll must run through the FSDP root's gather/reshard hooks.
793+
self._first_unroll = False
794+
performer = make_distributed_performer(self, self.unroll.__func__)
795+
return performer(unroll_length)
789796
if self._first_unroll:
790797
self._first_unroll = False
791798
if self._ddp_activated_rank != -1:
792799
# Even though we don't update parameters during unroll, we still need to
793800
# wrap self.unroll in DDP so that the parameters are synchronized across
794801
# all workers before the unroll starts. Otherwise, the parameters across
795802
# the workers are different for the first unroll.
796-
performer = make_ddp_performer(self, self.unroll.__func__)
803+
performer = make_distributed_performer(self,
804+
self.unroll.__func__)
797805
return performer(unroll_length)
798806

799807
return self.unroll(unroll_length)

alf/bin/train.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
4444
In case you have multiple GPUs on the machine and you would like to
4545
train with all of them, specify --distributed multi-gpu. This will use
46-
PyTorch's DistributedDataParallel for training.
46+
PyTorch's DistributedDataParallel for training. Add
47+
--distributed_strategy fsdp2 to use composable FullyShardedDataParallel.
4748
4849
If instead of Gin configuration file, you want to use ALF python conf file, then
4950
replace the "--gin_file" option with "--conf", and "--gin_param" with "--conf_param".
@@ -83,6 +84,9 @@ def _define_flags():
8384
flags.DEFINE_enum(
8485
'distributed', 'none', ['none', 'multi-gpu', 'multi-node-multi-gpu'],
8586
'Set whether and how to run training in distributed mode.')
87+
flags.DEFINE_enum(
88+
'distributed_strategy', 'ddp', ['ddp', 'fsdp2'],
89+
'Parameter distribution strategy used in distributed mode.')
8690
flags.DEFINE_integer(
8791
'num_gpus_per_ddp_worker', 1,
8892
"The number of gpus per DDP worker. If specified will create N DDP workers where each worker "
@@ -201,8 +205,11 @@ def _train(root_dir, local_rank=-1, rank=0, world_size=1):
201205
alg_wrapper_ctor = DistributedUnroller
202206
else:
203207
alg_wrapper_ctor = None
204-
trainer = policy_trainer.RLTrainer(trainer_conf, ddp_rank,
205-
alg_wrapper_ctor)
208+
trainer = policy_trainer.RLTrainer(
209+
trainer_conf,
210+
ddp_rank,
211+
alg_wrapper_ctor,
212+
distributed_strategy=FLAGS.distributed_strategy)
206213
elif trainer_conf.ml_type == 'sl':
207214
# NOTE: SLTrainer does not support distributed training yet
208215
if world_size > 1:

alf/trainers/policy_trainer.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

alf/utils/checkpoint_utils.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,18 @@ def _convert_legacy_parameter(checkpoint):
215215

216216
def _load_one(module, checkpoint):
217217
if isinstance(module, nn.Module):
218-
missing_keys, unexpected_keys = module.load_state_dict(
219-
checkpoint, strict=strict)
218+
from alf.utils.distributed import (FSDP2_OPTIMIZER_STATE,
219+
is_fsdp2_module,
220+
load_fsdp2_full_state_dict)
221+
fsdp2_checkpoint = (FSDP2_OPTIMIZER_STATE in checkpoint
222+
or not any('_optimizers.' in key
223+
for key in checkpoint))
224+
if is_fsdp2_module(module) and fsdp2_checkpoint:
225+
missing_keys, unexpected_keys = load_fsdp2_full_state_dict(
226+
module, checkpoint, strict=strict)
227+
else:
228+
missing_keys, unexpected_keys = module.load_state_dict(
229+
checkpoint, strict=strict)
220230
else:
221231
module.load_state_dict(checkpoint)
222232
missing_keys, unexpected_keys = [], []
@@ -363,7 +373,10 @@ def _separate_state(state):
363373
replay_buffer_state = {}
364374

365375
for k, v in state.items():
366-
if k.find('_optimizers.') >= 0 and isinstance(
376+
from alf.utils.distributed import FSDP2_OPTIMIZER_STATE
377+
if k == FSDP2_OPTIMIZER_STATE:
378+
optimizer_state[k] = v
379+
elif k.find('_optimizers.') >= 0 and isinstance(
367380
v, dict) and 'param_groups' in v:
368381
optimizer_state[k] = v
369382
elif Checkpointer._is_replay_buffer_key(k):
@@ -718,7 +731,8 @@ def save_replay_buffer(self,
718731
def save(self,
719732
global_step,
720733
suffix: Optional[str] = None,
721-
including_replay_buffer=True):
734+
including_replay_buffer=True,
735+
state_overrides=None):
722736
"""Save states of all modules to checkpoint
723737
724738
Args:
@@ -729,6 +743,9 @@ def save(self,
729743
If provided, it will be used as the suffix instead of ``global_step``.
730744
including_replay_buffer (bool): whether save replay buffer state in
731745
the main replay buffer checkpoint file.
746+
state_overrides (dict|None): precomputed states keyed by module
747+
name. Used by collective state-dict implementations such as
748+
FSDP2.
732749
"""
733750
suffix = suffix or str(global_step)
734751

@@ -744,10 +761,12 @@ def save(self,
744761
(replay_buffer, is_checkpoint_enabled(replay_buffer)))
745762
enable_checkpoint(replay_buffer, False)
746763
try:
764+
state_overrides = state_overrides or {}
747765
state = {
748766
k:
749-
v.module.state_dict()
750-
if type(v) == torch.nn.DataParallel else v.state_dict()
767+
state_overrides[k] if k in state_overrides else
768+
(v.module.state_dict()
769+
if type(v) == torch.nn.DataParallel else v.state_dict())
751770
for k, v in self._modules.items()
752771
}
753772
finally:

0 commit comments

Comments
 (0)