Skip to content

Commit 3943c1a

Browse files
authored
[QC] fused_moe blockwise fp8 w8a8 optimization (flagos-ai#2085)
1 parent 6c599cf commit 3943c1a

3 files changed

Lines changed: 467 additions & 3 deletions

File tree

benchmark/test_vllm_perf.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def is_cuda_available():
3232

3333

3434
CUDA_AVAILABLE = is_cuda_available()
35+
DEFAULT_BLOCK_SHAPE = [128, 128]
3536

3637

3738
def to_int8(tensor: torch.Tensor):
@@ -455,6 +456,117 @@ def _fp8_input_fn(self, config, dtype):
455456
)
456457

457458

459+
class FusedMoEFP8BlockwiseBenchmark(Benchmark):
460+
"""
461+
Benchmark for fused_experts_impl with FP8 W8A8 block-wise quantization.
462+
463+
Weights are stored in FP8 E4M3 and accompanied by block scales.
464+
Activations are dynamically quantized per-token per-group inside the kernel.
465+
"""
466+
467+
def __init__(self, op_name, torch_op, dtypes):
468+
super().__init__(op_name=op_name, torch_op=torch_op, dtypes=dtypes)
469+
self.block_shape = DEFAULT_BLOCK_SHAPE
470+
471+
def set_shapes(self, shape_file_path=None):
472+
# (num_tokens, num_experts, hidden_size, intermediate_size, topk)
473+
self.shapes = [
474+
# Mixtral-like shapes
475+
(1, 8, 4096, 14336, 2),
476+
(4, 8, 4096, 14336, 2),
477+
(16, 8, 4096, 14336, 2),
478+
(64, 8, 4096, 14336, 2),
479+
(128, 8, 4096, 14336, 2),
480+
(256, 8, 4096, 14336, 2),
481+
(512, 8, 4096, 14336, 2),
482+
# DeepSeek-V3-like shapes (TP=8 shard)
483+
(1, 256, 7168, 2048, 8),
484+
(4, 256, 7168, 2048, 8),
485+
(16, 256, 7168, 2048, 8),
486+
(64, 256, 7168, 2048, 8),
487+
(128, 256, 7168, 2048, 8),
488+
(256, 256, 7168, 2048, 8),
489+
# Qwen3.5-397B-A17B
490+
(1, 512, 4096, 1024, 10),
491+
(4, 512, 4096, 1024, 10),
492+
(16, 512, 4096, 1024, 10),
493+
(64, 512, 4096, 1024, 10),
494+
(128, 512, 4096, 1024, 10),
495+
(256, 512, 4096, 1024, 10),
496+
]
497+
498+
def get_input_iter(self, cur_dtype):
499+
del cur_dtype
500+
for config in self.shapes:
501+
yield from self._fp8_blockwise_input_fn(config)
502+
503+
def _fp8_blockwise_input_fn(self, config):
504+
num_tokens, num_experts, hidden_size, intermediate_size, topk = config
505+
block_n, block_k = self.block_shape
506+
device = flag_gems.device
507+
dtype = torch.bfloat16
508+
509+
hidden_states = torch.randn(num_tokens, hidden_size, device=device, dtype=dtype)
510+
w1_fp8 = (
511+
torch.randn(
512+
num_experts,
513+
intermediate_size * 2,
514+
hidden_size,
515+
device=device,
516+
dtype=torch.bfloat16,
517+
)
518+
* (1.0 / hidden_size**0.5)
519+
).to(torch.float8_e4m3fn)
520+
w2_fp8 = (
521+
torch.randn(
522+
num_experts,
523+
hidden_size,
524+
intermediate_size,
525+
device=device,
526+
dtype=torch.bfloat16,
527+
)
528+
* (1.0 / intermediate_size**0.5)
529+
).to(torch.float8_e4m3fn)
530+
531+
w1_scale = (
532+
torch.rand(
533+
num_experts,
534+
ceil(intermediate_size * 2 / block_n),
535+
ceil(hidden_size / block_k),
536+
device=device,
537+
dtype=torch.float32,
538+
)
539+
+ 0.01
540+
)
541+
w2_scale = (
542+
torch.rand(
543+
num_experts,
544+
ceil(hidden_size / block_n),
545+
ceil(intermediate_size / block_k),
546+
device=device,
547+
dtype=torch.float32,
548+
)
549+
+ 0.01
550+
)
551+
552+
gating = torch.randn(
553+
num_tokens, num_experts, device=device, dtype=torch.float32
554+
)
555+
topk_weights, topk_ids = torch.topk(torch.softmax(gating, dim=-1), topk, dim=-1)
556+
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
557+
topk_weights = topk_weights.to(torch.float32)
558+
559+
yield (
560+
hidden_states,
561+
w1_fp8,
562+
w2_fp8,
563+
w1_scale,
564+
w2_scale,
565+
topk_weights,
566+
topk_ids,
567+
)
568+
569+
458570
def _vllm_fused_moe_fp8_wrapper(
459571
hidden_states, w1, w2, topk_weights, topk_ids, w1_scale, w2_scale
460572
):
@@ -489,6 +601,42 @@ def _gems_fused_moe_fp8_wrapper(
489601
)
490602

491603

604+
def _vllm_fused_moe_fp8_blockwise_wrapper(
605+
hidden_states, w1, w2, w1_scale, w2_scale, topk_weights, topk_ids
606+
):
607+
"""Wrapper to call vllm fused_experts_impl with block-wise FP8."""
608+
return vllm_fused_experts_impl(
609+
hidden_states.clone(),
610+
w1,
611+
w2,
612+
topk_weights,
613+
topk_ids,
614+
inplace=False,
615+
activation="silu",
616+
use_fp8_w8a8=True,
617+
w1_scale=w1_scale,
618+
w2_scale=w2_scale,
619+
block_shape=DEFAULT_BLOCK_SHAPE,
620+
)
621+
622+
623+
def _gems_fused_moe_fp8_blockwise_wrapper(
624+
hidden_states, w1, w2, w1_scale, w2_scale, topk_weights, topk_ids
625+
):
626+
"""Wrapper to call FlagGems fused_experts_impl with block-wise FP8."""
627+
return flag_gems.fused_experts_impl(
628+
hidden_states,
629+
w1,
630+
w2,
631+
topk_weights,
632+
topk_ids,
633+
use_fp8_w8a8=True,
634+
w1_scale=w1_scale,
635+
w2_scale=w2_scale,
636+
block_shape=DEFAULT_BLOCK_SHAPE,
637+
)
638+
639+
492640
@pytest.mark.fused_moe
493641
@pytest.mark.skipif(
494642
not (HAS_VLLM_FUSED_MOE and CUDA_AVAILABLE),
@@ -507,6 +655,24 @@ def test_perf_fused_moe_fp8_gems_vs_vllm():
507655
bench.run()
508656

509657

658+
@pytest.mark.fused_moe
659+
@pytest.mark.skipif(
660+
not (HAS_VLLM_FUSED_MOE and CUDA_AVAILABLE),
661+
reason="requires vLLM and NVIDIA Hopper architecture for FP8 blockwise",
662+
)
663+
def test_perf_fused_moe_fp8_blockwise_gems_vs_vllm():
664+
"""
665+
Benchmark FlagGems vs vLLM fused_experts_impl with FP8 W8A8 block-wise quantization.
666+
"""
667+
bench = FusedMoEFP8BlockwiseBenchmark(
668+
op_name="fused_moe_fp8_blockwise_gems_vs_vllm",
669+
torch_op=_vllm_fused_moe_fp8_blockwise_wrapper,
670+
dtypes=[torch.bfloat16],
671+
)
672+
bench.set_gems(_gems_fused_moe_fp8_blockwise_wrapper)
673+
bench.run()
674+
675+
510676
class FusedMoEINT8Benchmark(Benchmark):
511677
"""
512678
Benchmark for fused_experts_impl with INT8 W8A8 quantization.

src/flag_gems/fused/fused_moe.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,42 @@ def try_get_optimal_moe_config(
188188
top_k: int,
189189
dtype: str | None,
190190
M: int,
191+
E: int,
191192
block_shape: list[int] | None = None,
192193
) -> dict[str, int]:
193194
override_config: Optional[dict[str, Any]] = None
195+
196+
is_hopper = torch.cuda.is_available() and torch.cuda.get_device_capability() == (
197+
9,
198+
0,
199+
)
200+
if (
201+
is_hopper
202+
and dtype == "fp8_w8a8"
203+
and block_shape is not None
204+
and len(block_shape) == 2
205+
):
206+
# Use heuristic config like hpc-ops
207+
avg_tokens_per_expert = M * top_k // E
208+
if avg_tokens_per_expert <= 16:
209+
block_size_m = 16
210+
elif avg_tokens_per_expert <= 32:
211+
block_size_m = 32
212+
elif avg_tokens_per_expert <= 48:
213+
block_size_m = 48
214+
else:
215+
block_size_m = 64
216+
config = {
217+
"BLOCK_SIZE_M": block_size_m,
218+
"BLOCK_SIZE_N": block_shape[0],
219+
"BLOCK_SIZE_K": block_shape[1],
220+
"GROUP_SIZE_M": 1,
221+
"num_warps": 4,
222+
"num_stages": 3,
223+
"SWAP_AB": True,
224+
}
225+
override_config = config
226+
194227
if override_config:
195228
config = override_config
196229
else:
@@ -508,6 +541,19 @@ def _fp8_quantize(
508541
assert len(block_shape) == 2
509542
block_k = block_shape[1]
510543
assert A.size(-1) % block_k == 0
544+
if A.ndim == 2 and A.stride(-1) == 1:
545+
from flag_gems.ops.per_token_group_quant_fp8 import (
546+
per_token_group_quant_fp8,
547+
)
548+
549+
return per_token_group_quant_fp8(
550+
A,
551+
group_size=block_k,
552+
eps=eps,
553+
dtype=fp8_dtype,
554+
column_major_scales=False,
555+
scale_ue8m0=False,
556+
)
511557
orig_shape = A.shape
512558
A_flat = A.reshape(-1, A.size(-1))
513559
M, K = A_flat.shape
@@ -913,6 +959,7 @@ def fused_moe_kernel(
913959
use_int8_w8a16: tl.constexpr,
914960
per_channel_quant: tl.constexpr,
915961
HAS_BIAS: tl.constexpr,
962+
SWAP_AB: tl.constexpr,
916963
):
917964
"""Fused MoE kernel: token × expert GEMM with quantization support."""
918965
# Map pid to C block (grouped ordering for L2 reuse)
@@ -1000,6 +1047,8 @@ def fused_moe_kernel(
10001047
bias = tl.load(bias_ptrs, mask=(offs_bn < N), other=0.0)
10011048
# Accumulate C block in fp32
10021049
accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
1050+
if SWAP_AB:
1051+
accumulator_nm = tl.zeros((BLOCK_SIZE_N, BLOCK_SIZE_M), dtype=tl.float32)
10031052
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
10041053
a = tl.load(
10051054
a_ptrs,
@@ -1016,9 +1065,16 @@ def fused_moe_kernel(
10161065
a_scale = tl.load(
10171066
a_scale_ptrs + offs_ks * stride_ask, mask=token_mask, other=0.0
10181067
)
1019-
b_scale = tl.load(b_scale_ptrs + offs_ks * stride_bsk)
1020-
1021-
accumulator += tl.dot(a, b) * a_scale[:, None] * b_scale[None, :]
1068+
if SWAP_AB:
1069+
b_scale = tl.load(b_scale_ptrs + offs_ks * stride_bsk)
1070+
accumulator_nm += (
1071+
tl.dot(tl.trans(b), tl.trans(a))
1072+
* b_scale[:, None]
1073+
* a_scale[None, :]
1074+
)
1075+
else:
1076+
b_scale = tl.load(b_scale_ptrs + offs_ks * stride_bsk)
1077+
accumulator += tl.dot(a, b) * a_scale[:, None] * b_scale[None, :]
10221078
else:
10231079
if use_fp8_w8a8:
10241080
accumulator = tl.dot(a, b, acc=accumulator)
@@ -1029,6 +1085,9 @@ def fused_moe_kernel(
10291085
a_ptrs += BLOCK_SIZE_K * stride_ak
10301086
b_ptrs += BLOCK_SIZE_K * stride_bk
10311087

1088+
if SWAP_AB:
1089+
accumulator = tl.trans(accumulator_nm)
1090+
10321091
# Dequantization
10331092
if use_int8_w8a16:
10341093
accumulator = accumulator * b_scale
@@ -1210,6 +1269,7 @@ def invoke_fused_moe_triton_kernel(
12101269
if block_shape is not None:
12111270
BLOCK_SIZE_K = min(BLOCK_SIZE_K, min(block_shape[0], block_shape[1]))
12121271

1272+
swap_AB = config.pop("SWAP_AB", False)
12131273
fused_moe_kernel[grid](
12141274
A,
12151275
B,
@@ -1251,6 +1311,7 @@ def invoke_fused_moe_triton_kernel(
12511311
naive_block_assignment=(sorted_token_ids is None),
12521312
HAS_BIAS=HAS_BIAS,
12531313
BLOCK_SIZE_K=BLOCK_SIZE_K,
1314+
SWAP_AB=swap_AB,
12541315
**config,
12551316
)
12561317

@@ -1431,6 +1492,7 @@ def fused_experts_impl(
14311492
top_k_num,
14321493
config_dtype,
14331494
block_shape=block_shape,
1495+
E=E,
14341496
)
14351497

14361498
config = get_config_func(M)

0 commit comments

Comments
 (0)