Skip to content

Commit 0855f9d

Browse files
committed
Refine fused moe demo, add source code.
1 parent 9befc49 commit 0855f9d

10 files changed

Lines changed: 8977 additions & 34 deletions

File tree

‎optimization/assets/fused-moe-fp8/00_baseline.co‎

Lines changed: 935 additions & 0 deletions
Large diffs are not rendered by default.

‎optimization/assets/fused-moe-fp8/01_scatter_fusion.co‎

Lines changed: 955 additions & 0 deletions
Large diffs are not rendered by default.

‎optimization/assets/fused-moe-fp8/02_qsg_fusion.co‎

Lines changed: 1049 additions & 0 deletions
Large diffs are not rendered by default.

‎optimization/assets/fused-moe-fp8/03_all_fusions.co‎

Lines changed: 1106 additions & 0 deletions
Large diffs are not rendered by default.

‎optimization/assets/fused-moe-fp8/04_host_micro.co‎

Lines changed: 1788 additions & 0 deletions
Large diffs are not rendered by default.

‎optimization/assets/fused-moe-fp8/05_cuda_optimized.co‎

Lines changed: 1774 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/env python3
2+
"""Benchmark vLLM's Triton fused_moe kernel for a SINGLE grouped GEMM.
3+
4+
Matches Croqtile's workload exactly:
5+
M=128 tokens, N=512, K=2048, 256 experts, top-8 routing, FP8 (e4m3)
6+
7+
This is NOT the full MLP (gate+up -> SiLU -> down). It is ONE grouped GEMM
8+
like Croqtile's fused_moe_grouped_wgmma_fp8, so the TFLOPS are comparable.
9+
10+
Two timed configurations:
11+
12+
1. GEMM-only: just dispatch_fused_moe_kernel (no Croqtile equivalent)
13+
2. End-to-end: zero + route + quant + align + GEMM + reduce
14+
(matches Croqtile launch_end_to_end)
15+
16+
FLOP formula (same as Croqtile):
17+
FLOPs = 2 * expanded_m * N * K = 2 * (M * topk) * N * K
18+
19+
Usage:
20+
python bench_vllm_single_gemm.py --gpu 1
21+
python bench_vllm_single_gemm.py --m 256 --gpu 0
22+
"""
23+
24+
import argparse
25+
26+
import torch
27+
import triton.language as tl
28+
from vllm import _custom_ops as ops
29+
from vllm.model_executor.layers.fused_moe.fused_moe import (
30+
dispatch_fused_moe_kernel,
31+
moe_align_block_size,
32+
moe_kernel_quantize_input,
33+
try_get_optimal_moe_config,
34+
)
35+
from vllm.model_executor.layers.fused_moe import fused_topk
36+
from vllm.platforms import current_platform
37+
38+
FP8_DTYPE = current_platform.fp8_dtype()
39+
40+
41+
def bench_single_gemm(
42+
m: int = 128,
43+
n: int = 512,
44+
k: int = 2048,
45+
num_experts: int = 256,
46+
topk: int = 8,
47+
num_warmup: int = 50,
48+
num_iters: int = 500,
49+
device: str = "cuda",
50+
config_override: dict | None = None,
51+
):
52+
expanded_m = m * topk
53+
54+
# --- Setup (not timed) ---
55+
a_bf16 = torch.randn((m, k), device=device, dtype=torch.bfloat16) / 10
56+
w_bf16 = torch.randn(
57+
(num_experts, n, k), device=device, dtype=torch.bfloat16
58+
) / 10
59+
gating = torch.randn((m, num_experts), device=device, dtype=torch.float32)
60+
61+
# Pre-quantize weights (weights are FP8 with per-expert scales)
62+
w_fp8 = w_bf16.to(FP8_DTYPE)
63+
w_scale = (
64+
w_bf16.abs().amax(dim=-1, keepdim=True).float().clamp(min=1e-12) / 448.0
65+
)
66+
67+
# Pre-quantize activations for GEMM-only path
68+
a_fp8_pre = a_bf16.to(FP8_DTYPE)
69+
a_scale_pre = (
70+
a_bf16.abs().amax(dim=-1, keepdim=True).float().clamp(min=1e-12) / 448.0
71+
)
72+
73+
# Pre-route for GEMM-only path
74+
topk_weights_pre, topk_ids_pre, _ = fused_topk(
75+
a_bf16, gating, topk, renormalize=True
76+
)
77+
78+
# Output buffers
79+
intermediate = torch.zeros(
80+
(m, topk, n), device=device, dtype=torch.bfloat16
81+
)
82+
final_output = torch.zeros((m, n), device=device, dtype=torch.bfloat16)
83+
84+
# Get Triton autotuning config
85+
if config_override:
86+
config = config_override
87+
print(f" Triton config (OVERRIDE): {config}")
88+
else:
89+
config = try_get_optimal_moe_config(
90+
w1_shape=w_fp8.shape,
91+
w2_shape=w_fp8.shape,
92+
top_k=topk,
93+
dtype=str(FP8_DTYPE),
94+
M=m,
95+
block_shape=None,
96+
)
97+
print(f" Triton config (auto): {config}")
98+
99+
# Pre-align for GEMM-only path
100+
sorted_ids_pre, expert_ids_pre, ntp_pre = moe_align_block_size(
101+
topk_ids_pre, config["BLOCK_SIZE_M"], num_experts
102+
)
103+
104+
def dispatch_gemm(a_q, a_s, tw, st, ei, ntp):
105+
dispatch_fused_moe_kernel(
106+
A=a_q,
107+
B=w_fp8,
108+
C=intermediate,
109+
A_scale=a_s,
110+
B_scale=w_scale,
111+
B_zp=None,
112+
topk_weights=tw,
113+
sorted_token_ids=st,
114+
expert_ids=ei,
115+
num_tokens_post_padded=ntp,
116+
mul_routed_weight=True,
117+
top_k=topk,
118+
config=config,
119+
compute_type=tl.bfloat16,
120+
use_fp8_w8a8=True,
121+
use_int8_w8a8=False,
122+
use_int8_w8a16=False,
123+
use_int4_w4a16=False,
124+
per_channel_quant=True,
125+
block_shape=None,
126+
)
127+
128+
# --- Configuration 1: GEMM only ---
129+
def run_gemm_only():
130+
dispatch_gemm(
131+
a_fp8_pre, a_scale_pre, topk_weights_pre,
132+
sorted_ids_pre, expert_ids_pre, ntp_pre,
133+
)
134+
135+
# --- Configuration 2: End-to-end ---
136+
# Matches Croqtile launch_end_to_end():
137+
# zero → route → count → build → quantize → sort → GEMM → scatter
138+
def run_end_to_end():
139+
final_output.zero_()
140+
intermediate.zero_()
141+
tw, ti, _ = fused_topk(a_bf16, gating, topk, renormalize=True)
142+
a_q, a_s = moe_kernel_quantize_input(
143+
A=a_bf16, A_scale=None, quant_dtype=FP8_DTYPE,
144+
per_act_token_quant=True, block_shape=None,
145+
)
146+
st, ei, ntp = moe_align_block_size(
147+
ti, config["BLOCK_SIZE_M"], num_experts
148+
)
149+
dispatch_gemm(a_q, a_s, tw, st, ei, ntp)
150+
ops.moe_sum(intermediate.view(m, -1, n), final_output)
151+
152+
flops = 2.0 * expanded_m * n * k
153+
154+
def measure(fn, label):
155+
for _ in range(num_warmup):
156+
fn()
157+
torch.cuda.synchronize()
158+
159+
start_events = [
160+
torch.cuda.Event(enable_timing=True) for _ in range(num_iters)
161+
]
162+
end_events = [
163+
torch.cuda.Event(enable_timing=True) for _ in range(num_iters)
164+
]
165+
for i in range(num_iters):
166+
torch.cuda.synchronize()
167+
start_events[i].record()
168+
fn()
169+
end_events[i].record()
170+
torch.cuda.synchronize()
171+
172+
times_ms = [s.elapsed_time(e) for s, e in zip(start_events, end_events)]
173+
avg_ms = sum(times_ms) / len(times_ms)
174+
min_ms = min(times_ms)
175+
max_ms = max(times_ms)
176+
tflops_val = (flops / (avg_ms / 1000.0)) / 1e12
177+
print(f" [{label}]")
178+
print(
179+
f" Avg: {avg_ms:.4f} ms "
180+
f"Min: {min_ms:.4f} ms Max: {max_ms:.4f} ms "
181+
f"TFLOPS: {tflops_val:.3f}"
182+
)
183+
return avg_ms, tflops_val
184+
185+
print(f"\n{'='*65}")
186+
print(f"vLLM Triton · single grouped GEMM (FP8)")
187+
print(f" M={m}, N={n}, K={k}, experts={num_experts}, topk={topk}")
188+
print(f" expanded_m = {expanded_m} (= M * topk)")
189+
print(f" FLOPs = 2 * {expanded_m} * {n} * {k} = {flops/1e9:.3f} GFLOP")
190+
print(f" vLLM version: ", end="")
191+
try:
192+
import vllm
193+
print(vllm.__version__)
194+
except Exception:
195+
print("unknown")
196+
print(f" FP8 dtype: {FP8_DTYPE}")
197+
print(f" GPU: {torch.cuda.get_device_name()}")
198+
print(f" Warmup: {num_warmup}, Iterations: {num_iters}")
199+
print(f"{'='*65}")
200+
201+
gemm_ms, gemm_tf = measure(run_gemm_only, "GEMM only")
202+
e2e_ms, e2e_tf = measure(
203+
run_end_to_end, "End-to-end (zero+route+quant+align+GEMM+reduce)"
204+
)
205+
206+
print(f"{'='*65}")
207+
print(f"\n Comparison with Croqtile (same FLOP formula, same hardware):")
208+
print(f" Croqtile numbers from 05_cuda_optimized.co, 500-rep timing.")
209+
print()
210+
print(f" {'Metric':<35} {'Croqtile':>10} {'vLLM':>10}")
211+
print(f" {'-'*35} {'-'*10} {'-'*10}")
212+
print(f" {'GEMM-only latency':<35} {'n/a':>10} {gemm_ms:>9.4f}ms")
213+
print(f" {'End-to-end latency':<35} {'~0.164ms':>10} {e2e_ms:>9.4f}ms")
214+
print(f" {'End-to-end TFLOPS':<35} {'13.18':>10} {e2e_tf:>10.3f}")
215+
print(f" {'Speedup (Croqtile / vLLM)':<35} {'':>10} {13.18/e2e_tf:>9.2f}x")
216+
print()
217+
218+
return e2e_ms, e2e_tf
219+
220+
221+
if __name__ == "__main__":
222+
parser = argparse.ArgumentParser(
223+
description="Benchmark vLLM Triton single grouped GEMM (FP8)",
224+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
225+
)
226+
parser.add_argument("--m", type=int, default=128, help="Number of tokens")
227+
parser.add_argument("--n", type=int, default=512, help="Output dimension")
228+
parser.add_argument(
229+
"--k", type=int, default=2048, help="Input/hidden dimension"
230+
)
231+
parser.add_argument(
232+
"--experts", type=int, default=256, help="Number of experts"
233+
)
234+
parser.add_argument("--topk", type=int, default=8, help="Top-k routing")
235+
parser.add_argument("--warmup", type=int, default=50, help="Warmup iters")
236+
parser.add_argument("--iters", type=int, default=500, help="Timing iters")
237+
parser.add_argument(
238+
"--gpu", type=int, default=None, help="GPU index (CUDA_VISIBLE_DEVICES)"
239+
)
240+
parser.add_argument(
241+
"--config", type=str, default=None,
242+
help='Override Triton config as JSON, e.g. \'{"BLOCK_SIZE_M":16,...}\''
243+
)
244+
args = parser.parse_args()
245+
246+
if args.gpu is not None:
247+
import os
248+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
249+
250+
config_override = None
251+
if args.config:
252+
import json
253+
config_override = json.loads(args.config)
254+
255+
bench_single_gemm(
256+
m=args.m,
257+
n=args.n,
258+
k=args.k,
259+
num_experts=args.experts,
260+
topk=args.topk,
261+
num_warmup=args.warmup,
262+
num_iters=args.iters,
263+
config_override=config_override,
264+
)
63.8 KB
Binary file not shown.

0 commit comments

Comments
 (0)