77import os
88import sys
99import argparse
10+ from dataclasses import dataclass
1011
1112import transformer_engine .pytorch as te
1213import transformer_engine .common .recipe
1314
1415import torch
1516import torch .distributed as dist
17+ from torch .distributed .checkpoint import save , load
18+ from torch .distributed .checkpoint .state_dict import (
19+ StateDictOptions ,
20+ get_state_dict ,
21+ set_state_dict ,
22+ )
23+ from torch .distributed .checkpoint .stateful import Stateful
1624from torch .distributed .tensor import DTensor
1725import torch .nn .functional as F
1826from torch import nn , optim
2533LOCAL_RANK = None
2634
2735
36+ @dataclass
37+ class AppState (Stateful ):
38+ """AppState for FSDP2 checkpoint via Torch DCP.
39+
40+ Adapted from https://docs.pytorch.org/tutorials/recipes/distributed_checkpoint_recipe.html
41+ """
42+
43+ model : torch .nn .Module
44+ optimizer : torch .optim .Optimizer
45+
46+ def state_dict (self ):
47+ """
48+ Get the state dict for the model, optimizer, scheduler, and step.
49+ This factory both retrieves the model state dictionary when saving
50+ checkpoints and initializes a destination for the state read from
51+ DCP checkpoint files when loading checkpoints.
52+ """
53+ model_state_dict , optimizer_state_dict = get_state_dict (self .model , self .optimizer )
54+ for fqn in list (model_state_dict .keys ()):
55+ # Get the model parameter.
56+ model_param = model_state_dict [fqn ]
57+ if isinstance (model_param , DTensor ):
58+ model_param = model_param .to_local ()
59+ if model_param .numel () == 0 and fqn in optimizer_state_dict ["state" ]:
60+ # Empty model parameter. Clear the associated optimizer state
61+ # when initializing the optimizer state upon DCP load, because
62+ # empty optimizer state DTensors are not checkpointed with DCP,
63+ # yet get_state_dict / _init_optim_state produce empty Tensors.
64+ # TransformerEngine uses empty Tensors for dummy Parameters.
65+ optimizer_state_dict ["state" ][fqn ] = {}
66+ if fqn .endswith ("._extra_state" ):
67+ # Evict `_extra_state` quantization data from model checkpoint.
68+ model_state_dict .pop (fqn )
69+ return {
70+ "model" : model_state_dict ,
71+ "optim" : optimizer_state_dict ,
72+ }
73+
74+ def load_state_dict (self , state_dict : dict ):
75+ """
76+ Load the state dict for the model, optimizer, scheduler, and step.
77+ Given the checkpoint-loaded state_dict, set the state of the model,
78+ optimizer, scheduler, step, and epoch to the values in state_dict.
79+ """
80+ set_state_dict (
81+ self .model ,
82+ self .optimizer ,
83+ model_state_dict = state_dict ["model" ],
84+ optim_state_dict = state_dict ["optim" ],
85+ # Non-strict checkpoint loading ignores empty optimizer states,
86+ # skips loading non-FP8 checkpoint weights (e.g. _extra_state).
87+ options = StateDictOptions (strict = False ),
88+ )
89+
90+
2891def dist_print (msg ):
2992 if LOCAL_RANK == 0 :
3093 print (msg )
@@ -86,11 +149,16 @@ def _parse_args(argv=None, namespace=None):
86149 "--sharding-dims" ,
87150 type = int ,
88151 nargs = "+" ,
89- help = 'FSDP/HSDP sharding dimensions ("replicate ", "shard ")' ,
152+ help = 'FSDP/HSDP sharding dimensions ("dp_replicate ", "dp_shard", "tp ")' ,
90153 )
91154 args = parser .parse_args (argv , namespace )
92155 if args .sharding_dims :
93- assert len (args .sharding_dims ) <= 2
156+ assert len (args .sharding_dims ) <= 3
157+ if len (args .sharding_dims ) >= 3 :
158+ # Set the TP size in args.
159+ args .tp_size = args .sharding_dims [2 ]
160+ else :
161+ args .tp_size = 1
94162 return args
95163
96164
@@ -133,11 +201,17 @@ def init_te_model(config):
133201 "params_dtype" : params_dtype ,
134202 }
135203 kwargs ["device" ] = config .device
204+ kwargs ["tp_size" ] = config .tp_size
136205
137206 layer_type = get_te_layer_from_string (config .layer_type )
138207 # We are creating model in a way so that we can test both reshard_after_forward=True/False cases.
139208 # more details below.
140- if layer_type in [te .MultiheadAttention , te .TransformerLayer ]:
209+ if layer_type in [
210+ te .TransformerLayer ,
211+ te .MultiheadAttention ,
212+ te .LayerNormMLP ,
213+ # TODO(@cspades): GroupedLinear testing.
214+ ]:
141215 # For this case, we are creating a model that resemebles production use-cases
142216 # wherein there are mltiple TransformerLayers in the model. And we would need
143217 # to shard each transformer layer. Since each transformer layer is not a root module,
@@ -147,44 +221,102 @@ def init_te_model(config):
147221 kwargs ["fuse_qkv_params" ] = True
148222 if layer_type is te .MultiheadAttention :
149223 kwargs ["input_layernorm" ] = True
224+ # DeviceMesh / DTensor-related model parameter operations!
225+ # NOTE(@cspades): `set_device_mesh` works, but needs to be called before reset_parameters.
226+ # If not using meta device initialization, reset_parameters is called during __init__.
227+ if config .tp_size > 1 :
228+ assert "dp_shard" in config .mesh .mesh_dim_names
229+ assert "tp" in config .mesh .mesh_dim_names
230+ dist_print (f"Tensor parallelism activated with size: { config .tp_size } " )
231+ # Activate TP in TE.
232+ kwargs ["set_parallel_mode" ] = True
233+ # For TP shards as DTensors.
234+ kwargs ["tp_mesh" ] = config .mesh ["tp" ]
235+ # For per-tensor quantization recipes with TP.
236+ kwargs ["weight_mesh" ] = config .mesh ["dp_shard" , "tp" ]._flatten ("weight_mesh" )
237+ elif len (config .mesh .mesh_dim_names ) > 1 :
238+ assert "dp_shard" in config .mesh .mesh_dim_names
239+ # HSDP (DP-Repl, DP-Shard) requires a call to `set_device_mesh(weight_mesh)`.
240+ # Used for per-tensor quantization recipes like Float8CurrentScaling.
241+ kwargs ["weight_mesh" ] = config .mesh ["dp_shard" ] # Only sharding with FSDP.
242+ # Initialize model.
150243 model = nn .Sequential (* [layer_type (* args , ** kwargs ) for _ in range (config .num_layers )])
151- elif layer_type == te .LayerNormLinear :
244+ elif layer_type in [ te .LayerNormLinear , te . Linear ] :
152245 # For this case, we are creating a model with just one LayerNormLinear layer
153246 # so that the model itself is a root module, and FSDP2's fully_shard assigns
154247 # reshard_after_forward=True for the parameters of these model.
155248 args [1 ] *= 3 # QKV projection
156249 out_shape [- 1 ] *= 3
250+ # DeviceMesh / DTensor-related model parameter operations!
251+ # NOTE(@cspades): `set_device_mesh` works, but needs to be called before reset_parameters.
252+ # If not using meta device initialization, reset_parameters is called during __init__.
253+ if config .tp_size > 1 :
254+ assert "dp_shard" in config .mesh .mesh_dim_names
255+ assert "tp" in config .mesh .mesh_dim_names
256+ dist_print (f"Tensor parallelism activated with size: { config .tp_size } " )
257+ # Activate TP in TE.
258+ kwargs ["parallel_mode" ] = "column"
259+ # For TP shards as DTensors.
260+ kwargs ["tp_mesh" ] = config .mesh ["tp" ]
261+ # For per-tensor quantization recipes with TP.
262+ kwargs ["weight_mesh" ] = config .mesh ["dp_shard" , "tp" ]._flatten ("weight_mesh" )
263+ # Modify output shape for column-parallel Linear.
264+ out_shape [- 1 ] //= config .tp_size
265+ elif len (config .mesh .mesh_dim_names ) > 1 :
266+ assert "dp_shard" in config .mesh .mesh_dim_names
267+ # HSDP (DP-Repl, DP-Shard) requires a call to `set_device_mesh(weight_mesh)`.
268+ # Used for per-tensor quantization recipes like Float8CurrentScaling.
269+ kwargs ["weight_mesh" ] = config .mesh ["dp_shard" ] # Only sharding with FSDP.
270+ # Initialize model.
157271 model = layer_type (* args , ** kwargs )
158272 else :
273+ # Other TE module. Just ambiguously initialize it.
159274 model = layer_type (* args , ** kwargs )
160275
161276 return model , inp_shape , out_shape
162277
163278
164279def get_device_mesh (world_size , sharding_dims ):
165- dist_print (f"sharding-dims:{ sharding_dims } " )
280+ dist_print (f"sharding-dims: { sharding_dims } " )
166281 device_ids = list (range (world_size ))
167- if sharding_dims is None : # FSDP
168- mesh = DeviceMesh ("cuda" , device_ids )
169- elif len (sharding_dims ) == 1 :
170- assert sharding_dims [0 ] == world_size
171- mesh = DeviceMesh ("cuda" , device_ids )
172- elif len (sharding_dims ) == 2 : # HSDP
282+ # FSDP
283+ if sharding_dims is None or len (sharding_dims ) == 1 :
284+ assert sharding_dims is None or sharding_dims [0 ] == world_size
285+ mesh = init_device_mesh (
286+ "cuda" ,
287+ (world_size ,),
288+ mesh_dim_names = ("dp_shard" ,),
289+ )
290+ # HSDP
291+ elif len (sharding_dims ) == 2 :
173292 assert sharding_dims [0 ] * sharding_dims [1 ] == world_size
174293 mesh = init_device_mesh (
175294 "cuda" ,
176295 (sharding_dims [0 ], sharding_dims [1 ]),
177- mesh_dim_names = ("replicate" , "shard" ),
296+ mesh_dim_names = ("dp_replicate" , "dp_shard" ),
297+ )
298+ # (H/F)SDP-TP
299+ elif len (sharding_dims ) == 3 :
300+ assert sharding_dims [0 ] * sharding_dims [1 ] * sharding_dims [2 ] == world_size
301+ mesh = init_device_mesh (
302+ "cuda" ,
303+ (sharding_dims [0 ], sharding_dims [1 ], sharding_dims [2 ]),
304+ mesh_dim_names = ("dp_replicate" , "dp_shard" , "tp" ),
178305 )
179306 else :
307+ # Unsupported topology.
180308 assert False
181309 return mesh
182310
183311
184312def shard_model_with_fsdp2 (model , mesh ):
313+ assert "dp_shard" in mesh .mesh_dim_names
314+ dp_dims = (
315+ ("dp_replicate" , "dp_shard" ) if "dp_replicate" in mesh .mesh_dim_names else ("dp_shard" ,)
316+ )
185317 for child in model .children ():
186- fully_shard (child , mesh = mesh )
187- fully_shard (model , mesh = mesh )
318+ fully_shard (child , mesh = mesh [ dp_dims ] )
319+ fully_shard (model , mesh = mesh [ dp_dims ] )
188320 return model
189321
190322
@@ -213,16 +345,18 @@ def restore_custom_attrs(module, custom_attrs):
213345
214346@torch .no_grad ()
215347def test_fp8_fsdp2_allgather (model ):
216- # Do manual allgather in fp32 and match against fp8 allgather done
217- # with fsdp2
348+ """
349+ Compare the result of the FP8 AG by FSDP2 with a manual AG in FP32
350+ after dequantizing the FP8 values.
351+ """
218352 # FP32 manual weight allgather
219353 fp32_allgathered_params = {}
220354 for name , param in model .named_parameters ():
221355 assert isinstance (param , DTensor )
222356 local_tensor = param ._local_tensor
223357 device_mesh = param .device_mesh
224358 dist_group = (
225- device_mesh .get_group (mesh_dim = "shard " )
359+ device_mesh .get_group (mesh_dim = "dp_shard " )
226360 if device_mesh .ndim > 1
227361 else device_mesh .get_group ()
228362 )
@@ -241,6 +375,10 @@ def test_fp8_fsdp2_allgather(model):
241375 module .unshard ()
242376 # Make sure allgathered parameters match exactly
243377 for name , param in model .named_parameters ():
378+ if isinstance (param , DTensor ):
379+ # Will still be a DTensor in the case of TP, even after FSDP2 AG,
380+ # because we wrap our weights as DTensor shards over the TP group.
381+ param = param ._local_tensor
244382 torch .testing .assert_close (param .dequantize (), fp32_allgathered_params [name ])
245383 # Revert model to original sharded state
246384 for module in model .modules ():
@@ -250,6 +388,9 @@ def test_fp8_fsdp2_allgather(model):
250388
251389
252390def _train (args ):
391+ """
392+ Torch Distributed Initialization
393+ """
253394 global LOCAL_RANK
254395 assert "TORCHELASTIC_RUN_ID" in os .environ
255396 WORLD_RANK = int (os .getenv ("RANK" , "0" ))
@@ -274,9 +415,19 @@ def _train(args):
274415 nccl_world = dist .new_group (backend = "nccl" )
275416 device = torch .device (f"cuda:{ LOCAL_RANK } " )
276417
418+ # Create a DeviceMesh for fully_shard.
419+ world_size = int (WORLD_SIZE )
420+ # Setup the sharding mesh for FSDP/HSDP.
421+ mesh = get_device_mesh (world_size , args .sharding_dims )
422+ args .mesh = mesh
423+
424+ """
425+ TransformerEngine Model Initialization
426+ """
277427 # FP8 Configuration
278428 fp8_recipe = get_recipe_from_string (args .recipe )
279429
430+ # Model initialization context.
280431 build_model_context_args = {}
281432 if not args .fp8_init :
282433 # Build model context (FP8 init)
@@ -297,18 +448,17 @@ def _train(args):
297448 f" { torch .cuda .memory_allocated (device ) / 1e6 } MB"
298449 )
299450
300- # Creating a DeviceMesh for fully_shard
301- world_size = int (WORLD_SIZE )
302- # Setup the sharding mesh for FSDP/HSDP
303- mesh = get_device_mesh (world_size , args .sharding_dims )
451+ # Avoid passing custom attributes to FSDP2.
304452 custom_attrs = save_custom_attrs (model )
453+ # Fully-shard the model. Will convert model parameters into DTensor
454+ # if not already converted by TP.
305455 model = shard_model_with_fsdp2 (model , mesh )
456+ # Restore custom attributes on parameters.
306457 restore_custom_attrs (model , custom_attrs )
307- # model now has DTensors as its parameters
308458
309459 if args .device == "meta" :
310460 # After FSDP2 has been applied, materialize and initialize the sharded parameters
311- # TE base.py's reset_parameters() handles DTensors with FP8 initialization
461+ # TE base.py's reset_parameters() handles DTensors with FP8 initialization.
312462 for module in model .modules ():
313463 if hasattr (module , "reset_parameters" ):
314464 module .reset_parameters ()
@@ -320,6 +470,9 @@ def _train(args):
320470
321471 optimizer = optim .Adam (model .parameters (), lr = 1e-3 )
322472
473+ """
474+ Pre-Save Training
475+ """
323476 for iteration in range (args .iter ):
324477 # Zero the parameter gradients
325478 optimizer .zero_grad ()
0 commit comments