已合并
[2/2] Add tp cases #18965
dilililiwhy创建于 2025年3月14日
[2/2] Add tp cases #18965
已合并
dilililiwhy创建于 2025年3月14日
refs/pull/18965/head合入到master
1 个文件变更+565-0
Atest/distributed/tensor/parallel/test_tp_examples.py+565-0
@@ -0,0 +1,565 @@
1+# Copyright (c) Meta Platforms, Inc. and affiliates
2+# Owner(s): ["oncall: distributed"]
3+ 
4+import itertools
5+from copy import deepcopy
6+from typing import NamedTuple, Optional
7+ 
8+import torch
9+import torch.distributed as dist
10+import torch.nn.functional as F
11+from torch.distributed._tensor import (
12+ DeviceMesh,
13+ distribute_tensor,
14+ DTensor,
15+ Replicate,
16+ Shard,
17+)
18+from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import (
19+ checkpoint_wrapper,
20+ CheckpointImpl,
21+)
22+from torch.distributed.tensor.debug import CommDebugMode
23+from torch.distributed.tensor.parallel import (
24+ ColwiseParallel,
25+ loss_parallel,
26+ parallelize_module,
27+ RowwiseParallel,
28+)
29+from torch.distributed.tensor.parallel.input_reshard import input_reshard
30+from torch.testing._internal.common_utils import (
31+ instantiate_parametrized_tests,
32+ parametrize,
33+ run_tests,
34+)
35+from torch.testing._internal.distributed._tensor.common_dtensor import (
36+ DTensorTestBase,
37+ MLPModule,
38+ ModelArgs,
39+ skip_unless_torch_gpu,
40+ Transformer,
41+)
42+ 
43+import torch_npu
44+from torch_npu.testing.common_distributed import with_comms, skipIfUnsupportMultiNPU
45+ 
46+ 
47+c10d_functional = torch.ops.c10d_functional
48+reduce_scatter, all_gather, all_reduce = (
49+ c10d_functional.reduce_scatter_tensor,
50+ c10d_functional.all_gather_into_tensor,
51+ c10d_functional.all_reduce,
52+)
53+ 
54+ 
55+class ExpCommCounts(NamedTuple):
56+ fwd: Optional[dict] = None
57+ bwd: Optional[dict] = None
58+ optim: Optional[dict] = None
59+ 
60+ 
61+class DistTensorParallelExampleTest(DTensorTestBase):
62+ @property
63+ def world_size(self):
64+ return 2
65+ 
66+ def _check_module(self, m1, m2, check_grad=False):
67+ named_parameters = dict(m1.named_parameters())
68+ for name, param_m2 in m2.named_parameters():
69+ self.assertTrue(name in named_parameters)
70+ param_m1 = named_parameters[name]
71+ if check_grad:
72+ param_m2 = param_m2.grad
73+ param_m1 = param_m1.grad
74+ if isinstance(param_m2, DTensor):
75+ replicate = [Replicate()]
76+ param_m2 = param_m2.redistribute(
77+ device_mesh=param_m2.device_mesh, placements=replicate
78+ ).to_local()
79+ self.assertEqual(param_m2, param_m1)
80+ 
81+ def _test_mlp_training_e2e(self, is_seq_parallel=False, recompute_activation=False):
82+ inp_size = [8, 10]
83+ # Ensure all tp ranks have same input.
84+ rng_seed = self.rank if is_seq_parallel else 0
85+ torch.manual_seed(rng_seed)
86+ inp = torch.rand(*inp_size, device=self.device_type)
87+ model = MLPModule(self.device_type)
88+ model_tp = deepcopy(model)
89+ 
90+ # Ensure model are initialized the same way.
91+ self._check_module(model, model_tp)
92+ 
93+ # Shard module and initialize optimizer.
94+ LR = 0.25
95+ device_mesh = DeviceMesh(
96+ self.device_type,
97+ torch.arange(0, self.world_size),
98+ )
99+ parallelize_plan = {
100+ "net1": (
101+ ColwiseParallel(input_layouts=Shard(0))
102+ if is_seq_parallel
103+ else ColwiseParallel()
104+ ),
105+ "net2": (
106+ RowwiseParallel(output_layouts=Shard(0))
107+ if is_seq_parallel
108+ else RowwiseParallel()
109+ ),
110+ }
111+ model_tp = parallelize_module(model_tp, device_mesh, parallelize_plan)
112+ if recompute_activation:
113+ model_tp = input_reshard(
114+ checkpoint_wrapper(
115+ model_tp, checkpoint_impl=CheckpointImpl.NO_REENTRANT
116+ ),
117+ device_mesh,
118+ None if is_seq_parallel else 0,
119+ )
120+ optim = torch.optim.SGD(model.parameters(), lr=LR)
121+ optim_tp = torch.optim.SGD(model_tp.parameters(), lr=LR)
122+ 
123+ output = model(inp)
124+ output.sum().backward()
125+ 
126+ comm_mode = CommDebugMode()
127+ with comm_mode:
128+ output_tp = model_tp(inp)
129+ output_tp.sum().backward()
130+ 
131+ self.assertEqual(output, output_tp)
132+ if is_seq_parallel:
133+ self.assertEqual(
134+ comm_mode.get_comm_counts()[c10d_functional.all_gather_into_tensor], 2
135+ )
136+ self.assertEqual(
137+ comm_mode.get_comm_counts()[c10d_functional.reduce_scatter_tensor], 1
138+ )
139+ else:
140+ self.assertEqual(comm_mode.get_comm_counts()[c10d_functional.all_reduce], 1)
141+ 
142+ if is_seq_parallel:
143+ # Sum gradients from different ranks, since input
144+ # are different across ranks for sequence parallel.
145+ dist.all_reduce(model.net1.weight.grad)
146+ dist.all_reduce(model.net1.bias.grad)
147+ dist.all_reduce(model.net2.weight.grad)
148+ dist.all_reduce(model.net2.bias.grad)
149+ 
150+ # Ensure gradients are same.
151+ self._check_module(model, model_tp, check_grad=True)
152+ 
153+ optim.step()
154+ optim_tp.step()
155+ 
156+ # Ensure model weights are still same after update.
157+ # Due to the trick we use for Partial aggregation, we only check the weight when local_rank = 0.
158+ self._check_module(model, model_tp)
159+ 
160+ inp = torch.rand(*inp_size, device=self.device_type)
161+ output = model(inp)
162+ output_tp = model_tp(inp)
163+ self.assertEqual(output, output_tp)
164+ 
165+ def _test_mlp_inference(self, device_mesh):
166+ inp_size = [8, 10]
167+ # Ensure all tp ranks have same input.
168+ torch.manual_seed(0)
169+ inp = torch.rand(*inp_size, device=self.device_type)
170+ model = MLPModule(self.device_type)
171+ model_tp = deepcopy(model)
172+ 
173+ # Ensure model are initialized the same way.
174+ self._check_module(model, model_tp)
175+ 
176+ # Shard module and initialize optimizer.
177+ parallelize_plan = {
178+ "net1": ColwiseParallel(),
179+ "net2": RowwiseParallel(),
180+ }
181+ model_tp = parallelize_module(model_tp, device_mesh, parallelize_plan)
182+ 
183+ output = model(inp)
184+ output_tp = model_tp(inp)
185+ self.assertEqual(output, output_tp)
186+ 
187+ @with_comms
188+ @parametrize("is_seq_parallel", [True, False])
189+ @skipIfUnsupportMultiNPU(2)
190+ # TODO: need to revisit input_reshard API about why it failed multi-gpu tests.
191+ # @parametrize("recompute_activation", [True, False])
192+ @parametrize("recompute_activation", [False])
193+ def test_mlp_training(self, is_seq_parallel, recompute_activation):
194+ self._test_mlp_training_e2e(
195+ is_seq_parallel=is_seq_parallel, recompute_activation=recompute_activation
196+ )
197+ 
198+ @with_comms
199+ @skipIfUnsupportMultiNPU(2)
200+ def test_mlp_inference(self):
201+ device_mesh = DeviceMesh(
202+ self.device_type,
203+ torch.arange(0, self.world_size),
204+ )
205+ with torch.inference_mode():
206+ self._test_mlp_inference(device_mesh)
207+ 
208+ def _setup_single_gpu_model(self, model_args, dtype):
209+ return Transformer(model_args).to(device=self.device_type, dtype=dtype)
210+ 
211+ def _setup_tp_model(self, model, is_seq_parallel, dtype):
212+ model_tp = deepcopy(model)
213+ self._check_module(model, model_tp)
214+ device_mesh = DeviceMesh(self.device_type, torch.arange(0, self.world_size))
215+ local_output_for_attn = dtype is torch.float64
216+ return Transformer.parallelize(
217+ model_tp,
218+ device_mesh,
219+ is_seq_parallel,
220+ local_output_for_attn=local_output_for_attn,
221+ )
222+ 
223+ def _setup_optimizer(self, model, model_tp):
224+ # Step 3: Run test by comparing outputs from single-gpu and multi-gpu models.
225+ LR = 0.25
226+ optim = torch.optim.Adam(model.parameters(), lr=LR)
227+ optim_tp = torch.optim.Adam(model_tp.parameters(), lr=LR)
228+ return optim, optim_tp
229+ 
230+ def _validate_fwd(
231+ self, model, model_tp, inp, expected_comms_dict=None, check_comms=True
232+ ):
233+ # Compare outputs on the same input.
234+ output = model(inp)
235+ with CommDebugMode() as comm_mode:
236+ output_tp = model_tp(inp)
237+ self.assertEqual(output, output_tp)
238+ if check_comms:
239+ self.assertDictEqual(comm_mode.get_comm_counts(), expected_comms_dict or {})
240+ return output, output_tp
241+ 
242+ def _validate_bwd(
243+ self,
244+ model,
245+ model_tp,
246+ output,
247+ output_tp,
248+ expected_comms_dict=None,
249+ check_comms=True,
250+ ):
251+ # Ensure gradients are equal.
252+ output.sum().backward()
253+ with CommDebugMode() as comm_mode:
254+ output_tp.sum().backward()
255+ self._check_module(model, model_tp, check_grad=True)
256+ if check_comms:
257+ self.assertDictEqual(comm_mode.get_comm_counts(), expected_comms_dict or {})
258+ 
259+ def _validate_optim_step(
260+ self,
261+ model,
262+ model_tp,
263+ optim,
264+ optim_tp,
265+ expected_comms_dict=None,
266+ check_comms=True,
267+ ):
268+ optim.step() # Ensure model weights are still the same after update.
269+ from torch.distributed._tensor.experimental import implicit_replication
270+ 
271+ with implicit_replication():
272+ with CommDebugMode() as comm_mode:
273+ optim_tp.step()
274+ self._check_module(model, model_tp)
275+ if check_comms:
276+ self.assertDictEqual(comm_mode.get_comm_counts(), expected_comms_dict or {})
277+ 
278+ @staticmethod
279+ def _thaw_params(thaw_params, model, model_tp):
280+ if not thaw_params:
281+ return
282+ for target_model in [model, model_tp]:
283+ for n, p in target_model.named_parameters():
284+ if n not in thaw_params:
285+ p.requires_grad_(False)
286+ 
287+ @with_comms
288+ @skip_unless_torch_gpu
289+ @parametrize("is_seq_parallel", [True, False])
290+ @parametrize("dtype", [torch.float64, torch.float32])
291+ def test_transformer_training(self, is_seq_parallel, dtype: torch.dtype):
292+ EXP_BASE_CC = ExpCommCounts(
293+ fwd={all_reduce: 6, all_gather: 1}, bwd={all_reduce: 9}
294+ )
295+ EXP_SEQ_PARALLEL_CC = ExpCommCounts(
296+ fwd={reduce_scatter: 6, all_gather: 6},
297+ bwd={reduce_scatter: 5, all_gather: 6},
298+ optim={all_reduce: 30},
299+ )
300+ 
301+ # Disable dropout in the test since we cannot reproduce the same random
302+ # behaviors when comparing single-gpu models with multi-gpu models.
303+ model_args = ModelArgs(dropout_p=0.0)
304+ model = self._setup_single_gpu_model(
305+ model_args, dtype
306+ ) # Step 1: Initialize single-gpu models.
307+ model_tp = self._setup_tp_model(
308+ model, is_seq_parallel, dtype
309+ ) # Step 2: Setup tp model, place onto device mesh.
310+ optim, optim_tp = self._setup_optimizer(
311+ model, model_tp
312+ ) # Step 3: Setup optimizers for both models
313+ 
314+ # Initialize input and make sure all ranks have the same input.
315+ inp_size = [8, 8] # [batch_size, seq_len]
316+ if is_seq_parallel:
317+ assert inp_size[1] % self.world_size == 0
318+ 
319+ torch.manual_seed(0)
320+ steps = 10 if type(model) is torch.float64 else 1
321+ for _ in range(steps):
322+ inp = torch.randint(
323+ model_args.vocab_size, inp_size, device=self.device_type
324+ )
325+ expected_fwd_comms = (
326+ EXP_SEQ_PARALLEL_CC.fwd if is_seq_parallel else EXP_BASE_CC.fwd
327+ )
328+ output, output_tp = self._validate_fwd(
329+ model, model_tp, inp, expected_fwd_comms
330+ )
331+ expected_bwd_comms = (
332+ EXP_SEQ_PARALLEL_CC.bwd if is_seq_parallel else EXP_BASE_CC.bwd
333+ )
334+ self._validate_bwd(model, model_tp, output, output_tp, expected_bwd_comms)
335+ expected_optim_comms = (
336+ EXP_SEQ_PARALLEL_CC.optim if is_seq_parallel else EXP_BASE_CC.optim
337+ )
338+ self._validate_optim_step(
339+ model, model_tp, optim, optim_tp, expected_optim_comms
340+ )
341+ 
342+ @with_comms
343+ @skip_unless_torch_gpu
344+ @parametrize(
345+ "thaw_params, is_seq_parallel, dtype, exp_cnts",
346+ [
347+ (
348+ None, # all require grad seq_parallel float32 baseline
349+ True,
350+ torch.float32,
351+ ExpCommCounts(
352+ bwd={reduce_scatter: 5, all_gather: 6}, optim={all_reduce: 30}
353+ ),
354+ ),
355+ (
356+ None, # all require grad no seq_parallel float64 baseline
357+ False,
358+ torch.float64,
359+ ExpCommCounts(bwd={all_reduce: 9}),
360+ ),
361+ # test a subset of LayerNorm bwd output_masks
362+ (
363+ ("output.weight", "norm.weight", "norm.bias"), # [False, True, True]
364+ True,
365+ torch.float32,
366+ ExpCommCounts(bwd={reduce_scatter: 1}, optim={all_reduce: 6}),
367+ ),
368+ (
369+ ("tok_embeddings.weight", "output.weight"), # [True, False, False]
370+ True,
371+ torch.float32,
372+ ExpCommCounts(bwd={reduce_scatter: 5, all_gather: 5}),
373+ ),
374+ (
375+ (
376+ "tok_embeddings.weight",
377+ "output.weight",
378+ "norm.weight",
379+ "norm.bias",
380+ ), # [True, True, True]
381+ True,
382+ torch.float32,
383+ ExpCommCounts(
384+ bwd={reduce_scatter: 5, all_gather: 5}, optim={all_reduce: 6}
385+ ),
386+ ),
387+ (
388+ (
389+ "tok_embeddings.weight",
390+ "output.weight",
391+ "norm.weight",
392+ "norm.bias",
393+ "layers.1.ffn_norm.weight",
394+ "layers.1.ffn_norm.bias",
395+ ), # a single transformerblock layernorm
396+ True,
397+ torch.float32,
398+ ExpCommCounts(
399+ bwd={reduce_scatter: 5, all_gather: 5}, optim={all_reduce: 12}
400+ ),
401+ ),
402+ (
403+ (
404+ "tok_embeddings.weight",
405+ "layers.0.attention.wv.weight",
406+ "layers.0.feed_forward.w1.bias",
407+ "layers.1.ffn_norm.bias",
408+ "layers.1.feed_forward.w2.weight",
409+ "output.weight",
410+ ), # varied layer/param types
411+ True,
412+ torch.float32,
413+ ExpCommCounts(
414+ bwd={reduce_scatter: 5, all_gather: 5}, optim={all_reduce: 3}
415+ ),
416+ ),
417+ ],
418+ name_fn=lambda thaw, seq, dtype, *_: f"{'seq_parallel_' if seq else ''}"
419+ + f"{str(dtype).split('.')[-1]}_"
420+ + f"thaw_{'__'.join(sorted({n.rpartition('.')[0].replace('.', '_') for n in thaw})) if thaw else 'all'}",
421+ )
422+ def test_transformer_req_grad(self, thaw_params, is_seq_parallel, dtype, exp_cnts):
423+ # Sample a subset of `requires_grad` patterns
424+ 
425+ # disabling dropout to facilitate single gpu to multi-device comparison
426+ # disable weight-tying to enable more fine-tuning configurations
427+ model_args = ModelArgs(dropout_p=0.0, weight_tying=False)
428+ model = self._setup_single_gpu_model(
429+ model_args, dtype
430+ ) # Step 1: Initialize single-gpu models.
431+ model_tp = self._setup_tp_model(
432+ model, is_seq_parallel, dtype
433+ ) # Step 2: Setup tp model, place onto device mesh.
434+ optim, optim_tp = self._setup_optimizer(
435+ model, model_tp
436+ ) # Step 3: Setup optimizers for both models
437+ DistTensorParallelExampleTest._thaw_params(
438+ thaw_params, model, model_tp
439+ ) # Step 4: set `requires_grad` patterns
440+ 
441+ # Initialize input and make sure all ranks have the same input.
442+ inp_size = [8, 8] # [batch_size, seq_len]
443+ if is_seq_parallel:
444+ assert inp_size[1] % self.world_size == 0
445+ 
446+ torch.manual_seed(0)
447+ inp = torch.randint(model_args.vocab_size, inp_size, device=self.device_type)
448+ output, output_tp = self._validate_fwd(model, model_tp, inp, check_comms=False)
449+ self._validate_bwd(
450+ model, model_tp, output, output_tp, exp_cnts.bwd, check_comms=True
451+ )
452+ self._validate_optim_step(
453+ model, model_tp, optim, optim_tp, exp_cnts.optim, check_comms=True
454+ )
455+ 
456+ @with_comms
457+ @skipIfUnsupportMultiNPU(2)
458+ def test_weight_tying(self):
459+ class TestModule(torch.nn.Module):
460+ def __init__(self) -> None:
461+ super().__init__()
462+ # Initialize different weights for embedding and fc.
463+ torch.manual_seed(1)
464+ self.embedding = torch.nn.Embedding(16, 8)
465+ torch.manual_seed(2)
466+ self.fc = torch.nn.Linear(8, 16)
467+ 
468+ def forward(self, x):
469+ return self.fc(self.embedding(x))
470+ 
471+ model = TestModule().to(self.device_type)
472+ parallelize_plan = {
473+ "embedding": ColwiseParallel(),
474+ "fc": RowwiseParallel(),
475+ }
476+ device_mesh = DeviceMesh(self.device_type, list(range(self.world_size)))
477+ parallelize_module(model, device_mesh, parallelize_plan)
478+ 
479+ input_size = [5]
480+ torch.manual_seed(0)
481+ inp = torch.randint(16, input_size, device=self.device_type)
482+ 
483+ # Without weight tying.
484+ self.assertNotEqual(
485+ model.embedding.weight.to_local(), model.fc.weight.to_local()
486+ )
487+ output = model(inp)
488+ output.sum().backward()
489+ self.assertNotEqual(
490+ model.embedding.weight.grad.to_local(), model.fc.weight.grad.to_local()
491+ )
492+ model.zero_grad()
493+ 
494+ # With weight tying.
495+ model.fc.weight = model.embedding.weight
496+ 
497+ self.assertEqual(model.embedding.weight, model.fc.weight)
498+ self.assertEqual(id(model.embedding.weight), id(model.fc.weight))
499+ output = model(inp)
500+ output.sum().backward()
501+ self.assertEqual(model.embedding.weight.grad, model.fc.weight.grad)
502+ self.assertEqual(id(model.embedding.weight.grad), id(model.fc.weight.grad))
503+ 
504+ @with_comms
505+ @skipIfUnsupportMultiNPU(2)
506+ def test_loss_parallel(self):
507+ device_mesh = self.build_device_mesh()
508+ comm_mode = CommDebugMode()
509+ 
510+ channel_size, channel_dim = 16, 1
511+ test_setup = [
512+ (2, (8, channel_size), (8,)), # calling aten.nll_loss_forward
513+ (3, (8, channel_size, 12), (8, 12)), # calling aten.nll_loss2d_forward
514+ ]
515+ weight = torch.rand(channel_size, device=self.device_type)
516+ for input_ndim, input_size, target_size in test_setup:
517+ x = torch.rand(*input_size, device=self.device_type, requires_grad=True)
518+ target = torch.randint(channel_size, target_size, device=self.device_type)
519+ 
520+ shard_dims = list(range(input_ndim))
521+ reductions = ["none", "mean", "sum"]
522+ for shard_dim, reduction in itertools.product(shard_dims, reductions):
523+ dist_x = distribute_tensor(x, device_mesh, [Shard(shard_dim)])
524+ y = F.cross_entropy(x, target, weight, reduction=reduction)
525+ with loss_parallel():
526+ if shard_dim == channel_dim:
527+ with comm_mode:
528+ dist_y = F.cross_entropy(
529+ dist_x, target, weight, reduction=reduction
530+ )
531+ self.assertEqual(comm_mode.get_total_counts(), 3)
532+ self.assertEqual(
533+ comm_mode.get_comm_counts()[c10d_functional.all_reduce],
534+ 3,
535+ )
536+ self.assertTrue(dist_y.placements[0].is_replicate())
537+ self.assertEqual(dist_y.to_local(), y)
538+ 
539+ with comm_mode:
540+ if reduction == "none":
541+ y.sum().backward()
542+ dist_y.sum().backward()
543+ else:
544+ y.backward()
545+ dist_y.backward()
546+ self.assertEqual(comm_mode.get_total_counts(), 0)
547+ self.assertTrue(
548+ dist_x.grad.placements[0].is_shard(shard_dim)
549+ )
550+ self.assertEqual(dist_x.grad.full_tensor(), x.grad)
551+ x.grad.zero_()
552+ else:
553+ with self.assertRaisesRegex(
554+ ValueError,
555+ "loss_parallel",
556+ ):
557+ dist_y = F.cross_entropy(
558+ dist_x, target, reduction=reduction
559+ )
560+ 
561+ 
562+instantiate_parametrized_tests(DistTensorParallelExampleTest)
563+ 
564+if __name__ == "__main__":
565+ run_tests()