|
28 | 28 | LlamaForSequenceClassification, |
29 | 29 | LlamaModel, |
30 | 30 | MistralConfig, |
| 31 | + Qwen3MoeConfig, |
31 | 32 | ) |
32 | 33 | from transformers.distributed import DistributedConfig, context_parallel |
33 | 34 | from transformers.distributed.context_parallel import ( |
@@ -446,6 +447,9 @@ def test_packed_flash_attention(self): |
446 | 447 |
|
447 | 448 | CP_RTOL = 1e-5 |
448 | 449 | CP_ATOL = 1e-5 |
| 450 | +# The all-reduce plan of `Qwen3Moe`, which overrides the token-dispatch rules of its own `ep_plan`: the router masks the |
| 451 | +# experts of the other ranks and the partial expert outputs are all-reduced. |
| 452 | +ALL_REDUCE_EP_PLAN = {"model.layers.*.mlp.gate": "ep_router", "model.layers.*.mlp.experts": "moe_tp_experts"} |
449 | 453 |
|
450 | 454 |
|
451 | 455 | def _tiny_batch(config, batch_size, seq_len, packed=False, seed=0): |
@@ -531,6 +535,7 @@ def _compare_with_unsharded(init_dir, batch, distributed_config, model_class=Aut |
531 | 535 | continue |
532 | 536 | grad = _average_over_data_parallel(grad, dp_group, fsdp_size * dp_replicate_size) |
533 | 537 | torch.testing.assert_close(grad, expected, rtol=CP_RTOL, atol=CP_ATOL, msg=f"Gradient mismatch for {name}") |
| 538 | + return model |
534 | 539 |
|
535 | 540 |
|
536 | 541 | def _test_matches_unsharded(rank, cp_size, fsdp_size, dp_replicate_size, packed): |
@@ -565,6 +570,64 @@ def _test_matches_unsharded_with_tp(rank, tp_size, cp_size, fsdp_size, packed): |
565 | 570 | dist.barrier() |
566 | 571 |
|
567 | 572 |
|
| 573 | +def _tiny_moe_config(**kwargs): |
| 574 | + """A `Qwen3MoeConfig` small enough for CPU tests, whose default `ep_plan` dispatches the tokens to the experts.""" |
| 575 | + return Qwen3MoeConfig( |
| 576 | + **{ |
| 577 | + "hidden_size": 32, |
| 578 | + "moe_intermediate_size": 16, |
| 579 | + "num_hidden_layers": 2, |
| 580 | + "num_attention_heads": 4, |
| 581 | + "num_key_value_heads": 4, |
| 582 | + "vocab_size": 99, |
| 583 | + "num_experts": 8, |
| 584 | + "num_experts_per_tok": 2, |
| 585 | + "attn_implementation": "sdpa", |
| 586 | + **kwargs, |
| 587 | + } |
| 588 | + ) |
| 589 | + |
| 590 | + |
| 591 | +def _test_matches_unsharded_with_ep(rank, cp_size, fsdp_size, ep_size, tp_size=1, dp_replicate_size=1): |
| 592 | + """ |
| 593 | + Token dispatch with context parallelism: every rank sends the tokens of its own slice to the owners of the experts |
| 594 | + they selected, and the experts' gradients reduce over the ranks that hold a copy of them. |
| 595 | + """ |
| 596 | + config = _tiny_moe_config() |
| 597 | + batch_size, seq_len = 2, 8 |
| 598 | + with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir: |
| 599 | + rows = batch_size * fsdp_size * dp_replicate_size |
| 600 | + input_ids, _ = _tiny_batch(config, rows, seq_len) |
| 601 | + distributed_config = DistributedConfig( |
| 602 | + cp_size=cp_size, |
| 603 | + fsdp_size=fsdp_size, |
| 604 | + ep_size=ep_size, |
| 605 | + tp_size=tp_size, |
| 606 | + dp_replicate_size=dp_replicate_size, |
| 607 | + ) |
| 608 | + batch = {"input_ids": input_ids, "labels": input_ids.clone(), "use_cache": False} |
| 609 | + model = _compare_with_unsharded(init_dir, batch, distributed_config) |
| 610 | + assert model.model.layers[0].mlp.experts.num_experts == config.num_experts // ep_size |
| 611 | + dist.barrier() |
| 612 | + |
| 613 | + |
| 614 | +def _test_matches_unsharded_with_all_reduce_ep(rank, cp_size, ep_size, tp_size): |
| 615 | + """ |
| 616 | + All-reduce experts need every rank of an ep group to see the same tokens: with `ep_size == tp_size` that group is |
| 617 | + the tp group, whose ranks hold the same cp slice. |
| 618 | + """ |
| 619 | + config = _tiny_moe_config() |
| 620 | + batch_size, seq_len = 2, 8 |
| 621 | + with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir: |
| 622 | + input_ids, _ = _tiny_batch(config, batch_size, seq_len) |
| 623 | + distributed_config = DistributedConfig( |
| 624 | + cp_size=cp_size, ep_size=ep_size, tp_size=tp_size, ep_plan=ALL_REDUCE_EP_PLAN |
| 625 | + ) |
| 626 | + batch = {"input_ids": input_ids, "labels": input_ids.clone(), "use_cache": False} |
| 627 | + _compare_with_unsharded(init_dir, batch, distributed_config) |
| 628 | + dist.barrier() |
| 629 | + |
| 630 | + |
568 | 631 | def _test_inputs_embeds(rank, cp_size): |
569 | 632 | """`inputs_embeds` is sharded along its sequence dim, not along its last, hidden one.""" |
570 | 633 | config = _tiny_config() |
@@ -726,17 +789,26 @@ def setUp(self): |
726 | 789 | if not is_torch_greater_or_equal("2.10"): |
727 | 790 | self.skipTest("Context parallelism requires torch>=2.10") |
728 | 791 |
|
729 | | - def test_refuses_other_parallelisms(self): |
730 | | - for other in ({"pp_size": 2}, {"ep_size": 2}): |
731 | | - with self.subTest(**other), self.assertRaisesRegex(ValueError, "not supported yet with"): |
732 | | - DistributedConfig(cp_size=2, **other) |
| 792 | + def test_refuses_pipeline_parallelism(self): |
| 793 | + with self.assertRaisesRegex(ValueError, "not supported yet with `pp_size=2`"): |
| 794 | + DistributedConfig(cp_size=2, pp_size=2) |
733 | 795 |
|
734 | 796 | def test_accepts_tensor_parallelism(self): |
735 | 797 | distributed_config = DistributedConfig(tp_size=2, cp_size=2) |
736 | 798 | self.assertEqual((distributed_config.tp_size, distributed_config.cp_size), (2, 2)) |
737 | 799 | # the tp ranks hold the same batch: they are not data parallel ranks |
738 | 800 | self.assertEqual(distributed_config.dp_size, 2) |
739 | 801 |
|
| 802 | + def test_accepts_expert_parallelism(self): |
| 803 | + distributed_config = DistributedConfig(cp_size=2, ep_size=2) |
| 804 | + self.assertEqual((distributed_config.cp_size, distributed_config.ep_size), (2, 2)) |
| 805 | + # the cp ranks are data parallel ranks, and the experts shard over the axis ep leaves of fsdp * cp * tp |
| 806 | + self.assertEqual(distributed_config.dp_size, 2) |
| 807 | + self.assertEqual(distributed_config.efsdp_size, 1) |
| 808 | + self.assertEqual(DistributedConfig(cp_size=2, fsdp_size=2, ep_size=2).efsdp_size, 2) |
| 809 | + with self.assertRaisesRegex(ValueError, r"must divide `fsdp_size \* cp_size \* tp_size`"): |
| 810 | + DistributedConfig(cp_size=2, ep_size=4) |
| 811 | + |
740 | 812 | def test_refuses_size_below_one(self): |
741 | 813 | with self.assertRaisesRegex(ValueError, "must be >= 1"): |
742 | 814 | DistributedConfig(cp_size=0) |
@@ -770,6 +842,29 @@ def test_matches_unsharded_with_tensor_parallelism(self): |
770 | 842 | def test_matches_unsharded_with_tensor_parallelism_and_fsdp(self): |
771 | 843 | run_distributed(world_size=8, backend="gloo")(_test_matches_unsharded_with_tp)(2, 2, 2, False) |
772 | 844 |
|
| 845 | + def test_matches_unsharded_with_expert_parallelism(self): |
| 846 | + run_distributed(world_size=2, backend="gloo")(_test_matches_unsharded_with_ep)(2, 1, 2) |
| 847 | + |
| 848 | + def test_matches_unsharded_with_fsdp_and_expert_parallelism(self): |
| 849 | + # ep_size = fsdp_size * cp_size, so efsdp_size is 1 and every expert lives whole on a single rank |
| 850 | + run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_ep)(2, 2, 4) |
| 851 | + |
| 852 | + def test_matches_unsharded_with_expert_fsdp(self): |
| 853 | + # efsdp_size = fsdp_size * cp_size * tp_size / ep_size = 2: pairs of ranks hold the same experts, sharded |
| 854 | + # between them |
| 855 | + run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_ep)(2, 2, 2) |
| 856 | + |
| 857 | + def test_matches_unsharded_with_tensor_parallelism_and_expert_parallelism(self): |
| 858 | + # ep_size = 2 * tp_size puts both cp slices of the batch, two ranks each, in the same ep group |
| 859 | + run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_ep)(2, 1, 4, 2) |
| 860 | + |
| 861 | + def test_matches_unsharded_with_hsdp_and_expert_parallelism(self): |
| 862 | + # the expert gradient reduces over efsdp and then over the two replicas: dp_size = 2 * 1 * 2 ranks |
| 863 | + run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_ep)(2, 1, 2, 1, 2) |
| 864 | + |
| 865 | + def test_matches_unsharded_with_all_reduce_expert_parallelism(self): |
| 866 | + run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded_with_all_reduce_ep)(2, 2, 2) |
| 867 | + |
773 | 868 | def test_inputs_embeds(self): |
774 | 869 | run_distributed(world_size=2, backend="gloo")(_test_inputs_embeds)(2) |
775 | 870 |
|
|
0 commit comments