Skip to content

Commit 4a238a2

Browse files
committed
[distributed] Test expert parallelism with context parallelism
The tiny Qwen3-MoE against the same weights on the full sequences, with token dispatch on `cp2 x ep2`, `cp2 x fsdp2 x ep4`, `cp2 x fsdp2 x ep2` (efsdp), `cp2 x tp2 x ep4` and `cp2 x hsdp2 x ep2`, plus the all-reduce plan with `ep_size=tp_size=2` where the ep group holds one cp slice. Every case stays within four ranks.
1 parent d7cd063 commit 4a238a2

1 file changed

Lines changed: 87 additions & 1 deletion

File tree

‎tests/context_parallel/test_context_parallel.py‎

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
LlamaForSequenceClassification,
3434
LlamaModel,
3535
MistralConfig,
36+
Qwen3MoeConfig,
3637
)
3738
from transformers.distributed import DistributedConfig
3839
from transformers.distributed.context_parallel import (
@@ -580,6 +581,9 @@ def test_styles_are_registered(self):
580581

581582
CP_RTOL = 1e-5
582583
CP_ATOL = 1e-5
584+
# The all-reduce plan of `Qwen3Moe`, which overrides the token-dispatch rules of its own `ep_plan`: the router masks the
585+
# experts of the other ranks and the partial expert outputs are all-reduced.
586+
ALL_REDUCE_EP_PLAN = {"model.layers.*.mlp.gate": "ep_router", "model.layers.*.mlp.experts": "moe_tp_experts"}
583587

584588

585589
def _tiny_batch(config, batch_size, seq_len, packed=False, seed=0):
@@ -679,6 +683,7 @@ def _compare_with_unsharded(init_dir, batch, distributed_config, keep=None):
679683
continue
680684
grad = _average_over_group(grad, groups, group_size)
681685
torch.testing.assert_close(grad, expected, rtol=CP_RTOL, atol=CP_ATOL, msg=f"Gradient mismatch for {name}")
686+
return model
682687

683688

684689
def _test_matches_unsharded_with_tp(rank, tp_size, cp_size, fsdp_size, packed):
@@ -758,6 +763,64 @@ def _test_padded(rank, cp_size, config=None, packed=False):
758763
dist.barrier()
759764

760765

766+
def _tiny_moe_config(**kwargs):
767+
"""A `Qwen3MoeConfig` small enough for CPU tests, whose default `ep_plan` dispatches the tokens to the experts."""
768+
return Qwen3MoeConfig(
769+
**{
770+
"hidden_size": 32,
771+
"moe_intermediate_size": 16,
772+
"num_hidden_layers": 2,
773+
"num_attention_heads": 4,
774+
"num_key_value_heads": 4,
775+
"vocab_size": 99,
776+
"num_experts": 8,
777+
"num_experts_per_tok": 2,
778+
"attn_implementation": "sdpa",
779+
**kwargs,
780+
}
781+
)
782+
783+
784+
def _test_matches_unsharded_with_ep(rank, cp_size, fsdp_size, ep_size, tp_size=1, dp_replicate_size=1):
785+
"""
786+
Token dispatch with context parallelism: every rank sends the tokens of its own slice to the owners of the experts
787+
they selected, and the experts' gradients reduce over the ranks that hold a copy of them.
788+
"""
789+
config = _tiny_moe_config()
790+
batch_size, seq_len = 2, 8
791+
with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir:
792+
rows = batch_size * fsdp_size * dp_replicate_size
793+
input_ids, _ = _tiny_batch(config, rows, seq_len)
794+
distributed_config = DistributedConfig(
795+
cp_size=cp_size,
796+
fsdp_size=fsdp_size,
797+
ep_size=ep_size,
798+
tp_size=tp_size,
799+
dp_replicate_size=dp_replicate_size,
800+
)
801+
batch = {"input_ids": input_ids, "labels": input_ids.clone(), "use_cache": False}
802+
model = _compare_with_unsharded(init_dir, batch, distributed_config)
803+
assert model.model.layers[0].mlp.experts.num_experts == config.num_experts // ep_size
804+
dist.barrier()
805+
806+
807+
def _test_matches_unsharded_with_all_reduce_ep(rank, cp_size, ep_size, tp_size):
808+
"""
809+
All-reduce experts need every rank of an ep group to see the same tokens: with `ep_size == tp_size` that group is
810+
the tp group, whose ranks hold the same cp slice.
811+
"""
812+
config = _tiny_moe_config()
813+
batch_size, seq_len = 2, 8
814+
with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir:
815+
input_ids, _ = _tiny_batch(config, batch_size, seq_len)
816+
distributed_config = DistributedConfig(
817+
cp_size=cp_size, ep_size=ep_size, tp_size=tp_size, ep_plan=ALL_REDUCE_EP_PLAN
818+
)
819+
batch = {"input_ids": input_ids, "labels": input_ids.clone(), "use_cache": False}
820+
_compare_with_unsharded(init_dir, batch, distributed_config)
821+
dist.barrier()
822+
823+
761824
def _test_inputs_embeds(rank, cp_size):
762825
"""`inputs_embeds` is sharded along its sequence dim, not along its last, hidden one."""
763826
config = _tiny_config()
@@ -852,7 +915,7 @@ def _test_save_load_distributed_checkpoint(rank, cp_size):
852915
@unittest.skipUnless(is_torch_greater_or_equal("2.10"), "Context parallelism requires torch>=2.10")
853916
class TestContextParallelConfig(unittest.TestCase):
854917
def test_refuses_pipeline_parallelism(self):
855-
with self.assertRaisesRegex(ValueError, "not supported yet with `pp_size`"):
918+
with self.assertRaisesRegex(ValueError, "not supported yet with pipeline parallelism"):
856919
DistributedConfig(cp_size=2, pp_size=2)
857920

858921
def test_refuses_a_model_without_a_cp_plan(self):
@@ -950,6 +1013,29 @@ def test_matches_unsharded_with_tensor_parallelism(self):
9501013
def test_tensor_parallelism_and_context_parallelism_compose(self):
9511014
run_distributed(world_size=4, backend="gloo")(_test_tp_and_cp_compose)(2, 2)
9521015

1016+
def test_matches_unsharded_with_expert_parallelism(self):
1017+
run_distributed(world_size=2, backend="gloo")(_test_matches_unsharded_with_ep)(2, 1, 2)
1018+
1019+
def test_matches_unsharded_with_fsdp_and_expert_parallelism(self):
1020+
# ep_size = fsdp_size * cp_size, so efsdp_size is 1 and every expert lives whole on a single rank
1021+
run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_ep)(2, 2, 4)
1022+
1023+
def test_matches_unsharded_with_expert_fsdp(self):
1024+
# efsdp_size = fsdp_size * cp_size * tp_size / ep_size = 2: pairs of ranks hold the same experts, sharded
1025+
# between them
1026+
run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_ep)(2, 2, 2)
1027+
1028+
def test_matches_unsharded_with_tensor_parallelism_and_expert_parallelism(self):
1029+
# ep_size = 2 * tp_size puts both cp slices of the batch, two ranks each, in the same ep group
1030+
run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_ep)(2, 1, 4, 2)
1031+
1032+
def test_matches_unsharded_with_hsdp_and_expert_parallelism(self):
1033+
# the expert gradient reduces over efsdp and then over the two replicas: dp_size = 2 * 1 * 2 ranks
1034+
run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_ep)(2, 1, 2, 1, 2)
1035+
1036+
def test_matches_unsharded_with_all_reduce_expert_parallelism(self):
1037+
run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_all_reduce_ep)(2, 2, 2)
1038+
9531039
def test_inputs_embeds(self):
9541040
run_distributed(world_size=2, backend="gloo")(_test_inputs_embeds)(2)
9551041

0 commit comments

Comments
 (0)