Skip to content

Commit e6ebee7

Browse files
committed
[distributed] Test tensor parallelism with context parallelism
`tp_size=2 x cp_size=2` on four ranks, plain and packed, against the same weights on the full sequences, plus the gathered save, the head divisibility check under TP, and a test that the two plans compose: the attention module whose call CP routes is the one whose projections TP sharded.
1 parent e5aeee2 commit e6ebee7

1 file changed

Lines changed: 87 additions & 15 deletions

File tree

‎tests/context_parallel/test_context_parallel.py‎

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import torch.distributed as dist
2424
from parameterized import parameterized
2525
from torch.distributed.device_mesh import init_device_mesh
26+
from torch.distributed.tensor import DTensor
2627

2728
from transformers import (
2829
AutoModelForCausalLM,
@@ -124,14 +125,16 @@ def _init_cp_mesh(device_type):
124125
def _tiny_config(**kwargs):
125126
"""A `LlamaConfig` small enough for CPU tests, in float32 with `sdpa` attention."""
126127
return LlamaConfig(
127-
hidden_size=32,
128-
intermediate_size=64,
129-
num_hidden_layers=2,
130-
num_attention_heads=4,
131-
num_key_value_heads=2,
132-
vocab_size=99,
133-
attn_implementation="sdpa",
134-
**kwargs,
128+
**{
129+
"hidden_size": 32,
130+
"intermediate_size": 64,
131+
"num_hidden_layers": 2,
132+
"num_attention_heads": 4,
133+
"num_key_value_heads": 2,
134+
"vocab_size": 99,
135+
"attn_implementation": "sdpa",
136+
**kwargs,
137+
}
135138
)
136139

137140

@@ -450,6 +453,26 @@ def test_refuses_indivisible_heads(self):
450453
with self.assertRaisesRegex(ValueError, "must divide both the number of attention heads"):
451454
self._check(config, cp_size=4)
452455

456+
def test_head_divisibility_covers_tensor_parallelism(self):
457+
# tensor parallelism keeps `num_heads / tp_size` heads on this rank, and those are what cp splits
458+
def config_with_heads(num_heads, num_kv_heads):
459+
config = LlamaConfig(
460+
hidden_size=32,
461+
num_attention_heads=num_heads,
462+
num_key_value_heads=num_kv_heads,
463+
attn_implementation="sdpa",
464+
)
465+
config.distributed_config = DistributedConfig(tp_size=2, cp_size=2)
466+
return config
467+
468+
self._check(config_with_heads(8, 4), cp_size=2)
469+
# 1 key/value head left on each rank cannot be split across 2 cp ranks
470+
with self.assertRaisesRegex(ValueError, r"`cp_size \* tp_size` must divide"):
471+
self._check(config_with_heads(8, 2), cp_size=2)
472+
# so few attention heads left on each rank that 2 cp ranks cannot split them either
473+
with self.assertRaisesRegex(ValueError, r"`cp_size \* tp_size` must divide"):
474+
self._check(config_with_heads(8, 4), cp_size=4)
475+
453476
def test_accepts_supported_config(self):
454477
self._check(LlamaConfig(attn_implementation="sdpa", num_attention_heads=8, num_key_value_heads=4))
455478
# the mask the model builds over the whole sequence already has the sliding window in it
@@ -668,6 +691,44 @@ def _compare_with_unsharded(init_dir, batch, distributed_config, keep=None):
668691
torch.testing.assert_close(grad, expected, rtol=CP_RTOL, atol=CP_ATOL, msg=f"Gradient mismatch for {name}")
669692

670693

694+
def _test_matches_unsharded_with_tp(rank, tp_size, cp_size, fsdp_size, packed):
695+
"""The tp ranks hold the same batch and the same cp slice, and split the heads before cp splits them again."""
696+
# 4 attention heads and 4 key/value heads, both divisible by `tp_size * cp_size` = 4
697+
config = _tiny_config(num_key_value_heads=4)
698+
batch_size, seq_len = 2, 8
699+
with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir:
700+
input_ids, position_ids = _tiny_batch(config, batch_size * fsdp_size, seq_len, packed)
701+
distributed_config = DistributedConfig(tp_size=tp_size, cp_size=cp_size, fsdp_size=fsdp_size)
702+
batch = {"input_ids": input_ids, "labels": input_ids.clone(), "use_cache": False}
703+
if position_ids is not None:
704+
batch["position_ids"] = position_ids
705+
_compare_with_unsharded(init_dir, batch, distributed_config)
706+
dist.barrier()
707+
708+
709+
def _test_tp_and_cp_compose(rank, tp_size, cp_size):
710+
"""The tp plan shards the projections of an attention layer, the cp plan registers its attention call."""
711+
config = _tiny_config(num_key_value_heads=4)
712+
with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir:
713+
distributed_config = DistributedConfig(tp_size=tp_size, cp_size=cp_size)
714+
model = AutoModelForCausalLM.from_pretrained(init_dir, distributed_config=distributed_config)
715+
attention = model.model.layers[0].self_attn
716+
# tensor parallelism sharded the projections of the very module whose forward context parallelism wrapped
717+
assert isinstance(attention.q_proj.weight, DTensor), type(attention.q_proj.weight)
718+
assert hasattr(attention.forward, "__wrapped__"), "the attention style was not installed"
719+
720+
# and one forward goes through both: the logits of this rank's slice match the unsharded run
721+
input_ids, _ = _tiny_batch(config, 2, 8)
722+
with torch.no_grad():
723+
logits = model(input_ids=input_ids).logits
724+
reference = AutoModelForCausalLM.from_pretrained(init_dir)
725+
with torch.no_grad():
726+
expected = reference(input_ids=input_ids).logits
727+
cp_rank = model._mesh_manager.get_mesh("cp").get_local_rank()
728+
torch.testing.assert_close(logits, expected.chunk(cp_size, dim=1)[cp_rank], rtol=CP_RTOL, atol=CP_ATOL)
729+
dist.barrier()
730+
731+
671732
def _test_matches_unsharded(rank, cp_size, fsdp_size, dp_replicate_size, packed, seq_len=8):
672733
config = _tiny_config()
673734
if cp_size > 2: # Ulysses splits the heads over the ranks, so four ranks need four key/value heads
@@ -757,15 +818,16 @@ def _test_custom_attention_style(rank, cp_size):
757818
dist.barrier()
758819

759820

760-
def _test_save_pretrained(rank, cp_size):
821+
def _test_save_pretrained(rank, cp_size, tp_size=1):
761822
"""Weights saved from a context parallel model reload without a distributed config, unchanged."""
762-
config = _tiny_config()
823+
config = _tiny_config(num_key_value_heads=4 if tp_size > 1 else 2)
763824
with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir:
764825
reference = AutoModelForCausalLM.from_pretrained(init_dir)
765826
expected = {name: value.clone() for name, value in reference.state_dict().items()}
766827
del reference
767828

768-
model = AutoModelForCausalLM.from_pretrained(init_dir, distributed_config=DistributedConfig(cp_size=cp_size))
829+
distributed_config = DistributedConfig(cp_size=cp_size, tp_size=tp_size)
830+
model = AutoModelForCausalLM.from_pretrained(init_dir, distributed_config=distributed_config)
769831
with _distributed_tmpdir(rank) as save_dir:
770832
model.save_pretrained(save_dir, is_main_process=(rank == 0))
771833
dist.barrier()
@@ -799,10 +861,9 @@ def _test_save_load_distributed_checkpoint(rank, cp_size):
799861

800862
@unittest.skipUnless(is_torch_greater_or_equal("2.10"), "Context parallelism requires torch>=2.10")
801863
class TestContextParallelConfig(unittest.TestCase):
802-
def test_refuses_other_parallelisms(self):
803-
for other in ({"pp_size": 2}, {"tp_size": 2}, {"ep_size": 2}):
804-
with self.subTest(**other), self.assertRaisesRegex(ValueError, "not supported yet with"):
805-
DistributedConfig(cp_size=2, **other)
864+
def test_refuses_pipeline_parallelism(self):
865+
with self.assertRaisesRegex(ValueError, "not supported yet with `pp_size`"):
866+
DistributedConfig(cp_size=2, pp_size=2)
806867

807868
def test_refuses_a_model_without_a_cp_plan(self):
808869
config = _tiny_config()
@@ -905,6 +966,14 @@ def test_sliding_window_and_full_layers(self):
905966
)
906967
run_distributed(world_size=2, backend="gloo")(_test_padded)(2, config, False)
907968

969+
def test_matches_unsharded_with_tensor_parallelism(self):
970+
for packed in (False, True):
971+
with self.subTest(packed=packed):
972+
run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_tp)(2, 2, 1, packed)
973+
974+
def test_tensor_parallelism_and_context_parallelism_compose(self):
975+
run_distributed(world_size=4, backend="gloo")(_test_tp_and_cp_compose)(2, 2)
976+
908977
def test_inputs_embeds(self):
909978
run_distributed(world_size=2, backend="gloo")(_test_inputs_embeds)(2)
910979

@@ -914,5 +983,8 @@ def test_custom_attention_style(self):
914983
def test_save_pretrained(self):
915984
run_distributed(world_size=2, backend="gloo")(_test_save_pretrained)(2)
916985

986+
def test_save_pretrained_with_tensor_parallelism(self):
987+
run_distributed(world_size=4, backend="gloo")(_test_save_pretrained)(2, 2)
988+
917989
def test_save_load_distributed_checkpoint(self):
918990
run_distributed(world_size=2, backend="gloo")(_test_save_load_distributed_checkpoint)(2)

0 commit comments

Comments
 (0)