Skip to content

Commit 2fa0ea7

Browse files
fix(moe): carry vLLM vllm-project#48698 — share FlashInfer B12x MoE workspace across layers
Cherry-picks unmerged upstream PR vllm-project#48698 (dumko2001) to fix the per-layer B12xMoEWrapper workspace blowup that hard-wedged GB10 DGX Spark nodes. Root cause (diagnosed via ora-1 + lib-1, 4 physical wedges): FlashInferB12xExperts allocated a B12xMoEWrapper static workspace lazily on first apply(), PER MoE LAYER. On GB10 UMA (shared 121GiB pool) this summed to a multi-GiB blowup during profile_run() — before any KV/util knob applies — exhausting the pool and starving the kubelet (vLLM vllm-project#47982, vllm-project#49476). --gpu-memory-utilization, --enforce-eager, max-num-batched-tokens all failed to bound it. The patch: - Weakref-shared wrapper registry keyed by (owner, geometry): ONE wrapper per geometry+vllm-config instead of one per layer. - Eager acquisition in __init__ so the workspace lands in the profiling baseline (not lazily in apply()). - DP fix: max_num_tokens = moe_config.max_num_tokens * dp_size. Preflight verified: - git apply --check clean on gb10-main (no local mods to this file). - flashinfer 0.6.15.post1 B12xMoEWrapper.__init__ accepts the new output_dtype + device kwargs (kwonly). Still necessary-but-not-sufficient for GB10: the UMA shared-pool overshoot (vllm-project#46307) is independent — keep gpu-memory-utilization <=0.6 + --enforce-eager on first boot. Upstream vllm-project#48698 is DRAFT/unmerged; drop this carry once it lands.
1 parent f30fa6d commit 2fa0ea7

2 files changed

Lines changed: 252 additions & 38 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
4+
import gc
5+
import sys
6+
import weakref
7+
from types import ModuleType, SimpleNamespace
8+
from typing import cast
9+
10+
import pytest
11+
import torch
12+
13+
import vllm.model_executor.layers.fused_moe.experts.flashinfer_b12x_moe as b12x
14+
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
15+
from vllm.model_executor.layers.fused_moe.config import (
16+
FusedMoEConfig,
17+
FusedMoEQuantConfig,
18+
)
19+
20+
pytestmark = [pytest.mark.cpu_test, pytest.mark.skip_global_cleanup]
21+
22+
23+
def _make_experts(
24+
activation: MoEActivation,
25+
max_num_tokens: int = 16,
26+
) -> b12x.FlashInferB12xExperts:
27+
quant_config = cast(
28+
FusedMoEQuantConfig,
29+
SimpleNamespace(
30+
quant_dtype="nvfp4",
31+
g1_alphas=torch.full((2,), 1.0),
32+
g2_alphas=torch.full((2,), 2.0),
33+
w1_scale=torch.full((2,), 3.0),
34+
w2_scale=torch.full((2,), 4.0),
35+
),
36+
)
37+
moe_config = cast(
38+
FusedMoEConfig,
39+
SimpleNamespace(
40+
in_dtype=torch.bfloat16,
41+
num_experts=2,
42+
experts_per_token=2,
43+
num_local_experts=2,
44+
hidden_dim=4,
45+
intermediate_size_per_partition=8,
46+
max_num_tokens=max_num_tokens,
47+
dp_size=2,
48+
device=torch.device("cpu"),
49+
activation=activation,
50+
),
51+
)
52+
experts = b12x.FlashInferB12xExperts(moe_config, quant_config)
53+
experts._fc2_input_scale = torch.full((2,), 5.0)
54+
experts.w1_sf_mma = torch.full((2,), 6.0)
55+
experts.w2_sf_mma = torch.full((2,), 7.0)
56+
return experts
57+
58+
59+
@pytest.mark.parametrize(
60+
("activation", "expected_activation"),
61+
[
62+
(MoEActivation.SILU, "silu"),
63+
(MoEActivation.RELU2_NO_MUL, "relu2"),
64+
],
65+
)
66+
def test_b12x_layers_share_owned_workspace_and_preserve_call_contract(
67+
monkeypatch: pytest.MonkeyPatch,
68+
activation: MoEActivation,
69+
expected_activation: str,
70+
):
71+
"""Reuse one fixed-capacity wrapper while passing each layer's weights."""
72+
wrappers = []
73+
calls = []
74+
75+
class FakeB12xMoEWrapper:
76+
def __init__(self, **kwargs):
77+
self.config = kwargs
78+
wrappers.append(self)
79+
80+
def run(self, **kwargs):
81+
calls.append(kwargs)
82+
return torch.full_like(kwargs["x"], len(calls))
83+
84+
fake_fused_moe = ModuleType("flashinfer.fused_moe")
85+
fake_fused_moe.__dict__["B12xMoEWrapper"] = FakeB12xMoEWrapper
86+
monkeypatch.setitem(sys.modules, "flashinfer.fused_moe", fake_fused_moe)
87+
88+
expert_instances = (_make_experts(activation), _make_experts(activation))
89+
assert len(wrappers) == 1
90+
assert wrappers[0].config == {
91+
"num_experts": 2,
92+
"top_k": 2,
93+
"hidden_size": 4,
94+
"intermediate_size": 8,
95+
"use_cuda_graph": True,
96+
"max_num_tokens": 32,
97+
"num_local_experts": 2,
98+
"output_dtype": torch.bfloat16,
99+
"device": "cpu",
100+
"activation": expected_activation,
101+
}
102+
103+
output = torch.empty(3, 4, dtype=torch.bfloat16)
104+
hidden_states = torch.empty_like(output)
105+
w1 = torch.full((2, 1, 1), 11.0)
106+
w2 = torch.full((2, 1, 1), 12.0)
107+
topk_weights = torch.empty(3, 2)
108+
topk_ids = torch.zeros(3, 2, dtype=torch.int64)
109+
110+
for experts in expert_instances:
111+
experts.apply(
112+
output=output,
113+
hidden_states=hidden_states,
114+
w1=w1,
115+
w2=w2,
116+
topk_weights=topk_weights,
117+
topk_ids=topk_ids,
118+
activation=activation,
119+
global_num_experts=2,
120+
expert_map=None,
121+
a1q_scale=None,
122+
a2_scale=None,
123+
workspace13=None,
124+
workspace2=None,
125+
expert_tokens_meta=None,
126+
apply_router_weight_on_input=False,
127+
)
128+
129+
assert len(calls) == 2
130+
assert torch.equal(output, torch.full_like(output, 2.0))
131+
for call, experts in zip(calls, expert_instances):
132+
assert call["x"] is hidden_states
133+
assert call["token_final_scales"] is topk_weights
134+
assert call["token_selected_experts"].dtype == torch.int32
135+
assert torch.equal(call["token_selected_experts"], topk_ids)
136+
assert call["w1_weight"] is w1
137+
assert call["w2_weight"] is w2
138+
assert call["w1_weight_sf"] is experts.w1_sf_mma
139+
assert call["w2_weight_sf"] is experts.w2_sf_mma
140+
assert call["w1_alpha"] is experts.g1_alphas
141+
assert call["w2_alpha"] is experts.g2_alphas
142+
assert call["fc2_input_scale"] is experts._fc2_input_scale
143+
144+
145+
def test_b12x_owned_workspace_is_released_with_model(
146+
monkeypatch: pytest.MonkeyPatch,
147+
):
148+
class FakeB12xMoEWrapper:
149+
def __init__(self, **kwargs):
150+
pass
151+
152+
fake_fused_moe = ModuleType("flashinfer.fused_moe")
153+
fake_fused_moe.__dict__["B12xMoEWrapper"] = FakeB12xMoEWrapper
154+
monkeypatch.setitem(sys.modules, "flashinfer.fused_moe", fake_fused_moe)
155+
156+
experts = _make_experts(MoEActivation.SILU, max_num_tokens=17)
157+
wrapper_ref = weakref.ref(experts._wrapper)
158+
159+
del experts
160+
gc.collect()
161+
162+
assert wrapper_ref() is None
163+
164+
165+
def test_b12x_workspace_is_not_shared_between_model_owners(
166+
monkeypatch: pytest.MonkeyPatch,
167+
):
168+
class FakeB12xMoEWrapper:
169+
def __init__(self, **kwargs):
170+
pass
171+
172+
fake_fused_moe = ModuleType("flashinfer.fused_moe")
173+
fake_fused_moe.__dict__["B12xMoEWrapper"] = FakeB12xMoEWrapper
174+
monkeypatch.setitem(sys.modules, "flashinfer.fused_moe", fake_fused_moe)
175+
176+
owners = iter((object(), object()))
177+
monkeypatch.setattr(b12x, "get_current_vllm_config_or_none", lambda: next(owners))
178+
179+
first = _make_experts(MoEActivation.SILU, max_num_tokens=18)
180+
second = _make_experts(MoEActivation.SILU, max_num_tokens=18)
181+
182+
assert first._wrapper is not second._wrapper

vllm/model_executor/layers/fused_moe/experts/flashinfer_b12x_moe.py

Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
33

4+
from dataclasses import dataclass
5+
from threading import Lock
46
from typing import Any
7+
from weakref import WeakValueDictionary
58

69
import torch
710

811
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
12+
from vllm.config import get_current_vllm_config_or_none
913
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
1014
from vllm.model_executor.layers.fused_moe.config import (
1115
FusedMoEConfig,
@@ -27,6 +31,46 @@
2731
)
2832

2933

34+
@dataclass(frozen=True)
35+
class _B12xWrapperKey:
36+
owner_id: int
37+
num_experts: int
38+
top_k: int
39+
hidden_size: int
40+
intermediate_size: int
41+
max_num_tokens: int
42+
num_local_experts: int
43+
output_dtype: torch.dtype
44+
device: str
45+
activation: str
46+
47+
48+
_B12X_WRAPPERS: WeakValueDictionary[_B12xWrapperKey, Any] = WeakValueDictionary()
49+
_B12X_WRAPPERS_LOCK = Lock()
50+
51+
52+
def _get_b12x_wrapper(key: _B12xWrapperKey) -> Any:
53+
with _B12X_WRAPPERS_LOCK:
54+
wrapper = _B12X_WRAPPERS.get(key)
55+
if wrapper is None:
56+
from flashinfer.fused_moe import B12xMoEWrapper
57+
58+
wrapper = B12xMoEWrapper(
59+
num_experts=key.num_experts,
60+
top_k=key.top_k,
61+
hidden_size=key.hidden_size,
62+
intermediate_size=key.intermediate_size,
63+
use_cuda_graph=True,
64+
max_num_tokens=key.max_num_tokens,
65+
num_local_experts=key.num_local_experts,
66+
output_dtype=key.output_dtype,
67+
device=key.device,
68+
activation=key.activation,
69+
)
70+
_B12X_WRAPPERS[key] = wrapper
71+
return wrapper
72+
73+
3074
class FlashInferB12xExperts(mk.FusedMoEExpertsModular):
3175
"""FlashInfer CuteDSL fused MoE expert for SM12x (SM120/SM121,
3276
RTX Pro 6000 / DGX Spark).
@@ -59,22 +103,19 @@ def __init__(
59103
)
60104
self.out_dtype = moe_config.in_dtype
61105
self.num_local_experts = moe_config.num_local_experts
62-
self.ep_rank = moe_config.moe_parallel_config.ep_rank
63-
# FC2 input scale tensor bound in process_weights_after_loading: the
64-
# calibrated (now-zeroed) a2_gscale for static-quant checkpoints, or
65-
# a synthesized uniform-1.0 tensor for W4A16 checkpoints that lack
66-
# one. Holding it on the instance keeps apply() alloc-free.
67-
self._fc2_input_scale: torch.Tensor | None = None
68-
69-
# Shape params for B12xMoEWrapper construction.
70106
self.global_num_experts = moe_config.num_experts
71107
self.topk = moe_config.experts_per_token
72108
self.hidden_dim = moe_config.hidden_dim
73109
self.intermediate_size_per_partition = (
74110
moe_config.intermediate_size_per_partition
75111
)
76-
self.max_num_tokens = moe_config.max_num_tokens
77-
self.local_expert_offset = self.ep_rank * self.num_local_experts
112+
self.max_num_tokens = moe_config.max_num_tokens * moe_config.dp_size
113+
self.device = str(torch.device(moe_config.device))
114+
# FC2 input scale tensor bound in process_weights_after_loading: the
115+
# calibrated (now-zeroed) a2_gscale for static-quant checkpoints, or
116+
# a synthesized uniform-1.0 tensor for W4A16 checkpoints that lack
117+
# one. Holding it on the instance keeps apply() alloc-free.
118+
self._fc2_input_scale: torch.Tensor | None = None
78119

79120
activation = moe_config.activation
80121
if activation not in self._ACTIVATION_MAP:
@@ -85,8 +126,21 @@ def __init__(
85126
)
86127
self._activation_str = self._ACTIVATION_MAP[activation]
87128

88-
# Lazily created on first apply() call.
89-
self._wrapper: Any | None = None
129+
self._wrapper = _get_b12x_wrapper(
130+
_B12xWrapperKey(
131+
owner_id=id(get_current_vllm_config_or_none()),
132+
num_experts=self.global_num_experts,
133+
top_k=self.topk,
134+
hidden_size=self.hidden_dim,
135+
intermediate_size=self.intermediate_size_per_partition,
136+
max_num_tokens=self.max_num_tokens,
137+
num_local_experts=self.num_local_experts,
138+
output_dtype=self.out_dtype,
139+
device=self.device,
140+
activation=self._activation_str,
141+
)
142+
)
143+
90144
self.w1_sf_mma: torch.Tensor | None = None
91145
self.w2_sf_mma: torch.Tensor | None = None
92146

@@ -214,7 +268,7 @@ def workspace_shapes(
214268
expert_tokens_meta: mk.ExpertTokensMetadata | None,
215269
activation: MoEActivation,
216270
) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
217-
# b12x_fused_moe manages its own internal workspace.
271+
# B12xMoEWrapper manages its own internal workspace.
218272
workspace1 = (1,)
219273
workspace2 = (0,)
220274
output_shape = (M, K)
@@ -227,24 +281,6 @@ def expects_unquantized_inputs(self) -> bool:
227281
# from pre-quantizing activations.
228282
return True
229283

230-
def _ensure_wrapper(self) -> None:
231-
"""Lazily create B12xMoEWrapper on first use."""
232-
if self._wrapper is not None:
233-
return
234-
235-
from flashinfer.fused_moe import B12xMoEWrapper
236-
237-
self._wrapper = B12xMoEWrapper(
238-
num_experts=self.global_num_experts,
239-
top_k=self.topk,
240-
hidden_size=self.hidden_dim,
241-
intermediate_size=self.intermediate_size_per_partition,
242-
use_cuda_graph=True,
243-
max_num_tokens=self.max_num_tokens,
244-
num_local_experts=self.num_local_experts,
245-
activation=self._activation_str,
246-
)
247-
248284
def apply(
249285
self,
250286
output: torch.Tensor,
@@ -276,20 +312,16 @@ def apply(
276312
"process_weights_after_loading must run before FlashInferB12xExperts.apply"
277313
)
278314

279-
self._ensure_wrapper()
280-
wrapper = self._wrapper
281-
assert wrapper is not None
282-
283-
wrapper_output = wrapper.run(
315+
wrapper_output = self._wrapper.run(
284316
x=hidden_states,
317+
token_selected_experts=topk_ids.to(torch.int32),
318+
token_final_scales=topk_weights,
285319
w1_weight=w1,
286320
w1_weight_sf=self.w1_sf_mma,
287321
w1_alpha=self.g1_alphas,
288322
fc2_input_scale=self._fc2_input_scale,
289323
w2_weight=w2,
290324
w2_weight_sf=self.w2_sf_mma,
291325
w2_alpha=self.g2_alphas,
292-
token_selected_experts=topk_ids.to(torch.int32),
293-
token_final_scales=topk_weights,
294326
)
295327
output.copy_(wrapper_output)

0 commit comments

Comments
 (0)