Skip to content

Commit 1d29e97

Browse files
ppu-devclaude
authored andcommitted
Add PPU-native DeepGEMM BF16 unquantized MoE (VLLM_FL_MOE=deepgemm)
Route unquantized BF16 MoE experts through deep_gemm's nopad grouped GEMM (m_grouped_gemm_bf16_bf16_bf16_nt_nopad + deepgemm_moe_permute, small-M decode auto-dispatches to the GEMV kernel) instead of FlagGems Triton fused_moe, on out-of-tree (PPU/thead) platforms. Opt-in via VLLM_FL_MOE=deepgemm; unset = unchanged. Also fixes an UnboundLocalError in _get_priority_backends for out-of-tree platforms when FlagGems is disabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d1327ae commit 1d29e97

4 files changed

Lines changed: 315 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Copyright 2026 FlagOS Contributors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""PPU-native DeepGEMM BF16 unquantized MoE experts (env-gated).
16+
17+
Opt-in via ``VLLM_FL_MOE=deepgemm`` (see ``vllm_fl.utils.use_deepgemm_moe``).
18+
Replaces the default FlagGems Triton ``fused_moe`` expert compute with
19+
``deep_gemm``'s grouped BF16 GEMM — the same kernels the vendor's native vLLM
20+
0.19 build used (``m_grouped_gemm_bf16_bf16_bf16_nt`` / ``..._gemv``).
21+
22+
Uses the **nopad** grouped GEMM with a compact (block_align=1) permute: each
23+
expert's rows are packed with NO 128-row padding, and small-M decode auto-
24+
dispatches to the GEMV kernel. (The contiguous/128-aligned layout wastes ~128x
25+
compute per active expert on sparse decode — do NOT use it here.)
26+
27+
Pipeline (BF16, no FP8 scales):
28+
deepgemm_moe_permute(block_align=1) → nopad GEMM1 → silu_and_mul
29+
→ nopad GEMM2 → weighted unpermute+reduce (ep_gather)
30+
31+
Permute/gather are vendor/vLLM Triton kernels (CUDA-graph safe); ``m_rows``
32+
(exact per-expert token counts) is fed to the nopad kernel so no host sync /
33+
internal bincount is needed.
34+
"""
35+
36+
import deep_gemm
37+
import torch
38+
from deep_gemm.deep_gemm_tuner.deepgemm_tools import deepgemm_moe_permute
39+
40+
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
41+
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
42+
from vllm.model_executor.layers.fused_moe.deep_gemm_utils import (
43+
compute_aligned_M,
44+
ep_gather,
45+
)
46+
from vllm.model_executor.layers.fused_moe.fused_moe import TritonExperts
47+
from vllm.model_executor.layers.fused_moe.utils import _resize_cache
48+
49+
from vllm_fl.ops.fused_moe.activation import apply_moe_activation
50+
51+
52+
class DeepGemmExpertsFL(TritonExperts):
53+
"""OOT unquantized BF16 MoE experts backed by deep_gemm nopad grouped GEMM.
54+
55+
Subclasses ``TritonExperts`` to inherit ``moe_problem_size``,
56+
``adjust_N_for_activation`` and the ``TopKWeightAndReduceNoOP`` finalize
57+
contract; overrides ``workspace_shapes`` (compact M_sum = M*topk) and
58+
``apply``.
59+
"""
60+
61+
def workspace_shapes(
62+
self,
63+
M: int,
64+
N: int,
65+
K: int,
66+
topk: int,
67+
global_num_experts: int,
68+
local_num_experts: int,
69+
expert_tokens_meta: "mk.ExpertTokensMetadata | None",
70+
activation: MoEActivation,
71+
) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
72+
# Compact layout (block_align=1): no per-expert 128-row padding.
73+
M_sum = compute_aligned_M(M, topk, local_num_experts, 1, expert_tokens_meta)
74+
activation_out_dim = self.adjust_N_for_activation(N, activation)
75+
workspace1 = (M_sum, max(activation_out_dim, K))
76+
workspace2 = (M_sum, max(N, K))
77+
output = (M, K)
78+
return (workspace1, workspace2, output)
79+
80+
def apply(
81+
self,
82+
output: torch.Tensor,
83+
hidden_states: torch.Tensor,
84+
w1: torch.Tensor,
85+
w2: torch.Tensor,
86+
topk_weights: torch.Tensor,
87+
topk_ids: torch.Tensor,
88+
activation: MoEActivation,
89+
global_num_experts: int,
90+
expert_map: torch.Tensor | None,
91+
a1q_scale: torch.Tensor | None,
92+
a2_scale: torch.Tensor | None,
93+
workspace13: torch.Tensor,
94+
workspace2: torch.Tensor,
95+
expert_tokens_meta: "mk.ExpertTokensMetadata | None",
96+
apply_router_weight_on_input: bool,
97+
):
98+
assert hidden_states.dtype == torch.bfloat16, (
99+
"DeepGemmExpertsFL only supports bf16 unquantized MoE"
100+
)
101+
assert hidden_states.is_contiguous()
102+
assert expert_map is None, (
103+
"DeepGemmExpertsFL does not support expert parallelism (expert_map)"
104+
)
105+
106+
a1 = hidden_states # [M, K]
107+
M, K = a1.shape
108+
local_num_experts, N, K_w = w1.shape # w1: [E, 2I, K]
109+
assert K_w == K
110+
111+
# Kernels use -1 for invalid ids -> topk_ids must be signed (router: int32).
112+
if not topk_ids.dtype.is_signed:
113+
topk_ids = topk_ids.to(torch.int32)
114+
115+
# ---- compact permute: pack tokens per-expert (no 128 padding) ----
116+
# returns: a1_perm [M_sum, K], m_indices [M_sum], inv_perm [M, topk],
117+
# m_rows (expert_num_tokens) [E]. M_sum == M * topk.
118+
a1_perm, _scale_out, m_indices, inv_perm, m_rows = deepgemm_moe_permute(
119+
a1, None, topk_ids, local_num_experts, block_align=1, block_k=K
120+
)
121+
M_sum = a1_perm.size(0)
122+
123+
# ---- grouped GEMM 1 (nopad): [M_sum, K] x [E, 2I, K]^T -> [M_sum, 2I] ----
124+
mm1_out = _resize_cache(workspace2, (M_sum, N))
125+
deep_gemm.m_grouped_gemm_bf16_bf16_bf16_nt_nopad(
126+
a1_perm, w1, mm1_out, m_indices, m_rows
127+
)
128+
129+
# ---- activation: silu_and_mul -> [M_sum, I] ----
130+
activation_out_dim = self.adjust_N_for_activation(N, activation)
131+
act_out = _resize_cache(workspace13, (M_sum, activation_out_dim))
132+
apply_moe_activation(activation, act_out, mm1_out.view(-1, N))
133+
134+
# ---- grouped GEMM 2 (nopad): [M_sum, I] x [E, K, I]^T -> [M_sum, K] ----
135+
mm2_out = _resize_cache(workspace2, (M_sum, K))
136+
deep_gemm.m_grouped_gemm_bf16_bf16_bf16_nt_nopad(
137+
act_out, w2, mm2_out, m_indices, m_rows
138+
)
139+
140+
# ---- weighted unpermute + reduce over topk -> output [M, K] ----
141+
if apply_router_weight_on_input:
142+
topk_weights = torch.ones_like(topk_weights)
143+
ep_gather(
144+
input_tensor=mm2_out,
145+
recv_topk_ids=topk_ids,
146+
recv_topk_weight=topk_weights,
147+
input_index=inv_perm,
148+
expert_map=None,
149+
output_tensor=output,
150+
)
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Copyright 2026 FlagOS Contributors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Microbenchmark for the PPU-native DeepGEMM BF16 MoE path.
16+
17+
Validates, on the real PPU device and Qwen3.5-MoE shapes, that
18+
``deep_gemm.m_grouped_gemm_bf16_bf16_bf16_nt_nopad`` can drive the two expert
19+
GEMMs of an unquantized BF16 MoE.
20+
21+
Uses a self-contained pure-torch argsort permute (dtype-agnostic, no dependency
22+
on vLLM's fp8-oriented ep_scatter nor the missing
23+
get_mk_alignment_for_contiguous_layout). This isolates the grouped GEMM math and
24+
speed; the serving path uses the vendor ``deepgemm_moe_permute`` kernel.
25+
26+
Run: python -m vllm_fl.ops.fused_moe.deepgemm_microbench
27+
"""
28+
29+
import deep_gemm
30+
import torch
31+
import torch.nn.functional as F
32+
33+
E = 256 # experts (TP, no EP -> all experts local)
34+
I = 256 # intermediate size per partition (moe_intermediate_size / TP2)
35+
H = 2048 # hidden size
36+
TOPK = 8
37+
DTYPE = torch.bfloat16
38+
DEVICE = "cuda"
39+
40+
41+
def _grouped_gemm_nopad(lhs, rhs, m_indices, m_rows):
42+
"""lhs [m,k] @ rhs[G,n,k]^T grouped by m_indices -> out [m,n]."""
43+
m, k = lhs.shape
44+
G, n, k2 = rhs.shape
45+
assert k == k2
46+
out = torch.empty(m, n, dtype=DTYPE, device=DEVICE)
47+
deep_gemm.m_grouped_gemm_bf16_bf16_bf16_nt_nopad(lhs, rhs, out, m_indices, m_rows)
48+
return out
49+
50+
51+
def _permute(hidden, topk_ids):
52+
"""Pure-torch permute: group (token,expert) rows contiguously by expert.
53+
54+
Returns a1[m,H], m_indices[m] int32, m_rows[E] int32, order, tok_of_row.
55+
"""
56+
M = hidden.shape[0]
57+
flat_expert = topk_ids.reshape(-1).to(torch.int32) # [M*topk]
58+
tok_idx = torch.arange(M, device=DEVICE).repeat_interleave(TOPK) # [M*topk]
59+
order = torch.argsort(flat_expert) # group by expert
60+
m_indices = flat_expert[order].contiguous()
61+
tok_of_row = tok_idx[order].contiguous()
62+
a1 = hidden.index_select(0, tok_of_row).contiguous() # [m,H]
63+
m_rows = torch.bincount(flat_expert, minlength=E).to(torch.int32)
64+
return a1, m_indices, m_rows, order, tok_of_row
65+
66+
67+
def _run(M, w1, w2, gen, check_numeric):
68+
hidden = torch.randn(M, H, dtype=DTYPE, device=DEVICE, generator=gen)
69+
topk_ids = torch.randint(
70+
0, E, (M, TOPK), device=DEVICE, generator=gen, dtype=torch.int64
71+
)
72+
topk_w = torch.rand(M, TOPK, dtype=torch.float32, device=DEVICE, generator=gen)
73+
74+
def pipeline():
75+
a1, m_indices, m_rows, order, tok_of_row = _permute(hidden, topk_ids)
76+
mm1 = _grouped_gemm_nopad(a1, w1, m_indices, m_rows) # [m, 2I]
77+
act = (
78+
F.silu(mm1[:, :I].float()).to(DTYPE) * mm1[:, I:]
79+
) # silu_and_mul -> [m, I]
80+
act = act.contiguous()
81+
mm3 = _grouped_gemm_nopad(act, w2, m_indices, m_rows) # [m, H]
82+
# unpermute + weighted reduce
83+
tmp = torch.empty_like(mm3)
84+
tmp[order] = mm3
85+
tmp = tmp.view(M, TOPK, H)
86+
out = (tmp.float() * topk_w.unsqueeze(-1)).sum(1).to(DTYPE)
87+
return out
88+
89+
out = pipeline()
90+
torch.cuda.synchronize()
91+
ok = bool(torch.isfinite(out).all())
92+
print(f"\n=== M={M} (m={M * TOPK}) === out {tuple(out.shape)} finite={ok}")
93+
94+
if check_numeric:
95+
# reference via gathered per-row bmm (bounded memory: only small M)
96+
a1, m_indices, m_rows, order, tok_of_row = _permute(hidden, topk_ids)
97+
w1g = w1.index_select(0, m_indices) # [m,2I,H]
98+
gate_up = torch.bmm(w1g.float(), a1.float().unsqueeze(-1)).squeeze(-1) # [m,2I]
99+
ref_act = F.silu(gate_up[:, :I]) * gate_up[:, I:] # [m,I]
100+
w2g = w2.index_select(0, m_indices) # [m,H,I]
101+
ref_row = torch.bmm(w2g.float(), ref_act.unsqueeze(-1)).squeeze(-1) # [m,H]
102+
tmp = torch.empty(M * TOPK, H, device=DEVICE)
103+
tmp[order] = ref_row
104+
ref = (tmp.view(M, TOPK, H) * topk_w.unsqueeze(-1)).sum(1)
105+
diff = (out.float() - ref).abs()
106+
rel = diff.max().item() / (ref.abs().max().item() + 1e-6)
107+
print(
108+
f" numeric vs torch ref: max_abs={diff.max().item():.3f} "
109+
f"max_rel={rel:.4f} -> {'PASS' if rel < 2e-2 else 'CHECK'}"
110+
)
111+
112+
# timing
113+
for _ in range(5):
114+
pipeline()
115+
torch.cuda.synchronize()
116+
s = torch.cuda.Event(enable_timing=True)
117+
e = torch.cuda.Event(enable_timing=True)
118+
s.record()
119+
for _ in range(20):
120+
pipeline()
121+
e.record()
122+
torch.cuda.synchronize()
123+
print(f" full pipeline: {s.elapsed_time(e) / 20 * 1000:.1f} us/iter")
124+
125+
126+
def main():
127+
print("device:", torch.cuda.get_device_name(0))
128+
gen = torch.Generator(device=DEVICE).manual_seed(0)
129+
w1 = torch.randn(E, 2 * I, H, dtype=DTYPE, device=DEVICE, generator=gen) * 0.02
130+
w2 = torch.randn(E, H, I, dtype=DTYPE, device=DEVICE, generator=gen) * 0.02
131+
132+
# decode-ish (small M -> should hit gemv path) with numeric check
133+
_run(16, w1, w2, gen, check_numeric=True)
134+
# prefill-ish (large M -> gemm path), finite + latency only
135+
_run(2048, w1, w2, gen, check_numeric=False)
136+
print("\nValidation complete: see finite/PASS results above")
137+
138+
139+
if __name__ == "__main__":
140+
main()

vllm_fl/ops/fused_moe/fused_moe_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ def _move_to_back(
8585
_AVAILABLE_BACKENDS = [UnquantizedMoeBackend.XPU]
8686
elif current_platform.is_cpu():
8787
_AVAILABLE_BACKENDS = [UnquantizedMoeBackend.CPU]
88+
else:
89+
# Out-of-tree platforms (e.g. the FL plugin backends). When FlagGems is
90+
# disabled (USE_FLAGGEMS=0) we still need a native fallback, otherwise
91+
# _AVAILABLE_BACKENDS would be unbound. Native Triton MoE kernels work
92+
# on these CUDA-like devices.
93+
_AVAILABLE_BACKENDS = [
94+
UnquantizedMoeBackend.TRITON,
95+
UnquantizedMoeBackend.BATCHED_TRITON,
96+
]
8897
return _AVAILABLE_BACKENDS
8998

9099
## Adopt from select_unquantized_moe_backend
@@ -103,6 +112,12 @@ def select_unquantized_moe_backend_oot(moe_config: FusedMoEConfig,
103112
return UnquantizedMoeBackend.TPU, None
104113

105114
if current_platform.is_out_of_tree() and use_flaggems():
115+
# Opt-in (VLLM_FL_MOE=deepgemm): route BF16 experts through the
116+
# PPU-native DeepGEMM grouped GEMM instead of FlagGems Triton fused_moe.
117+
from vllm_fl.utils import use_deepgemm_moe
118+
if use_deepgemm_moe():
119+
from vllm_fl.ops.fused_moe.deepgemm_experts import DeepGemmExpertsFL
120+
return UnquantizedMoeBackend.TRITON, DeepGemmExpertsFL
106121
return UnquantizedMoeBackend.TRITON, TritonExpertsFL
107122

108123
if moe_config.is_lora_enabled:

vllm_fl/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ def use_flaggems(default: bool = True) -> bool:
9797
return value.lower() in ("true", "1")
9898

9999

100+
def use_deepgemm_moe() -> bool:
101+
"""Opt-in switch to route the unquantized BF16 MoE experts through the
102+
PPU-native DeepGEMM grouped GEMM (``deep_gemm``) instead of the default
103+
FlagGems/Triton fused_moe. Enabled with ``VLLM_FL_MOE=deepgemm``.
104+
105+
When unset, behavior is identical to the previous default path.
106+
"""
107+
return os.environ.get("VLLM_FL_MOE", "").strip().lower() == "deepgemm"
108+
109+
100110
def get_flag_gems_whitelist_blacklist() -> Tuple[
101111
Optional[list[str]], Optional[list[str]]
102112
]:

0 commit comments

Comments
 (0)