[Distributed][MoonEP] BF16 integration of MoonEP balanced EP backend (#52101)

Signed-off-by: kevinli <306243285+kaijunli-infr@users.noreply.github.com> Co-authored-by: kevinli <306243285+kaijunli-infr@users.noreply.github.com>

Kevin Li committed Sep 18, 2026 at 07:34 UTC 911548247a7b04e0c9cffca1cfba66b7120d6d3f
15 files changed +1338 -12
tests/evals/gsm8k/configs/moe-refactor-dp-ep/Qwen3-30B-A3B-BF16-moonep.yaml new
+5
@@ -0,0 +1,5 @@
1 +model_name: "Qwen/Qwen3-30B-A3B"
2 +accuracy_threshold: 0.88
3 +num_questions: 1319
4 +num_fewshot: 5
5 +server_args: "--enforce-eager --max-model-len 8192 --data-parallel-size 2 --enable-expert-parallel --all2all-backend=moonep"
tests/evals/gsm8k/configs/moe-refactor-dp-ep/config-b200.txt
+1
@@ -10,3 +10,4 @@ Qwen3-30B-A3B-Fp8-CT-Block-deepgemm-deepep-ht.yaml
10 Qwen3-30B-A3B-Fp8-CT-Block-deepgemm-deepep-ll.yaml
11 Qwen3-30B-A3B-Fp8-CT-Block-deepgemm.yaml
12 Qwen3-30B-A3B-BF16-triton.yaml
13 +Qwen3-30B-A3B-BF16-moonep.yaml
tests/evals/gsm8k/test_gsm8k_correctness.py
+6
@@ -128,6 +128,12 @@ def test_gsm8k_correctness(config_filename):
128 "Skipping Qwen3.5-35B-A3B-MXFP4-AITER-TP2 on non-GFX950 platforms. "
129 "The quantization scheme is not supported on non-GFX950 platforms."
130 )
131 + if "--all2all-backend=moonep" in eval_config.get("server_args", ""):
132 + from vllm.utils.import_utils import has_moonep
133 +
134 + if not has_moonep():
135 + pytest.skip("Skipping MoonEP config: the moonep package is not installed.")
136 +
137 # Parse server arguments from config (use shlex to handle quoted strings)
138 server_args_str = eval_config.get("server_args", "")
139 server_args = shlex.split(server_args_str) if server_args_str else []
tests/kernels/moe/test_moonep_bf16_poc.py new
+412
@@ -0,0 +1,412 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Test MoonEP dispatch / prefetch / combine logic (BF16 PoC).
4 +
5 +Two paths, both compared against the pure-PyTorch reference MoE:
6 +- MoonEPPrepareAndFinalize + a reference segment-loop expert runner over
7 + MoonEP's expert-grouped ``[NvS, H]`` layout;
8 +- the full modular kernel, MoonEPPrepareAndFinalize + MoonEPExperts through
9 + FusedMoEKernel.
10 +Requires NVSwitch multicast capable GPUs.
11 +"""
12 +
13 +import dataclasses
14 +import types
15 +
16 +import pytest
17 +import torch
18 +import torch.nn.functional as F
19 +from torch.distributed import ProcessGroup
20 +
21 +from tests.kernels.moe.utils import make_test_weights
22 +from tests.kernels.utils import torch_experts
23 +from vllm.config import VllmConfig, set_current_vllm_config
24 +from vllm.model_executor.layers.fused_moe.activation import MoEActivation
25 +from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
26 + TopKWeightAndReduceNoOP,
27 +)
28 +from vllm.utils.import_utils import has_moonep
29 +from vllm.utils.torch_utils import set_random_seed
30 +
31 +from ...utils import multi_gpu_test
32 +from .parallel_utils import ProcessGroupInfo, parallel_launch
33 +
34 +if has_moonep():
35 + from vllm.model_executor.layers.fused_moe.prepare_finalize.moonep import (
36 + MoonEPExpertWeightLayout,
37 + MoonEPPrepareAndFinalize,
38 + make_moonep_weight_layout,
39 + )
40 +
41 +requires_moonep = pytest.mark.skipif(
42 + not has_moonep(),
43 + reason="Requires MoonEP",
44 +)
45 +
46 +
47 +class MulticastNotAvailableError(RuntimeError):
48 + pass
49 +
50 +
51 +@dataclasses.dataclass
52 +class TestConfig:
53 + topk: int
54 + m: int
55 + k: int
56 + n: int
57 + num_experts: int
58 + router_skew: float
59 + apply_router_weight_on_input: bool = False
60 +
61 +
62 +@dataclasses.dataclass
63 +class TestTensors:
64 + rank_tokens: torch.Tensor
65 + topk: torch.Tensor
66 + topk_weights: torch.Tensor
67 + config: TestConfig
68 +
69 + @staticmethod
70 + def make(config: TestConfig) -> "TestTensors":
71 + rank_tokens = (
72 + torch.randn((config.m, config.k), device="cuda", dtype=torch.bfloat16) / 10
73 + )
74 + # Skewed routing via a per-expert bias: scaling logits by a scalar
75 + # preserves their ordering and would not change the selected topk
76 + # ids, but a shared bias makes a few experts hot on every token so
77 + # the planner has to fill redundant-expert prefetch slots.
78 + expert_bias = config.router_skew * torch.randn(
79 + 1, config.num_experts, device="cuda", dtype=torch.float32
80 + )
81 + logits = (
82 + torch.randn(
83 + config.m, config.num_experts, device="cuda", dtype=torch.float32
84 + )
85 + + expert_bias
86 + )
87 + topk_weights, topk = torch.topk(logits, config.topk, dim=-1)
88 + topk_weights = torch.softmax(topk_weights, dim=-1)
89 + return TestTensors(
90 + rank_tokens=rank_tokens,
91 + topk=topk.to(dtype=torch.int64),
92 + topk_weights=topk_weights,
93 + config=config,
94 + )
95 +
96 +
97 +def reference_moonep_experts(
98 + hidden_nvsh: torch.Tensor,
99 + route_weights_nvs: torch.Tensor,
100 + cu_seqlens: torch.Tensor,
101 + weight_layout: "MoonEPExpertWeightLayout",
102 +) -> torch.Tensor:
103 + """Segment loop over MoonEP's expert-grouped layout.
104 +
105 + ``prefetch_weight`` has already materialized redundant experts' weights
106 + in rows ``[E, E+B)``, so every segment reads its own row. Route weights
107 + are applied here; MoonEP's combine does the K-sum.
108 + """
109 + output = torch.empty_like(hidden_nvsh)
110 + prev = 0
111 + for row, cur in enumerate(cu_seqlens.tolist()):
112 + if cur == prev:
113 + continue
114 + x = hidden_nvsh[prev:cur]
115 + gate = F.linear(x, weight_layout.full_gate_weight[row])
116 + up = F.linear(x, weight_layout.full_up_weight[row])
117 + y = F.linear(F.silu(gate) * up, weight_layout.full_down_weight[row])
118 + y = y * route_weights_nvs[prev:cur].to(dtype=y.dtype).unsqueeze(-1)
119 + output[prev:cur].copy_(y)
120 + prev = cur
121 + if prev < hidden_nvsh.shape[0]:
122 + output[prev:].zero_()
123 + return output
124 +
125 +
126 +def make_moonep_prepare_finalize(
127 + pg: ProcessGroup,
128 + pgi: ProcessGroupInfo,
129 + hidden_size: int,
130 + num_experts: int,
131 + topk: int,
132 + max_tokens_per_rank: int,
133 + weight_layout: "MoonEPExpertWeightLayout",
134 + pass_layout_to_pf: bool = True,
135 +):
136 + from moonep._C import nvl_multicast_supported
137 +
138 + from vllm.model_executor.layers.fused_moe.prepare_finalize.moonep import (
139 + MoonEPBufferPool,
140 + )
141 +
142 + if not nvl_multicast_supported():
143 + raise MulticastNotAvailableError("NVSwitch multicast not available")
144 +
145 + pool = MoonEPBufferPool(
146 + dict(
147 + H=hidden_size,
148 + K=topk,
149 + E=num_experts,
150 + num_ep_ranks=pgi.world_size,
151 + B=weight_layout.num_prefetch_slots,
152 + group=pg,
153 + explicitly_destroy=True,
154 + ),
155 + max_tokens_per_rank=max_tokens_per_rank,
156 + )
157 + return pool, MoonEPPrepareAndFinalize(
158 + buffer_pool=pool,
159 + max_tokens_per_rank=max_tokens_per_rank,
160 + num_dispatchers=pgi.world_size,
161 + num_global_experts=num_experts,
162 + weight_layout=weight_layout if pass_layout_to_pf else None,
163 + )
164 +
165 +
166 +def moonep_moe_impl(
167 + pg: ProcessGroup,
168 + pgi: ProcessGroupInfo,
169 + test_tensors: TestTensors,
170 + w1: torch.Tensor,
171 + w2: torch.Tensor,
172 + num_prefetch_slots: int,
173 +) -> torch.Tensor:
174 + config = test_tensors.config
175 + hidden_size = test_tensors.rank_tokens.shape[1]
176 + max_tokens_per_rank = 128 * ((config.m + 127) // 128)
177 +
178 + weight_layout = make_moonep_weight_layout(w1, w2, num_prefetch_slots)
179 + pool, pf = make_moonep_prepare_finalize(
180 + pg,
181 + pgi,
182 + hidden_size,
183 + config.num_experts,
184 + config.topk,
185 + max_tokens_per_rank,
186 + weight_layout,
187 + )
188 + try:
189 + hidden_nvsh, _, _, _, route_weights_nvs = pf.prepare(
190 + test_tensors.rank_tokens,
191 + test_tensors.topk_weights,
192 + test_tensors.topk,
193 + num_experts=config.num_experts,
194 + expert_map=None,
195 + apply_router_weight_on_input=config.apply_router_weight_on_input,
196 + quant_config=_no_quant_config(),
197 + )
198 + assert route_weights_nvs is not None
199 + if config.apply_router_weight_on_input:
200 + # prepare() already applied the weights to the inputs; the
201 + # runner must not apply them again.
202 + route_weights_nvs = torch.ones_like(route_weights_nvs)
203 + if config.router_skew >= 8 and config.m >= 100:
204 + # Heavy skew must actually engage the redundant-expert planner.
205 + assert (pf.plan.experts_to_copy >= 0).any(), (
206 + "no prefetch slot used despite heavy router skew"
207 + )
208 + expert_out = reference_moonep_experts(
209 + hidden_nvsh, route_weights_nvs, pf.cu_seqlens, weight_layout
210 + )
211 + output = torch.empty_like(test_tensors.rank_tokens)
212 + pf.finalize(
213 + output,
214 + expert_out,
215 + test_tensors.topk_weights,
216 + test_tensors.topk,
217 + apply_router_weight_on_input=False,
218 + weight_and_reduce_impl=TopKWeightAndReduceNoOP(),
219 + )
220 + torch.accelerator.synchronize()
221 + return output
222 + finally:
223 + pool.destroy()
224 +
225 +
226 +def _no_quant_config():
227 + from vllm.model_executor.layers.fused_moe.config import FusedMoEQuantConfig
228 +
229 + return FusedMoEQuantConfig.make(quant_dtype=None)
230 +
231 +
232 +def moonep_modular_kernel_impl(
233 + pg: ProcessGroup,
234 + pgi: ProcessGroupInfo,
235 + test_tensors: TestTensors,
236 + w1: torch.Tensor,
237 + w2: torch.Tensor,
238 + num_prefetch_slots: int,
239 +) -> torch.Tensor:
240 + """Full modular-kernel path: MoonEPPrepareAndFinalize + MoonEPExperts."""
241 + from tests.kernels.moe.utils import make_dummy_moe_config
242 + from vllm.model_executor.layers.fused_moe.experts.moonep_experts import (
243 + MoonEPExperts,
244 + )
245 + from vllm.model_executor.layers.fused_moe.modular_kernel import FusedMoEKernel
246 +
247 + config = test_tensors.config
248 + hidden_size = test_tensors.rank_tokens.shape[1]
249 + max_tokens_per_rank = 128 * ((config.m + 127) // 128)
250 +
251 + weight_layout = make_moonep_weight_layout(w1, w2, num_prefetch_slots)
252 + # weight_layout deliberately not passed to the P/F: the engine path
253 + # resolves it through post_init_setup + the experts' weight hook.
254 + pool, pf = make_moonep_prepare_finalize(
255 + pg,
256 + pgi,
257 + hidden_size,
258 + config.num_experts,
259 + config.topk,
260 + max_tokens_per_rank,
261 + weight_layout,
262 + pass_layout_to_pf=False,
263 + )
264 + try:
265 + moe_config = make_dummy_moe_config(
266 + num_experts=config.num_experts,
267 + experts_per_token=config.topk,
268 + hidden_dim=hidden_size,
269 + intermediate_size=config.n,
270 + max_num_tokens=max_tokens_per_rank,
271 + )
272 + experts = MoonEPExperts(moe_config=moe_config, quant_config=_no_quant_config())
273 + # Stand-in for the layer carrying the layout, as
274 + # convert_to_unquantized_kernel_format leaves it in the engine path.
275 + fake_layer = types.SimpleNamespace(_moonep_weight_layout=weight_layout)
276 + experts.process_weights_after_loading(fake_layer)
277 + kernel = FusedMoEKernel(prepare_finalize=pf, fused_experts=experts)
278 + out = kernel.apply(
279 + hidden_states=test_tensors.rank_tokens,
280 + w1=weight_layout.full_gate_weight,
281 + w2=weight_layout.full_down_weight,
282 + topk_weights=test_tensors.topk_weights,
283 + topk_ids=test_tensors.topk,
284 + activation=MoEActivation.SILU,
285 + global_num_experts=config.num_experts,
286 + expert_map=None,
287 + apply_router_weight_on_input=config.apply_router_weight_on_input,
288 + )
289 + torch.accelerator.synchronize()
290 + return out
291 + finally:
292 + pool.destroy()
293 +
294 +
295 +def _moonep_moe(
296 + pgi: ProcessGroupInfo,
297 + config: TestConfig,
298 + w1: torch.Tensor,
299 + w2: torch.Tensor,
300 + num_prefetch_slots: int,
301 + use_modular_kernel: bool,
302 +):
303 + from vllm.v1.worker.workspace import init_workspace_manager
304 +
305 + device_idx = torch.accelerator.current_device_index()
306 + init_workspace_manager(torch.device("cuda", device_idx))
307 + w1 = w1.to(device=device_idx)
308 + w2 = w2.to(device=device_idx)
309 +
310 + pg = torch.distributed.new_group(list(range(pgi.world_size)))
311 + set_random_seed(7 + pgi.rank)
312 + test_tensors = TestTensors.make(config)
313 +
314 + with set_current_vllm_config(VllmConfig()):
315 + torch_combined = torch_experts(
316 + test_tensors.rank_tokens,
317 + w1,
318 + w2,
319 + test_tensors.topk_weights,
320 + test_tensors.topk,
321 + apply_router_weights_on_input=config.apply_router_weight_on_input,
322 + )
323 + impl = moonep_modular_kernel_impl if use_modular_kernel else moonep_moe_impl
324 + moonep_combined = impl(pg, pgi, test_tensors, w1, w2, num_prefetch_slots)
325 +
326 + torch.testing.assert_close(
327 + torch_combined,
328 + moonep_combined,
329 + atol=6e-2,
330 + rtol=6e-2,
331 + )
332 +
333 +
334 +MNKs = [
335 + (1, 256, 512),
336 + (37, 256, 512),
337 + (100, 512, 1024),
338 + (512, 768, 2048),
339 +]
340 +
341 +
342 +@pytest.mark.parametrize("m,n,k", MNKs)
343 +@pytest.mark.parametrize("num_experts", [32])
344 +@pytest.mark.parametrize("topk", [4])
345 +@pytest.mark.parametrize("router_skew", [1.0, 8.0])
346 +@pytest.mark.parametrize("num_prefetch_slots", [4])
347 +@pytest.mark.parametrize("world_size", [2])
348 +@pytest.mark.parametrize("use_modular_kernel", [False, True])
349 +@multi_gpu_test(num_gpus=2)
350 +@requires_moonep
351 +def test_moonep_bf16_moe(
352 + m: int,
353 + n: int,
354 + k: int,
355 + num_experts: int,
356 + topk: int,
357 + router_skew: float,
358 + num_prefetch_slots: int,
359 + world_size: int,
360 + use_modular_kernel: bool,
361 +):
362 + set_random_seed(7)
363 + config = TestConfig(
364 + topk=topk,
365 + m=m,
366 + k=k,
367 + n=n,
368 + num_experts=num_experts,
369 + router_skew=router_skew,
370 + )
371 + (_, w1, _, _), (_, w2, _, _) = make_test_weights(num_experts, n, k)
372 +
373 + try:
374 + parallel_launch(
375 + world_size,
376 + _moonep_moe,
377 + config,
378 + w1,
379 + w2,
380 + num_prefetch_slots,
381 + use_modular_kernel,
382 + )
383 + except Exception as exc:
384 + if "MulticastNotAvailableError" in str(exc):
385 + pytest.skip("NVSwitch multicast not available")
386 + raise
387 +
388 +
389 +@pytest.mark.parametrize("use_modular_kernel", [False, True])
390 +@multi_gpu_test(num_gpus=2)
391 +@requires_moonep
392 +def test_moonep_input_weighted_moe(use_modular_kernel: bool):
393 + """Llama-4-style routing applies route weights to the expert inputs."""
394 + set_random_seed(7)
395 + config = TestConfig(
396 + topk=1,
397 + m=100,
398 + k=1024,
399 + n=512,
400 + num_experts=32,
401 + router_skew=1.0,
402 + apply_router_weight_on_input=True,
403 + )
404 + (_, w1, _, _), (_, w2, _, _) = make_test_weights(
405 + config.num_experts, config.n, config.k
406 + )
407 + try:
408 + parallel_launch(2, _moonep_moe, config, w1, w2, 4, use_modular_kernel)
409 + except Exception as exc:
410 + if "MulticastNotAvailableError" in str(exc):
411 + pytest.skip("NVSwitch multicast not available")
412 + raise
vllm/config/parallel.py
+2
@@ -49,6 +49,7 @@ All2AllBackend = Literal[
49 "deepep_v2",
50 "mori_high_throughput",
51 "mori_low_latency",
52 + "moonep",
53 "nixl_ep",
54 "allgather_reducescatter",
55 "flashinfer_all2allv", # temporary alias for flashinfer_nvlink_two_sided
@@ -206,6 +207,7 @@ class ParallelConfig:
207 - "deepep_low_latency": Use deepep low-latency kernels
208 - "mori_high_throughput": MoRI EP with InterNodeV1 for multi-node
209 - "mori_low_latency": MoRI EP with InterNodeV1LL for multi-node
210 + - "moonep": MoonEP balanced EP with dynamic redundant experts (NVLink)
211 - "nixl_ep": Use nixl-ep kernels
212 - "flashinfer_nvlink_one_sided": Use flashinfer high-throughput a2a kernels
213 - "flashinfer_nvlink_two_sided": Use flashinfer two-sided kernels for mnnvl"""
vllm/config/vllm.py
+49
@@ -1881,6 +1881,55 @@ class VllmConfig:
1881 # (e.g., XPU may lower max_num_batched_tokens when MLA is enabled)
1882 self._set_compile_ranges()
1883
1884 + if self.parallel_config.all2all_backend == "moonep":
1885 + if (
1886 + self.model_config is not None
1887 + and self.model_config.quantization is not None
1888 + ):
1889 + raise ValueError(
1890 + "The moonep all2all backend currently supports unquantized "
1891 + "BF16 models only; got "
1892 + f"quantization={self.model_config.quantization!r}. Use a "
1893 + "different --all2all-backend for quantized models."
1894 + )
1895 + if (
1896 + self.model_config is not None
1897 + and self.model_config.dtype != torch.bfloat16
1898 + ):
1899 + raise ValueError(
1900 + "The moonep all2all backend currently supports BF16 models "
1901 + f"only; got dtype={self.model_config.dtype}. Use a "
1902 + "different --all2all-backend or --dtype bfloat16."
1903 + )
1904 + if self.parallel_config.enable_eplb:
1905 + raise ValueError(
1906 + "The moonep all2all backend does not support EPLB yet: "
1907 + "EPLB rearranges expert parameters in a layout MoonEP's "
1908 + "replicated [E+B] weights do not follow. Disable "
1909 + "--enable-eplb or use a different --all2all-backend."
1910 + )
1911 + if self.parallel_config.expert_placement_strategy != "linear":
1912 + raise ValueError(
1913 + "The moonep all2all backend requires linear expert "
1914 + "placement: its load-time all-gather assumes each rank "
1915 + "holds a contiguous chunk of the global expert range. Got "
1916 + "--expert-placement-strategy "
1917 + f"{self.parallel_config.expert_placement_strategy!r}."
1918 + )
1919 + # Enforced here rather than in set_splitting_ops_for_v1 so it
1920 + # holds for every compilation mode, and keyed on use_all2all so
1921 + # PCP/SP-only topologies are covered too: MoonEP dispatch/combine
1922 + # are eager-only and must not be captured.
1923 + if (
1924 + self.parallel_config.use_all2all
1925 + and self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE
1926 + ):
1927 + logger.info(
1928 + "MoonEP: Disabling CUDA Graphs since the MoonEP "
1929 + "integration is currently eager-only."
1930 + )
1931 + self.compilation_config.cudagraph_mode = CUDAGraphMode.NONE
1932 +
1933 # Do this after all the updates to compilation_config.mode
1934 effective_dp_size = (
1935 self.parallel_config.data_parallel_size
vllm/distributed/device_communicators/all2all.py
+74 -1
@@ -21,7 +21,12 @@ from vllm.utils.flashinfer import (
21 has_flashinfer_nvlink_two_sided,
22 )
23 from vllm.utils.func_utils import supports_kw
24 -from vllm.utils.import_utils import has_deep_ep, has_deep_ep_v2, has_mori
24 +from vllm.utils.import_utils import (
25 + check_moonep_system_support,
26 + has_deep_ep,
27 + has_deep_ep_v2,
28 + has_mori,
29 +)
30
31 from .base_device_communicator import All2AllManagerBase, Cache
32
@@ -1094,3 +1099,71 @@ class DeepEPV2All2AllManager(All2AllManagerBase):
1099 for _, handle in self.handle_cache._cache.items():
1100 handle.destroy()
1101 self.handle_cache._cache.clear()
1102 +
1103 +
1104 +class MoonEPAll2AllManager(All2AllManagerBase):
1105 + """All2All communication based on MoonEP
1106 + (https://github.com/MoonshotAI/MoonEP).
1107 +
1108 + MoonEP keeps token loads perfectly balanced across EP ranks by planning
1109 + a small number of dynamically redundant experts online and prefetching
1110 + their weights before expert compute. Every rank receives exactly
1111 + S x K token slots regardless of router skew, so all communication and
1112 + compute shapes are static.
1113 +
1114 + Requires NVLink symmetric-memory / multicast capable topologies
1115 + (single node NVSwitch, e.g. H100/H200/B200/GB300 class).
1116 + """
1117 +
1118 + def __init__(self, cpu_group, tcp_store_group=None, device_group=None):
1119 + check_moonep_system_support()
1120 + super().__init__(cpu_group, tcp_store_group)
1121 + self._device_group = device_group
1122 + self.handle_cache = Cache()
1123 +
1124 + def _make_buffer_kwargs(
1125 + self,
1126 + max_num_tokens_per_dp_rank: int,
1127 + token_hidden_size: int,
1128 + num_topk: int,
1129 + num_global_experts: int,
1130 + num_prefetch_slots: int,
1131 + token_padding: int,
1132 + num_sms: int,
1133 + ) -> dict:
1134 + return dict(
1135 + S=max_num_tokens_per_dp_rank,
1136 + H=token_hidden_size,
1137 + K=num_topk,
1138 + E=num_global_experts,
1139 + num_ep_ranks=self.world_size,
1140 + num_sms=num_sms,
1141 + token_padding=token_padding,
1142 + B=num_prefetch_slots,
1143 + group=self._device_group
1144 + if self._device_group is not None
1145 + else self.cpu_group,
1146 + explicitly_destroy=True,
1147 + )
1148 +
1149 + def get_handle(self, kwargs):
1150 + from vllm.model_executor.layers.fused_moe.prepare_finalize.moonep import (
1151 + MoonEPBufferPool,
1152 + )
1153 +
1154 + buffer_kwargs = self._make_buffer_kwargs(**kwargs)
1155 + logger.debug("MoonEP all2all args %s", buffer_kwargs)
1156 +
1157 + def make_pool(**kw):
1158 + return MoonEPBufferPool(kw, max_tokens_per_rank=kw["S"])
1159 +
1160 + handle: MoonEPBufferPool = self.handle_cache.get_or_create(
1161 + buffer_kwargs, make_pool
1162 + )
1163 + return handle
1164 +
1165 + def destroy(self):
1166 + with self.handle_cache._lock:
1167 + for _, handle in self.handle_cache._cache.items():
1168 + handle.destroy()
1169 + self.handle_cache._cache.clear()
vllm/distributed/device_communicators/cuda_communicator.py
+8
@@ -214,6 +214,14 @@ class CudaCommunicator(DeviceCommunicatorBase):
214 tcp_store_group,
215 device_group=self.device_group,
216 )
217 + elif self.all2all_backend == "moonep":
218 + from .all2all import MoonEPAll2AllManager
219 +
220 + self.all2all_manager = MoonEPAll2AllManager(
221 + self.cpu_group,
222 + tcp_store_group,
223 + device_group=self.device_group,
224 + )
225 elif self.all2all_backend == "nixl_ep":
226 from .all2all import NixlEPAll2AllManager
227
vllm/model_executor/layers/fused_moe/all2all_utils.py
+30
@@ -35,6 +35,7 @@ from vllm.platforms import current_platform
35 from vllm.utils.import_utils import (
36 has_deep_ep,
37 has_deep_ep_v2,
38 + has_moonep,
39 has_mori,
40 has_nixl_ep,
41 )
@@ -103,6 +104,13 @@ if current_platform.is_cuda_alike():
104 NIXL_EP_QUANT_BLOCK_SHAPE,
105 NixlEPPrepareAndFinalize,
106 )
107 + if has_moonep():
108 + from .prepare_finalize.moonep import (
109 + MOONEP_DEFAULT_NUM_PREFETCH_SLOTS,
110 + MOONEP_DEFAULT_NUM_SMS,
111 + MOONEP_DEFAULT_TOKEN_PADDING,
112 + MoonEPPrepareAndFinalize,
113 + )
114
115
116 def get_ep_all2all_manager(
@@ -279,6 +287,28 @@ def maybe_make_prepare_finalize(
287 sp_size=moe.moe_parallel_config.sp_size,
288 )
289
290 + elif moe.use_moonep_kernels:
291 + all_to_all_args = dict(
292 + max_num_tokens_per_dp_rank=moe.max_num_tokens,
293 + token_hidden_size=moe.hidden_dim,
294 + num_topk=moe.experts_per_token,
295 + num_global_experts=moe.num_experts,
296 + num_prefetch_slots=MOONEP_DEFAULT_NUM_PREFETCH_SLOTS,
297 + token_padding=MOONEP_DEFAULT_TOKEN_PADDING,
298 + num_sms=MOONEP_DEFAULT_NUM_SMS,
299 + )
300 + handle = all2all_manager.get_handle(all_to_all_args)
301 +
302 + # The [E+B] weight layout is picked up from the experts (their
303 + # process_weights_after_loading hook) once the layer has loaded and
304 + # converted its weights.
305 + prepare_finalize = MoonEPPrepareAndFinalize(
306 + buffer_pool=handle,
307 + max_tokens_per_rank=moe.max_num_tokens,
308 + num_dispatchers=all2all_manager.world_size,
309 + num_global_experts=moe.num_experts,
310 + )
311 +
312 elif moe.use_mori_kernels:
313 assert quant_config is not None
314
vllm/model_executor/layers/fused_moe/config.py
+8
@@ -1085,6 +1085,10 @@ class FusedMoEParallelConfig:
1085 def use_deepep_v2_kernels(self):
1086 return self.use_all2all_kernels and self.all2all_backend == "deepep_v2"
1087
1088 + @property
1089 + def use_moonep_kernels(self):
1090 + return self.use_all2all_kernels and self.all2all_backend == "moonep"
1091 +
1092 @staticmethod
1093 def flatten_tp_across_dp_and_pcp(
1094 tp_size: int, dp_size: int, dp_rank: int, pcp_size: int, pcp_rank: int
@@ -1473,6 +1477,10 @@ class FusedMoEConfig:
1477 def use_deepep_v2_kernels(self):
1478 return self.moe_parallel_config.use_deepep_v2_kernels
1479
1480 + @property
1481 + def use_moonep_kernels(self):
1482 + return self.moe_parallel_config.use_moonep_kernels
1483 +
1484 @property
1485 def needs_round_robin_routing_tables(self):
1486 return self.moe_parallel_config.needs_round_robin_routing_tables
vllm/model_executor/layers/fused_moe/experts/moonep_experts.py new
+220
@@ -0,0 +1,220 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""MoonEP experts: grouped GEMM over MoonEP's expert-grouped activations.
4 +
5 +MoonEP dispatch delivers tokens already contiguous per weight row
6 +(``[NvS, H]`` in ``cu_seqlens[E+B]`` segment order), so the expert compute
7 +is three grouped GEMMs over those segments with no permute/unpermute:
8 +
9 + gate = grouped_mm(x, w_gate[E+B]) # [NvS, I]
10 + up = grouped_mm(x, w_up[E+B]) # [NvS, I]
11 + act = silu(gate) * up * route_weight # route weights applied here
12 + out = grouped_mm(act, w_down[E+B]) # [NvS, H]
13 +
14 +Rows ``[E, E+B)`` are the redundant-expert prefetch slots that
15 +``MoonEPPrepareAndFinalize`` fills before ``apply`` runs. Empty segments
16 +(including unused prefetch slots) are skipped by the grouped GEMM.
17 +
18 +Weight layout: MoonEP's ``prefetch_weight`` requires each projection to be
19 +its own contiguous ``[E+B, ., .]`` tensor, so gate and up are separate
20 +tensors rather than vLLM's interleaved ``[E, 2I, H]`` ``w13``. The modular
21 +kernel passes ``w1``/``w2`` from the layer (``w1`` is the gate tensor); the
22 +full layout is picked up from the layer in ``process_weights_after_loading``
23 +and shared with ``MoonEPPrepareAndFinalize`` via ``post_init_setup``.
24 +"""
25 +
26 +import torch
27 +
28 +import vllm.model_executor.layers.fused_moe.modular_kernel as mk
29 +from vllm.model_executor.layers.fused_moe.activation import MoEActivation
30 +from vllm.model_executor.layers.fused_moe.config import (
31 + FusedMoEConfig,
32 + FusedMoEParallelConfig,
33 + FusedMoEQuantConfig,
34 +)
35 +from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
36 + TopKWeightAndReduceNoOP,
37 +)
38 +from vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
39 +from vllm.platforms import current_platform
40 +
41 +
42 +def moonep_grouped_gemm(
43 + a: torch.Tensor,
44 + w: torch.Tensor,
45 + cu_seqlens: torch.Tensor,
46 +) -> torch.Tensor:
47 + """``out[seg_g] = a[seg_g] @ w[g].T`` for each ``cu_seqlens`` segment.
48 +
49 + Args:
50 + a: ``[NvS, K]`` activations in segment order.
51 + w: ``[G, N, K]`` weights (vLLM ``[E, N, K]`` convention).
52 + cu_seqlens: ``[G]`` int32 cumulative segment end offsets.
53 +
54 + Returns:
55 + ``[NvS, N]``; rows past ``cu_seqlens[-1]`` are zero-filled.
56 +
57 + """
58 + return torch._grouped_mm(a, w.transpose(1, 2), offs=cu_seqlens)
59 +
60 +
61 +class MoonEPExperts(mk.FusedMoEExpertsModular):
62 + """Grouped-GEMM experts over MoonEP's ``[NvS, H]`` layout (BF16)."""
63 +
64 + def __init__(
65 + self,
66 + moe_config: FusedMoEConfig,
67 + quant_config: FusedMoEQuantConfig,
68 + max_num_tokens: int | None = None,
69 + num_dispatchers: int | None = None,
70 + ):
71 + super().__init__(moe_config, quant_config, max_num_tokens, num_dispatchers)
72 + assert quant_config.quant_dtype is None, (
73 + "MoonEPExperts supports unquantized BF16 only"
74 + )
75 + if (
76 + quant_config.gemm1_alpha is not None
77 + or quant_config.gemm1_beta is not None
78 + or quant_config.gemm1_clamp_limit is not None
79 + ):
80 + raise NotImplementedError(
81 + "MoonEPExperts implements plain silu(gate) * up only; SwiGLU "
82 + "variants with alpha/beta/clamp parameters are not supported"
83 + )
84 + self._weight_layout = None
85 +
86 + def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
87 + # The [E+B] gate/up/down layout built by
88 + # convert_to_unquantized_kernel_format; MoonEPPrepareAndFinalize
89 + # reads it from here (post_init_setup) for weight prefetch.
90 + layout = getattr(layer, "_moonep_weight_layout", None)
91 + assert layout is not None, (
92 + "MoonEPExperts requires the layer to carry _moonep_weight_layout "
93 + "(set by convert_to_unquantized_kernel_format)"
94 + )
95 + self._weight_layout = layout
96 +
97 + @property
98 + def weight_layout(self):
99 + return self._weight_layout
100 +
101 + @staticmethod
102 + def activation_format() -> mk.FusedMoEActivationFormat:
103 + return mk.FusedMoEActivationFormat.Standard
104 +
105 + @staticmethod
106 + def _supports_current_device() -> bool:
107 + return current_platform.is_cuda()
108 +
109 + @staticmethod
110 + def _supports_no_act_and_mul() -> bool:
111 + return False
112 +
113 + @staticmethod
114 + def _supports_activation(activation: MoEActivation) -> bool:
115 + return activation == MoEActivation.SILU
116 +
117 + @staticmethod
118 + def _supports_parallel_config(moe_parallel_config: FusedMoEParallelConfig) -> bool:
119 + # EPLB rearranges the named expert parameters as local_num_experts
120 + # rows, which does not understand the replicated [E+B] layout.
121 + return (
122 + moe_parallel_config.use_moonep_kernels
123 + and not moe_parallel_config.enable_eplb
124 + )
125 +
126 + @staticmethod
127 + def _supports_quant_scheme(
128 + weight_key: QuantKey | None,
129 + activation_key: QuantKey | None,
130 + ) -> bool:
131 + return weight_key is None and activation_key is None
132 +
133 + def supports_chunking(self) -> bool:
134 + # NvS is static and segments must stay whole.
135 + return False
136 +
137 + def supports_expert_map(self) -> bool:
138 + # MoonEP addresses global expert rows directly.
139 + return False
140 +
141 + def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
142 + return TopKWeightAndReduceNoOP()
143 +
144 + def moe_problem_size(
145 + self,
146 + a1: torch.Tensor,
147 + w1: torch.Tensor,
148 + w2: torch.Tensor,
149 + topk_ids: torch.Tensor,
150 + ) -> tuple[int, int, int, int, int]:
151 + # a1 is [NvS, H] in segment order, not token-major, so the base
152 + # implementation's topk_ids/a1 row-count check does not apply.
153 + assert a1.dim() == 2 and w1.dim() == 3 and w2.dim() == 3
154 + num_rows, intermediate, hidden = w1.shape # [E+B, I, H]
155 + assert a1.size(1) == hidden
156 + return num_rows, a1.size(0), intermediate, hidden, topk_ids.size(1)
157 +
158 + def workspace_shapes(
159 + self,
160 + M: int,
161 + N: int,
162 + K: int,
163 + topk: int,
164 + global_num_experts: int,
165 + local_num_experts: int,
166 + expert_tokens_meta: mk.ExpertTokensMetadata | None,
167 + activation: MoEActivation,
168 + ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
169 + # M is NvS. torch._grouped_mm allocates its own outputs, so no
170 + # workspaces are needed.
171 + return ((0,), (0,), (M, K))
172 +
173 + def apply(
174 + self,
175 + output: torch.Tensor,
176 + hidden_states: torch.Tensor,
177 + w1: torch.Tensor,
178 + w2: torch.Tensor,
179 + topk_weights: torch.Tensor,
180 + topk_ids: torch.Tensor,
181 + activation: MoEActivation,
182 + global_num_experts: int,
183 + expert_map: torch.Tensor | None,
184 + a1q_scale: torch.Tensor | None,
185 + a2_scale: torch.Tensor | None,
186 + workspace13: torch.Tensor,
187 + workspace2: torch.Tensor,
188 + expert_tokens_meta: mk.ExpertTokensMetadata | None,
189 + apply_router_weight_on_input: bool,
190 + ) -> None:
191 + # expert_map is ignored: MoonEP dispatches on global expert ids and
192 + # the weights are addressed by global [E+B] row, on every rank.
193 + assert activation == MoEActivation.SILU
194 + assert self._weight_layout is not None, (
195 + "process_weights_after_loading() not called"
196 + )
197 + assert expert_tokens_meta is not None
198 + # MoonEPPrepareAndFinalize returns route weights in NvS order as the
199 + # dispatched topk_weights, and per-row segment lengths as
200 + # expert_tokens_meta.
201 + route_weights_nvs = topk_weights
202 + assert route_weights_nvs.dim() == 1
203 + cu_seqlens = torch.cumsum(
204 + expert_tokens_meta.expert_num_tokens, dim=0, dtype=torch.int32
205 + )
206 +
207 + assert cu_seqlens.numel() == w1.size(0)
208 +
209 + gate = moonep_grouped_gemm(hidden_states, w1, cu_seqlens)
210 + up = moonep_grouped_gemm(
211 + hidden_states, self._weight_layout.full_up_weight, cu_seqlens
212 + )
213 + act = torch.nn.functional.silu(gate)
214 + act.mul_(up)
215 + if not apply_router_weight_on_input:
216 + # Input-weighted routing (top-1, e.g. Llama 4) was already
217 + # applied to the activations by prepare(), before dispatch.
218 + act.mul_(route_weights_nvs.to(act.dtype).unsqueeze(-1))
219 + # Padding rows past the last segment come out zero-filled.
220 + output.copy_(moonep_grouped_gemm(act, w2, cu_seqlens))
vllm/model_executor/layers/fused_moe/oracle/unquantized.py
+52 -1
@@ -40,6 +40,7 @@ class UnquantizedMoeBackend(Enum):
40 AITER = "ROCm AITER"
41 TRITON = "TRITON"
42 BATCHED_TRITON = "BATCHED_TRITON"
43 + MOONEP = "MOONEP"
44 CPU = "CPU"
45 XPU = "XPU"
46 TPU = "TPU"
@@ -138,6 +139,13 @@ def backend_to_kernel_cls(
139
140 return [BatchedTritonExperts]
141
142 + elif backend == UnquantizedMoeBackend.MOONEP:
143 + from vllm.model_executor.layers.fused_moe.experts.moonep_experts import (
144 + MoonEPExperts,
145 + )
146 +
147 + return [MoonEPExperts]
148 +
149 elif backend == UnquantizedMoeBackend.XPU:
150 from vllm.model_executor.layers.fused_moe.experts.xpu_moe import XPUExperts
151
@@ -281,6 +289,13 @@ def select_unquantized_moe_backend(
289 return backend, k_cls
290 raise ValueError(_make_log_unsupported(backend, reason))
291
292 + # MoonEP owns the expert layout (expert-grouped [NvS, H] + cu_seqlens),
293 + # so it is not interchangeable with the token-major experts backends.
294 + if moe_config.moe_parallel_config.use_moonep_kernels:
295 + return _return_or_raise(
296 + UnquantizedMoeBackend.MOONEP, moe_config, activation_format
297 + )
298 +
299 runner_backend = moe_config.moe_backend
300 # 'humming' is quantization-only; an unquantized layer (e.g. excluded via
301 # modules_to_not_convert) falls through to auto instead of erroring.
@@ -366,7 +381,42 @@ def convert_to_unquantized_kernel_format(
381 moe_config: FusedMoEConfig,
382 w13_weight: torch.Tensor,
383 w2_weight: torch.Tensor,
384 + layer: torch.nn.Module | None = None,
385 ) -> tuple[torch.Tensor, torch.Tensor]:
386 + if unquantized_backend == UnquantizedMoeBackend.MOONEP:
387 + if layer is None:
388 + raise ValueError(
389 + "MoonEP weight conversion requires the layer module to stash "
390 + "the [E+B] weight layout on"
391 + )
392 + # MoonEP addresses expert weights by global [E+B] row and needs one
393 + # contiguous tensor per projection (gate / up / down). The layer's
394 + # w13_weight becomes the gate tensor and w2_weight the down tensor;
395 + # the up tensor and the full layout are stashed on the layer for
396 + # the kernel wiring in _setup_kernel.
397 + from vllm.model_executor.layers.fused_moe.prepare_finalize.moonep import (
398 + MOONEP_DEFAULT_NUM_PREFETCH_SLOTS,
399 + gather_moonep_weight_layout,
400 + )
401 +
402 + if getattr(layer, "_moonep_weight_layout", None) is not None:
403 + # On a reload the layer parameters were already replaced by the
404 + # converted [E+B] gate/down tensors, so the source-format weights
405 + # this conversion needs are gone (and the unregistered up
406 + # projection cannot be recovered from them at all). Tracked in
407 + # RFC #52095.
408 + raise NotImplementedError(
409 + "MoonEP does not support in-place weight reloads yet"
410 + )
411 + layout = gather_moonep_weight_layout(
412 + w13_weight,
413 + w2_weight,
414 + num_global_experts=moe_config.num_experts,
415 + num_prefetch_slots=MOONEP_DEFAULT_NUM_PREFETCH_SLOTS,
416 + )
417 + layer._moonep_weight_layout = layout
418 + return layout.full_gate_weight, layout.full_down_weight
419 +
420 if unquantized_backend == UnquantizedMoeBackend.AITER:
421 w13_weight, w2_weight = rocm_aiter_ops.shuffle_weights(w13_weight, w2_weight)
422 w13_weight.is_shuffled = True
@@ -522,9 +572,10 @@ class UnquantizedMoEKernelOracle(MoEKernelOracle[UnquantizedMoeBackend]):
572 moe_config: FusedMoEConfig,
573 w13_weight: torch.Tensor,
574 w2_weight: torch.Tensor,
575 + layer: torch.nn.Module | None = None,
576 ) -> tuple[torch.Tensor, torch.Tensor]:
577 return convert_to_unquantized_kernel_format(
527 - backend, moe_config, w13_weight, w2_weight
578 + backend, moe_config, w13_weight, w2_weight, layer=layer
579 )
580
581 def make_kernel(
vllm/model_executor/layers/fused_moe/prepare_finalize/moonep.py new
+429
@@ -0,0 +1,429 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""MoonEP (https://github.com/MoonshotAI/MoonEP) prepare/finalize.
4 +
5 +BF16 correctness-first proof of concept on top of vLLM's modular kernel
6 +interface.
7 +
8 +MoonEP differs from DeepEP-style backends in two ways that shape this
9 +integration:
10 +
11 +- ``dispatch`` returns tokens already grouped by expert *row* (a fixed
12 + ``[NvS, H]`` layout with ``NvS = S x K`` real slots plus padding) together
13 + with a ``cu_seqlens[E+B]`` segment table and an opaque ``plan``. There is
14 + no per-token topk id tensor after dispatch; the expert compute must be a
15 + grouped GEMM over ``cu_seqlens`` segments.
16 +- Rows ``[E, E+B)`` of the weight/segment space are dynamic redundant-expert
17 + prefetch slots. ``plan.experts_to_copy`` names the source expert of each
18 + slot and ``Buffer.prefetch_weight`` must run between dispatch and expert
19 + compute.
20 +
21 +PoC limitations:
22 +- BF16 / unquantized only, eager only.
23 +- Expert weights are replicated in global-expert order on every rank
24 + (memory-heavy). Production Kimi-K3 serving requires sharded
25 + symmetric-memory expert ownership, where rows ``[0, E)`` physically alias
26 + each home rank's parameter memory.
27 +- Route weights are applied inside the expert compute and MoonEP's
28 + ``combine`` performs the K-sum, so ``finalize`` requires
29 + ``TopKWeightAndReduceNoOP``.
30 +"""
31 +
32 +from typing import Any, NamedTuple
33 +
34 +import torch
35 +import torch.nn.functional as F
36 +
37 +import vllm.model_executor.layers.fused_moe.modular_kernel as mk
38 +from vllm.forward_context import get_forward_context
39 +from vllm.model_executor.layers.fused_moe.config import FusedMoEQuantConfig
40 +from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
41 + TopKWeightAndReduceNoOP,
42 +)
43 +
44 +MOONEP_DEFAULT_NUM_PREFETCH_SLOTS = 4
45 +MOONEP_DEFAULT_TOKEN_PADDING = 128
46 +MOONEP_DEFAULT_NUM_SMS = 32
47 +# MoonEP's weight-prefetch kernel tiles hidden and intermediate dims by 128.
48 +MOONEP_WEIGHT_TILE = 128
49 +
50 +
51 +class MoonEPExpertWeightLayout(NamedTuple):
52 + """Contiguous BF16 expert weights in MoonEP ``[E+B, ...]`` layout.
53 +
54 + Rows ``[0, E)`` hold expert weights in global expert order; rows
55 + ``[E, E+B)`` are mutable prefetch slots filled by
56 + ``Buffer.prefetch_weight``.
57 +
58 + The prefetch slots never need re-zeroing between calls: the planner
59 + marks unused slots with ``-1`` in ``plan.experts_to_copy`` (skipped by
60 + ``prefetch_weight``) and gives them an empty ``cu_seqlens`` segment, so
61 + stale slot contents are never read; used slots are fully overwritten.
62 + """
63 +
64 + full_gate_weight: torch.Tensor
65 + full_up_weight: torch.Tensor
66 + full_down_weight: torch.Tensor
67 + num_prefetch_slots: int
68 +
69 +
70 +def make_moonep_weight_layout(
71 + w13_weight: torch.Tensor,
72 + w2_weight: torch.Tensor,
73 + num_prefetch_slots: int,
74 +) -> MoonEPExpertWeightLayout:
75 + """Build the replicated ``[E+B, ...]`` PoC weight layout.
76 +
77 + ``w13_weight`` must be ``[E, 2I, H]`` (gate rows first) and ``w2_weight``
78 + ``[E, H, I]``, both BF16 in global expert order.
79 + """
80 + if num_prefetch_slots <= 0:
81 + raise ValueError(
82 + f"num_prefetch_slots must be positive, got {num_prefetch_slots}"
83 + )
84 + if w13_weight.dtype != torch.bfloat16 or w2_weight.dtype != torch.bfloat16:
85 + raise NotImplementedError("MoonEP PoC supports BF16 weights only.")
86 + num_experts, two_i, hidden_size = w13_weight.shape
87 + intermediate_size = two_i // 2
88 + if tuple(w2_weight.shape) != (num_experts, hidden_size, intermediate_size):
89 + raise ValueError(
90 + f"w2_weight shape {tuple(w2_weight.shape)} does not match "
91 + f"w13_weight shape {tuple(w13_weight.shape)}"
92 + )
93 + if hidden_size % MOONEP_WEIGHT_TILE or intermediate_size % MOONEP_WEIGHT_TILE:
94 + raise ValueError(
95 + "MoonEP weight prefetch requires hidden_size and intermediate_size "
96 + f"to be multiples of {MOONEP_WEIGHT_TILE}; got H={hidden_size}, "
97 + f"I={intermediate_size}"
98 + )
99 +
100 + full_gate_weight = torch.empty(
101 + num_experts + num_prefetch_slots,
102 + intermediate_size,
103 + hidden_size,
104 + dtype=torch.bfloat16,
105 + device=w13_weight.device,
106 + )
107 + full_up_weight = torch.empty_like(full_gate_weight)
108 + full_down_weight = torch.empty(
109 + num_experts + num_prefetch_slots,
110 + hidden_size,
111 + intermediate_size,
112 + dtype=torch.bfloat16,
113 + device=w2_weight.device,
114 + )
115 +
116 + full_gate_weight[:num_experts].copy_(w13_weight[:, :intermediate_size, :])
117 + full_up_weight[:num_experts].copy_(w13_weight[:, intermediate_size:, :])
118 + full_down_weight[:num_experts].copy_(w2_weight)
119 + full_gate_weight[num_experts:].zero_()
120 + full_up_weight[num_experts:].zero_()
121 + full_down_weight[num_experts:].zero_()
122 +
123 + return MoonEPExpertWeightLayout(
124 + full_gate_weight=full_gate_weight.contiguous(),
125 + full_up_weight=full_up_weight.contiguous(),
126 + full_down_weight=full_down_weight.contiguous(),
127 + num_prefetch_slots=num_prefetch_slots,
128 + )
129 +
130 +
131 +def gather_moonep_weight_layout(
132 + w13_local: torch.Tensor,
133 + w2_local: torch.Tensor,
134 + num_global_experts: int,
135 + num_prefetch_slots: int,
136 +) -> MoonEPExpertWeightLayout:
137 + """Build the replicated ``[E+B, ...]`` layout from this rank's local experts.
138 +
139 + PoC bridge: each EP rank loads only its own experts (linear placement),
140 + so all-gather them once at load time into global expert order on every
141 + rank. Production MoonEP instead maps rows ``[0, E)`` onto each home
142 + rank's parameter memory via symmetric memory (RFC #52095 item 5).
143 + """
144 + from vllm.distributed import get_ep_group
145 +
146 + ep_group = get_ep_group()
147 + ep_size = ep_group.world_size
148 + if ep_size == 1:
149 + return make_moonep_weight_layout(w13_local, w2_local, num_prefetch_slots)
150 + if w13_local.size(0) * ep_size != num_global_experts:
151 + raise NotImplementedError(
152 + "MoonEP PoC requires num_experts to be evenly divisible across EP "
153 + f"ranks: {num_global_experts} experts, {ep_size} ranks"
154 + )
155 + w13_global = ep_group.all_gather(w13_local.contiguous(), dim=0)
156 + w2_global = ep_group.all_gather(w2_local.contiguous(), dim=0)
157 + return make_moonep_weight_layout(w13_global, w2_global, num_prefetch_slots)
158 +
159 +
160 +MOONEP_MIN_DISPATCH_TOKENS = 128
161 +
162 +
163 +class MoonEPBufferPool:
164 + """Lazily creates ``moonep.Buffer`` instances at power-of-two capacities.
165 +
166 + ``Buffer`` fixes its token capacity ``S`` at construction, so a single
167 + full-capacity buffer would pad every dispatch to
168 + ``max_num_batched_tokens``. The pool instead serves the smallest
169 + power-of-two capacity that fits the step's token count, so decode-sized
170 + batches dispatch a few hundred slots rather than the full capacity.
171 +
172 + Selection must be identical on every EP rank (dispatch is collective and
173 + ``Buffer`` construction is itself a collective): callers derive the token
174 + count from ``dp_metadata`` (the max across DP ranks) so all ranks create
175 + and pick the same buffer at the same step.
176 + """
177 +
178 + def __init__(self, buffer_kwargs: dict[str, Any], max_tokens_per_rank: int):
179 + self._kwargs = {k: v for k, v in buffer_kwargs.items() if k != "S"}
180 + self.max_tokens_per_rank = max_tokens_per_rank
181 + self._buffers: dict[int, Any] = {}
182 +
183 + def capacity_for(self, num_tokens: int) -> int:
184 + if num_tokens >= self.max_tokens_per_rank:
185 + return self.max_tokens_per_rank
186 + cap = 1 << max(num_tokens - 1, 0).bit_length()
187 + return min(max(cap, MOONEP_MIN_DISPATCH_TOKENS), self.max_tokens_per_rank)
188 +
189 + def get(self, num_tokens: int) -> tuple[int, Any]:
190 + """Return ``(capacity, buffer)`` for the given step token count."""
191 + from moonep import Buffer # type: ignore[import-not-found]
192 +
193 + capacity = self.capacity_for(num_tokens)
194 + buffer = self._buffers.get(capacity)
195 + if buffer is None:
196 + buffer = Buffer(S=capacity, **self._kwargs)
197 + self._buffers[capacity] = buffer
198 + return capacity, buffer
199 +
200 + def destroy(self) -> None:
201 + for buffer in self._buffers.values():
202 + buffer.destroy()
203 + self._buffers.clear()
204 +
205 +
206 +class MoonEPPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
207 + """Prepare/Finalize using MoonEP balanced dispatch/combine.
208 +
209 + ``prepare`` pads the batch to the selected buffer's static token
210 + capacity, dispatches, runs ``prefetch_weight`` for the planned redundant
211 + experts, and stashes the ``plan`` for ``finalize`` (the same pattern
212 + DeepEP-HT uses for its handle). Downstream expert compute must consume
213 + the expert-grouped ``[NvS, H]`` layout via ``cu_seqlens``.
214 + """
215 +
216 + def __init__(
217 + self,
218 + buffer_pool: "MoonEPBufferPool",
219 + max_tokens_per_rank: int,
220 + num_dispatchers: int,
221 + num_global_experts: int,
222 + weight_layout: MoonEPExpertWeightLayout | None = None,
223 + ):
224 + super().__init__()
225 + self.buffer_pool = buffer_pool
226 + self.max_tokens_per_rank = max_tokens_per_rank
227 + self.num_dispatchers_ = num_dispatchers
228 + self.num_global_experts = num_global_experts
229 + self.weight_layout = weight_layout
230 + self._fused_experts: Any = None
231 +
232 + # dispatch state consumed by finalize (and the expert runner)
233 + self._plan: Any = None
234 + self._cu_seqlens: torch.Tensor | None = None
235 + self._num_tokens: int = 0
236 + self._buffer: Any = None # the pool buffer used by the current step
237 +
238 + def post_init_setup(self, fused_experts: mk.FusedMoEExperts) -> None:
239 + # The [E+B] weight layout is attached to the experts by their
240 + # process_weights_after_loading hook (after this runs), so keep the
241 + # reference and resolve the layout lazily in prepare().
242 + self._fused_experts = fused_experts
243 +
244 + def _resolve_weight_layout(self) -> MoonEPExpertWeightLayout:
245 + if self.weight_layout is None and self._fused_experts is not None:
246 + layout = getattr(self._fused_experts, "weight_layout", None)
247 + if isinstance(layout, MoonEPExpertWeightLayout):
248 + self.weight_layout = layout
249 + # Redundant experts' weights must be in rows [E, E+B) before the
250 + # expert compute reads them; skipping prefetch silently corrupts
251 + # output.
252 + assert self.weight_layout is not None, (
253 + "MoonEPPrepareAndFinalize: weight layout not available (the "
254 + "experts' process_weights_after_loading has not run)"
255 + )
256 + return self.weight_layout
257 +
258 + @property
259 + def num_dispatched_slots(self) -> int:
260 + """``NvS`` of the buffer selected by the current step's prepare()."""
261 + assert self._buffer is not None, "prepare() has not been called"
262 + return int(self._buffer._ctx["NvS"])
263 +
264 + def num_dispatchers(self) -> int:
265 + return self.num_dispatchers_
266 +
267 + def output_is_reduced(self) -> bool:
268 + # combine returns the fully weighted+reduced token-major output
269 + return True
270 +
271 + @property
272 + def activation_format(self) -> mk.FusedMoEActivationFormat:
273 + return mk.FusedMoEActivationFormat.Standard
274 +
275 + def max_num_tokens_per_rank(self) -> int | None:
276 + return self.max_tokens_per_rank
277 +
278 + def topk_indices_dtype(self) -> torch.dtype | None:
279 + return torch.int32
280 +
281 + def supports_async(self) -> bool:
282 + return False
283 +
284 + @property
285 + def cu_seqlens(self) -> torch.Tensor:
286 + assert self._cu_seqlens is not None, "prepare() has not been called"
287 + return self._cu_seqlens
288 +
289 + @property
290 + def plan(self) -> Any:
291 + assert self._plan is not None, "prepare() has not been called"
292 + return self._plan
293 +
294 + def _pad_to_capacity(
295 + self,
296 + a1: torch.Tensor,
297 + topk_ids: torch.Tensor,
298 + topk_weights: torch.Tensor,
299 + capacity: int,
300 + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int]:
301 + num_tokens = a1.shape[0]
302 + if num_tokens > capacity:
303 + raise ValueError(
304 + f"MoonEP static buffer capacity exceeded: {num_tokens} tokens "
305 + f"> S={capacity}."
306 + )
307 + a1 = a1.contiguous()
308 + topk_ids = topk_ids.to(dtype=torch.int32)
309 + topk_weights = topk_weights.to(dtype=torch.float32)
310 + # MoonEP's planner has no sentinel for invalid expert ids, so map
311 + # negative ids (padded/invalid slots from the engine) to expert 0
312 + # with zero route weight — the same convention as the capacity
313 + # padding rows below. Wasted compute, never wrong output.
314 + invalid = topk_ids < 0
315 + topk_ids = torch.where(invalid, torch.zeros_like(topk_ids), topk_ids)
316 + topk_weights = torch.where(
317 + invalid, torch.zeros_like(topk_weights), topk_weights
318 + ).contiguous()
319 + topk_ids = topk_ids.contiguous()
320 + if num_tokens == capacity:
321 + return a1, topk_ids, topk_weights, num_tokens
322 + pad = capacity - num_tokens
323 + return (
324 + F.pad(a1, (0, 0, 0, pad)),
325 + F.pad(topk_ids, (0, 0, 0, pad)),
326 + F.pad(topk_weights, (0, 0, 0, pad)),
327 + num_tokens,
328 + )
329 +
330 + def prepare(
331 + self,
332 + a1: torch.Tensor,
333 + topk_weights: torch.Tensor,
334 + topk_ids: torch.Tensor,
335 + num_experts: int,
336 + expert_map: torch.Tensor | None,
337 + apply_router_weight_on_input: bool,
338 + quant_config: FusedMoEQuantConfig,
339 + defer_input_quant: bool = False,
340 + ) -> mk.PrepareResultType:
341 + if a1.dtype != torch.bfloat16:
342 + raise NotImplementedError("MoonEP PoC supports BF16 hidden states only.")
343 + if quant_config.quant_dtype is not None:
344 + raise NotImplementedError("MoonEP PoC does not support quantized dispatch.")
345 + assert num_experts == self.num_global_experts
346 + assert self._plan is None, (
347 + "MoonEPPrepareAndFinalize.prepare() called again before finalize()"
348 + )
349 + if apply_router_weight_on_input:
350 + assert topk_ids.size(1) == 1, (
351 + "apply_router_weight_on_input requires top-1 routing"
352 + )
353 + a1 = a1 * topk_weights.to(a1.dtype)
354 +
355 + # Buffer choice must be rank-symmetric: use the max token count
356 + # across DP ranks (every rank sees the same dp_metadata), falling
357 + # back to the local count outside a forward context (tests).
358 + num_tokens = a1.shape[0]
359 + try:
360 + dp_meta = get_forward_context().dp_metadata
361 + except AssertionError:
362 + dp_meta = None
363 + if dp_meta is not None:
364 + num_tokens = int(dp_meta.num_tokens_across_dp_cpu.max())
365 + capacity, self._buffer = self.buffer_pool.get(num_tokens)
366 +
367 + a1, topk_ids, topk_weights, self._num_tokens = self._pad_to_capacity(
368 + a1, topk_ids, topk_weights, capacity
369 + )
370 + tokens_per_expert = torch.bincount(
371 + topk_ids.reshape(-1).to(dtype=torch.int64),
372 + minlength=num_experts,
373 + ).to(dtype=torch.int32)
374 +
375 + hidden_nvsh, route_weights_nvs, cu_seqlens, plan = self._buffer.dispatch(
376 + a1,
377 + topk_weights,
378 + topk_ids,
379 + tokens_per_expert,
380 + )
381 + self._plan = plan
382 + self._cu_seqlens = cu_seqlens
383 +
384 + weight_layout = self._resolve_weight_layout()
385 + self._buffer.prefetch_weight(
386 + plan=plan,
387 + full_gate_weight=weight_layout.full_gate_weight,
388 + full_up_weight=weight_layout.full_up_weight,
389 + full_down_weight=weight_layout.full_down_weight,
390 + )
391 +
392 + # Segment sizes per [E+B] weight row. NOTE: this is per *row*, not
393 + # per local expert, and NvS rows are expert-grouped rather than
394 + # token-major — only MoonEP-aware expert implementations can consume
395 + # this activation layout.
396 + expert_num_tokens = torch.diff(cu_seqlens, prepend=cu_seqlens.new_zeros(1))
397 + expert_tokens_meta = mk.ExpertTokensMetadata(
398 + expert_num_tokens=expert_num_tokens,
399 + expert_num_tokens_cpu=None,
400 + )
401 +
402 + # MoonEP has no post-dispatch per-token topk ids/weights; route
403 + # weights come back in NvS order.
404 + return hidden_nvsh, None, expert_tokens_meta, None, route_weights_nvs
405 +
406 + def finalize(
407 + self,
408 + output: torch.Tensor,
409 + fused_expert_output: torch.Tensor,
410 + topk_weights: torch.Tensor,
411 + topk_ids: torch.Tensor,
412 + apply_router_weight_on_input: bool,
413 + weight_and_reduce_impl: mk.TopKWeightAndReduce,
414 + ) -> None:
415 + # Route weights are applied in the expert compute and MoonEP's
416 + # combine performs the K-sum, so any weight/reduce work in finalize
417 + # means MoonEP was paired with the wrong kind of experts.
418 + assert isinstance(weight_and_reduce_impl, TopKWeightAndReduceNoOP), (
419 + "MoonEP requires TopKWeightAndReduceNoOP, got "
420 + f"{type(weight_and_reduce_impl).__name__}"
421 + )
422 + combined, _, _ = self._buffer.combine(
423 + plan=self.plan,
424 + hidden_nvsh=fused_expert_output,
425 + route_weights_nvs=None,
426 + )
427 + output.copy_(combined[: self._num_tokens])
428 + self._plan = None
429 + self._cu_seqlens = None
vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py
+14 -10
@@ -49,13 +49,17 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
49
50 # --8<-- [end:unquantized_fused_moe]
51
52 - supports_pre_processed_weights = True
53 -
52 def __init__(self, moe: FusedMoEConfig):
53 super().__init__(moe)
54 self.unquantized_backend, self.experts_cls = select_unquantized_moe_backend(
55 moe_config=self.moe,
56 )
57 + # MoonEP replaces the named parameters with its [E+B] gate/down
58 + # split and keeps the up projection outside the parameter registry,
59 + # so a pre-processed (e.g. ipc_cache) load cannot reconstruct it.
60 + self.supports_pre_processed_weights = (
61 + self.unquantized_backend != UnquantizedMoeBackend.MOONEP
62 + )
63
64 @property
65 def supports_eplb(self) -> bool:
@@ -171,6 +175,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
175 moe_config=layer.moe_config,
176 w13_weight=w13,
177 w2_weight=w2,
178 + layer=layer,
179 )
180 # `moe_kernel` is initialized to None in FusedMoEMethodBase.__init__;
181 # On the first call we replace the parameter normally. On subsequent
@@ -191,14 +196,13 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
196 # does not need to be re-built.
197 self._init_moe_kernel(layer)
198
194 - if self.unquantized_backend == UnquantizedMoeBackend.CPU:
195 - # The CPU experts need the layer itself for the setup that
196 - # convert_to_unquantized_kernel_format cannot express, since
197 - # it only sees the two weight tensors: padding and prepacking
198 - # into the grouped-gemm layout (bias included), and capturing
199 - # the router config that monolithic apply() cannot carry.
200 - assert self.moe_kernel is not None
201 - self.moe_kernel.fused_experts.process_weights_after_loading(layer)
199 + # No-op by default. Experts that need the layer itself for setup
200 + # convert_to_unquantized_kernel_format cannot express override
201 + # it: CPU prepacks into its grouped-gemm layout and captures the
202 + # router config; MoonEP picks up the [E+B] weight layout for
203 + # prefetch and the up projection.
204 + assert self.moe_kernel is not None
205 + self.moe_kernel.fused_experts.process_weights_after_loading(layer)
206
207 def _init_moe_kernel(self, layer: "RoutedExperts") -> None:
208 """Build the MoE kernel from the layer's current (shuffled) weights."""
vllm/utils/import_utils.py
+28
@@ -512,6 +512,34 @@ def has_nixl_ep() -> bool:
512 return _has_module("nixl_ep")
513
514
515 +def has_moonep() -> bool:
516 + """Whether the optional `moonep` package is available."""
517 + return _has_module("moonep")
518 +
519 +
520 +def check_moonep_system_support() -> None:
521 + """Raise if the current device cannot run MoonEP.
522 +
523 + MoonEP's symmetric-memory buffers require CUDA VMM plus NVSwitch multicast
524 + (SHARP) on every EP rank; NVLink-only topologies without NVSwitch (e.g.
525 + 4x H100 NV6) fail deep inside ``moonep.Buffer`` otherwise.
526 + """
527 + if not has_moonep():
528 + raise RuntimeError(
529 + "MoonEP not available. Install it from "
530 + "https://github.com/MoonshotAI/MoonEP."
531 + )
532 + from moonep._C import ( # type: ignore[import-not-found]
533 + nvl_multicast_supported,
534 + )
535 +
536 + if not nvl_multicast_supported():
537 + raise RuntimeError(
538 + "MoonEP requires NVSwitch multicast (SHARP) support on the "
539 + "current device; it is not supported on this GPU/topology."
540 + )
541 +
542 +
543 def is_numba_available() -> bool:
544 """Whether the optional `numba` package is available."""
545 return _has_module("numba")