2323import torch .distributed as dist
2424from parameterized import parameterized
2525from torch .distributed .device_mesh import init_device_mesh
26+ from torch .distributed .tensor import DTensor
2627
2728from transformers import (
2829 AutoModelForCausalLM ,
@@ -124,14 +125,16 @@ def _init_cp_mesh(device_type):
124125def _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+
671732def _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" )
801863class 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