Skip to content

Commit 8292034

Browse files
committed
Rename fused_marlin_moe kernels by precision scheme and split benchmarks
1 parent e6bd49b commit 8292034

11 files changed

Lines changed: 909 additions & 814 deletions

benchmark/test_fused_marlin_moe.py

Lines changed: 0 additions & 743 deletions
This file was deleted.
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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+
import pytest
16+
import torch
17+
18+
# vLLM imports (baseline). Optional: when vllm is not installed (e.g. in CI),
19+
# the entire benchmark is skipped via the skipif marker below.
20+
try:
21+
from vllm.model_executor.layers.fused_moe.fused_marlin_moe import (
22+
fused_marlin_moe as vllm_fused_marlin_moe,
23+
)
24+
from vllm.model_executor.layers.quantization.utils.marlin_utils_test import (
25+
marlin_quantize,
26+
)
27+
from vllm.model_executor.layers.quantization.utils.quant_utils import (
28+
quantize_weights,
29+
)
30+
from vllm.scalar_type import scalar_types
31+
32+
VLLM_QUANT_TYPE = scalar_types.uint4b8
33+
HAS_VLLM_FUSED_MARLIN_MOE = True
34+
except ImportError:
35+
HAS_VLLM_FUSED_MARLIN_MOE = False
36+
37+
import flag_gems
38+
39+
# FlagGems wrapper under test
40+
from flag_gems.fused.fused_marlin_moe import QUANT_TYPE_UINT4B8
41+
from flag_gems.fused.fused_marlin_moe import fused_marlin_moe as gems_fused_marlin_moe
42+
43+
from . import base
44+
45+
46+
def is_cuda_available():
47+
if flag_gems.device != "cuda":
48+
return False
49+
major, minor = torch.cuda.get_device_capability()
50+
sm_version_num = major * 10 + minor
51+
return sm_version_num >= 90 and sm_version_num < 100
52+
53+
54+
CUDA_AVAILABLE = is_cuda_available()
55+
56+
GROUP_SIZE = 128
57+
58+
59+
def _wna16_quantize_per_expert(w_fp):
60+
"""
61+
Per-expert GPTQ-style INT4 quantization for FlagGems wna16 kernel layout.
62+
63+
Input w_fp: (E, out_dim, in_dim), bf16/fp16
64+
Output w_q: (E, out_dim, in_dim // 2), uint8 (two nibbles per byte)
65+
scales: (E, out_dim, in_dim // GROUP_SIZE), same dtype as w_fp
66+
"""
67+
E, out_dim, in_dim = w_fp.shape
68+
assert in_dim % GROUP_SIZE == 0
69+
w_q = torch.empty(E, out_dim, in_dim // 2, device=w_fp.device, dtype=torch.uint8)
70+
scales = torch.empty(
71+
E, out_dim, in_dim // GROUP_SIZE, device=w_fp.device, dtype=w_fp.dtype
72+
)
73+
for e in range(E):
74+
_, q_e, sc_e, _ = quantize_weights(
75+
w_fp[e].T, VLLM_QUANT_TYPE, GROUP_SIZE, False, False
76+
)
77+
q_e = q_e.T.contiguous().to(torch.uint8)
78+
sc_e = sc_e.T
79+
w_q[e] = q_e[:, 1::2] * 16 + q_e[:, ::2]
80+
scales[e] = sc_e
81+
return w_q, scales
82+
83+
84+
def _marlin_quantize_per_expert(w_fp):
85+
"""
86+
Per-expert Marlin-layout INT4 quantization for vLLM's fused_marlin_moe.
87+
88+
Input w_fp: (E, out_dim, in_dim), bf16/fp16
89+
Output qweight: stacked (E, ...), int32 (Marlin packed layout)
90+
scales: stacked (E, ...), same dtype as w_fp
91+
"""
92+
qweight_l, scales_l = [], []
93+
E = w_fp.shape[0]
94+
for e in range(E):
95+
# marlin_quantize expects (in_dim, out_dim)
96+
_, qw, sc, _, _, _ = marlin_quantize(
97+
w_fp[e].T.contiguous(), VLLM_QUANT_TYPE, GROUP_SIZE, act_order=False
98+
)
99+
qweight_l.append(qw)
100+
scales_l.append(sc)
101+
qweight = torch.stack(qweight_l, dim=0).contiguous()
102+
scales = torch.stack(scales_l, dim=0).contiguous()
103+
return qweight, scales
104+
105+
106+
class FusedMarlinMoEW4A16INT4Benchmark(base.Benchmark):
107+
"""
108+
Benchmark for fused_marlin_moe W4A16 INT4 (fused-dequant MoE GEMM).
109+
110+
Compares FlagGems' Triton wna16 kernel against vLLM's Marlin CUDA kernel.
111+
Both consume per-group-128 GPTQ uint4b8 weights (different packed layouts).
112+
"""
113+
114+
def __init__(self, op_name, torch_op, dtypes):
115+
super().__init__(op_name=op_name, torch_op=torch_op, dtypes=dtypes)
116+
117+
def set_shapes(self, shape_file_path=None):
118+
# The three production MoE architectures from profile_fused_marlin_moe.py
119+
# over the decode token range (1 .. 256).
120+
self.shapes = [
121+
# Mixtral-8x7B
122+
(1, 8, 4096, 14336, 2),
123+
(4, 8, 4096, 14336, 2),
124+
(8, 8, 4096, 14336, 2),
125+
(16, 8, 4096, 14336, 2),
126+
(32, 8, 4096, 14336, 2),
127+
(64, 8, 4096, 14336, 2),
128+
(128, 8, 4096, 14336, 2),
129+
(256, 8, 4096, 14336, 2),
130+
# DeepSeek-V3 (TP=8 shard)
131+
(1, 256, 7168, 2048, 8),
132+
(4, 256, 7168, 2048, 8),
133+
(8, 256, 7168, 2048, 8),
134+
(16, 256, 7168, 2048, 8),
135+
(32, 256, 7168, 2048, 8),
136+
(64, 256, 7168, 2048, 8),
137+
(128, 256, 7168, 2048, 8),
138+
(256, 256, 7168, 2048, 8),
139+
# Qwen3-5-397B-A17B
140+
(1, 512, 4096, 1024, 10),
141+
(4, 512, 4096, 1024, 10),
142+
(8, 512, 4096, 1024, 10),
143+
(16, 512, 4096, 1024, 10),
144+
(32, 512, 4096, 1024, 10),
145+
(64, 512, 4096, 1024, 10),
146+
(128, 512, 4096, 1024, 10),
147+
(256, 512, 4096, 1024, 10),
148+
# DeepSeek-V4-Flash
149+
(1, 256, 4096, 2048, 6),
150+
(4, 256, 4096, 2048, 6),
151+
(8, 256, 4096, 2048, 6),
152+
(16, 256, 4096, 2048, 6),
153+
(32, 256, 4096, 2048, 6),
154+
(64, 256, 4096, 2048, 6),
155+
(128, 256, 4096, 2048, 6),
156+
(256, 256, 4096, 2048, 6),
157+
]
158+
159+
def get_input_iter(self, cur_dtype):
160+
for config in self.shapes:
161+
yield from self._gen(config, cur_dtype)
162+
163+
def _gen(self, config, dtype):
164+
num_tokens, num_experts, hidden_size, intermediate_size, topk = config
165+
device = flag_gems.device
166+
167+
hidden_states = torch.randn(num_tokens, hidden_size, device=device, dtype=dtype)
168+
169+
# Original FP weights (kept only as source for both quantizers).
170+
w1_fp = (
171+
torch.randn(
172+
num_experts,
173+
intermediate_size * 2,
174+
hidden_size,
175+
device=device,
176+
dtype=dtype,
177+
)
178+
/ 10.0
179+
)
180+
w2_fp = (
181+
torch.randn(
182+
num_experts,
183+
hidden_size,
184+
intermediate_size,
185+
device=device,
186+
dtype=dtype,
187+
)
188+
/ 10.0
189+
)
190+
191+
# FlagGems wna16 layout
192+
w1_q_wna16, w1_scale_wna16 = _wna16_quantize_per_expert(w1_fp)
193+
w2_q_wna16, w2_scale_wna16 = _wna16_quantize_per_expert(w2_fp)
194+
195+
# vLLM Marlin layout
196+
w1_q_marlin, w1_scale_marlin = _marlin_quantize_per_expert(w1_fp)
197+
w2_q_marlin, w2_scale_marlin = _marlin_quantize_per_expert(w2_fp)
198+
199+
del w1_fp, w2_fp
200+
torch.cuda.empty_cache()
201+
202+
# Routing
203+
gating = torch.randn(
204+
num_tokens, num_experts, device=device, dtype=torch.float32
205+
)
206+
topk_weights, topk_ids = torch.topk(torch.softmax(gating, dim=-1), topk, dim=-1)
207+
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
208+
# vLLM requires fp32 topk_weights; FlagGems wrapper is dtype-agnostic.
209+
210+
# Both ops get the same tuple; each picks what it needs.
211+
yield (
212+
hidden_states,
213+
w1_q_wna16,
214+
w2_q_wna16,
215+
w1_scale_wna16,
216+
w2_scale_wna16,
217+
w1_q_marlin,
218+
w2_q_marlin,
219+
w1_scale_marlin,
220+
w2_scale_marlin,
221+
topk_weights,
222+
topk_ids,
223+
)
224+
225+
226+
def _vllm_baseline(
227+
hidden_states,
228+
w1_q_wna16,
229+
w2_q_wna16,
230+
w1_scale_wna16,
231+
w2_scale_wna16,
232+
w1_q_marlin,
233+
w2_q_marlin,
234+
w1_scale_marlin,
235+
w2_scale_marlin,
236+
topk_weights,
237+
topk_ids,
238+
):
239+
"""Baseline: vLLM's CUDA Marlin fused_marlin_moe."""
240+
return vllm_fused_marlin_moe(
241+
hidden_states=hidden_states,
242+
w1=w1_q_marlin,
243+
w2=w2_q_marlin,
244+
bias1=None,
245+
bias2=None,
246+
w1_scale=w1_scale_marlin,
247+
w2_scale=w2_scale_marlin,
248+
topk_weights=topk_weights,
249+
topk_ids=topk_ids,
250+
quant_type_id=VLLM_QUANT_TYPE.id,
251+
)
252+
253+
254+
def _gems_call(
255+
hidden_states,
256+
w1_q_wna16,
257+
w2_q_wna16,
258+
w1_scale_wna16,
259+
w2_scale_wna16,
260+
w1_q_marlin,
261+
w2_q_marlin,
262+
w1_scale_marlin,
263+
w2_scale_marlin,
264+
topk_weights,
265+
topk_ids,
266+
):
267+
"""FlagGems' Triton wna16 fused_marlin_moe (Phase 2)."""
268+
return gems_fused_marlin_moe(
269+
hidden_states=hidden_states,
270+
w1=w1_q_wna16,
271+
w2=w2_q_wna16,
272+
bias1=None,
273+
bias2=None,
274+
w1_scale=w1_scale_wna16,
275+
w2_scale=w2_scale_wna16,
276+
topk_weights=topk_weights,
277+
topk_ids=topk_ids,
278+
quant_type_id=QUANT_TYPE_UINT4B8,
279+
)
280+
281+
282+
@pytest.mark.fused_marlin_moe
283+
@pytest.mark.skipif(
284+
not HAS_VLLM_FUSED_MARLIN_MOE, reason="vllm not installed; baseline unavailable"
285+
)
286+
@pytest.mark.skipif(not CUDA_AVAILABLE, reason="requires NVIDIA Hopper architecture")
287+
def test_fused_marlin_moe_w4a16_int4():
288+
"""
289+
Benchmark FlagGems fused_marlin_moe (Triton wna16) vs vLLM fused_marlin_moe
290+
(CUDA Marlin). Both run GPTQ uint4b8 + per-group-128 W4A16 GEMM.
291+
"""
292+
bench = FusedMarlinMoEW4A16INT4Benchmark(
293+
op_name="fused_marlin_moe_w4a16_int4",
294+
torch_op=_vllm_baseline,
295+
dtypes=[torch.bfloat16],
296+
)
297+
bench.set_gems(_gems_call)
298+
bench.run()

0 commit comments

Comments
 (0)