|
33 | 33 | LlamaForSequenceClassification, |
34 | 34 | LlamaModel, |
35 | 35 | MistralConfig, |
| 36 | + Qwen3MoeConfig, |
36 | 37 | ) |
37 | 38 | from transformers.distributed import DistributedConfig |
38 | 39 | from transformers.distributed.context_parallel import ( |
@@ -580,6 +581,9 @@ def test_styles_are_registered(self): |
580 | 581 |
|
581 | 582 | CP_RTOL = 1e-5 |
582 | 583 | 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"} |
583 | 587 |
|
584 | 588 |
|
585 | 589 | 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): |
679 | 683 | continue |
680 | 684 | grad = _average_over_group(grad, groups, group_size) |
681 | 685 | torch.testing.assert_close(grad, expected, rtol=CP_RTOL, atol=CP_ATOL, msg=f"Gradient mismatch for {name}") |
| 686 | + return model |
682 | 687 |
|
683 | 688 |
|
684 | 689 | 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): |
758 | 763 | dist.barrier() |
759 | 764 |
|
760 | 765 |
|
| 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 | + |
761 | 824 | def _test_inputs_embeds(rank, cp_size): |
762 | 825 | """`inputs_embeds` is sharded along its sequence dim, not along its last, hidden one.""" |
763 | 826 | config = _tiny_config() |
@@ -852,7 +915,7 @@ def _test_save_load_distributed_checkpoint(rank, cp_size): |
852 | 915 | @unittest.skipUnless(is_torch_greater_or_equal("2.10"), "Context parallelism requires torch>=2.10") |
853 | 916 | class TestContextParallelConfig(unittest.TestCase): |
854 | 917 | 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"): |
856 | 919 | DistributedConfig(cp_size=2, pp_size=2) |
857 | 920 |
|
858 | 921 | def test_refuses_a_model_without_a_cp_plan(self): |
@@ -950,6 +1013,29 @@ def test_matches_unsharded_with_tensor_parallelism(self): |
950 | 1013 | def test_tensor_parallelism_and_context_parallelism_compose(self): |
951 | 1014 | run_distributed(world_size=4, backend="gloo")(_test_tp_and_cp_compose)(2, 2) |
952 | 1015 |
|
| 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 | + |
953 | 1039 | def test_inputs_embeds(self): |
954 | 1040 | run_distributed(world_size=2, backend="gloo")(_test_inputs_embeds)(2) |
955 | 1041 |
|
|
0 commit comments