Repository navigation
[Kernel] Add prepared-input fast path with MiniMax-M2 top-k/act quant fusion #43592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
qianlihuang
wants to merge
26
commits into
vllm-project:main
Choose a base branch
from
qianlihuang:minimax-m2-moe-prepared-input
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
edaba56
Add prepared FP8 input path for MMM2 MoE
qianlihuang 62fd04e
fix
qianlihuang b917f81
fuse kernel
qianlihuang 7a270bc
fix precision
qianlihuang b09b98f
init
qianlihuang c7d55f8
gate
qianlihuang 2e4b27f
add bench
qianlihuang 6200fc1
apply when m<=128
qianlihuang 01007fd
add cuda kernel
qianlihuang 681499c
fix cuda
qianlihuang 427881e
let ai cook
qianlihuang c5248d6
let her cook
qianlihuang 8149a73
try
qianlihuang 0769c79
give up cuda for now
qianlihuang c2e2c6c
rm dead code
qianlihuang 9981231
take advice
qianlihuang b2d1211
modify bench
qianlihuang 80bf43a
improve triton
qianlihuang 614a9f2
Merge branch 'main' into minimax-m2-moe-prepared-input
qianlihuang 43bd744
update triton
qianlihuang 5eb39b7
clean
qianlihuang c9e8c0d
add 128
qianlihuang 8568751
fix
qianlihuang c37d361
take advice
qianlihuang 508b430
fix bench
qianlihuang 9534162
move
qianlihuang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,381 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """Benchmark MiniMax-M2 fused MoE top-k routing + FP8 input quantization. | ||
|
|
||
| Example: | ||
| python3 benchmarks/kernels/benchmark_minimax_moe_topk_quant.py \ | ||
| --m-values 1 2 4 8 16 32 64 128 256 512 1024 \ | ||
| --modes eager cudagraph | ||
| """ | ||
|
|
||
| import argparse | ||
| import itertools | ||
| import math | ||
| from collections.abc import Callable | ||
| from dataclasses import dataclass | ||
|
|
||
| import torch | ||
|
|
||
| from vllm import _custom_ops as ops | ||
| from vllm.model_executor.layers.fused_moe.topk_quant_kernels import ( | ||
| minimax_moe_topk_sigmoid_quant, | ||
| ) | ||
| from vllm.model_executor.layers.quantization.utils.fp8_utils import ( | ||
| per_token_group_quant_fp8, | ||
| ) | ||
| from vllm.platforms import current_platform | ||
| from vllm.triton_utils import triton | ||
| from vllm.utils.argparse_utils import FlexibleArgumentParser | ||
| from vllm.utils.torch_utils import set_random_seed | ||
|
|
||
|
|
||
| AccuracyResult = tuple[int | None, float | None, float | None, float | None] | ||
|
|
||
|
|
||
| @dataclass | ||
| class BenchResult: | ||
| mode: str | ||
| m: int | ||
| hidden_size: int | ||
| num_experts: int | ||
| top_k: int | ||
| block_k: int | ||
| topk_us: float | ||
| quant_us: float | ||
| baseline_us: float | ||
| fused_us: float | ||
| speedup: float | ||
| delta_us: float | ||
| topk_id_mismatch: int | None | ||
| topk_weight_max_abs_err: float | None | ||
| a1q_max_abs_err: float | None | ||
| a1q_scale_max_abs_err: float | None | ||
|
|
||
|
|
||
| def _baseline_topk_sigmoid( | ||
| router_logits: torch.Tensor, | ||
| e_score_correction_bias: torch.Tensor, | ||
| top_k: int, | ||
| ) -> tuple[torch.Tensor, torch.Tensor]: | ||
| num_tokens = router_logits.shape[0] | ||
| topk_weights = torch.empty( | ||
| (num_tokens, top_k), dtype=torch.float32, device=router_logits.device | ||
| ) | ||
| topk_ids = torch.empty( | ||
| (num_tokens, top_k), dtype=torch.int32, device=router_logits.device | ||
| ) | ||
| token_expert_indices = torch.empty_like(topk_ids) | ||
| ops.topk_sigmoid( | ||
| topk_weights, | ||
| topk_ids, | ||
| token_expert_indices, | ||
| router_logits, | ||
| True, | ||
| e_score_correction_bias, | ||
| ) | ||
| return topk_weights, topk_ids | ||
|
|
||
|
|
||
| def _baseline_quant( | ||
| hidden_states: torch.Tensor, | ||
| block_k: int, | ||
| ) -> tuple[torch.Tensor, torch.Tensor]: | ||
| return per_token_group_quant_fp8(hidden_states, block_k, use_ue8m0=False) | ||
|
|
||
|
|
||
| def _baseline_topk_quant( | ||
| hidden_states: torch.Tensor, | ||
| router_logits: torch.Tensor, | ||
| e_score_correction_bias: torch.Tensor, | ||
| top_k: int, | ||
| block_k: int, | ||
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: | ||
| topk_weights, topk_ids = _baseline_topk_sigmoid( | ||
| router_logits, e_score_correction_bias, top_k | ||
| ) | ||
| a1q, a1q_scale = _baseline_quant(hidden_states, block_k) | ||
| return topk_weights, topk_ids, a1q, a1q_scale | ||
|
|
||
|
|
||
| def _bench_us( | ||
| fn: Callable[[], object], | ||
| mode: str, | ||
| quantiles: list[float], | ||
| ) -> float: | ||
| torch.cuda.synchronize() | ||
| if mode == "cudagraph": | ||
| ms, _, _ = triton.testing.do_bench_cudagraph(fn, quantiles=quantiles) | ||
| elif mode == "eager": | ||
| ms, _, _ = triton.testing.do_bench(fn, quantiles=quantiles) | ||
| else: | ||
| raise ValueError(f"Unsupported benchmark mode: {mode}") | ||
| return ms * 1000.0 | ||
|
|
||
|
|
||
| def _max_abs_err(actual: torch.Tensor, expected: torch.Tensor) -> float: | ||
| if actual.numel() == 0: | ||
| return 0.0 | ||
| return torch.max(torch.abs(actual - expected)).item() | ||
|
|
||
|
|
||
| def _check_correctness( | ||
| hidden_states: torch.Tensor, | ||
| router_logits: torch.Tensor, | ||
| e_score_correction_bias: torch.Tensor, | ||
| top_k: int, | ||
| block_k: int, | ||
| ) -> AccuracyResult: | ||
| ref_topk_weights, ref_topk_ids, ref_a1q, ref_a1q_scale = _baseline_topk_quant( | ||
| hidden_states, | ||
| router_logits, | ||
| e_score_correction_bias, | ||
| top_k, | ||
| block_k, | ||
| ) | ||
| topk_weights, topk_ids, a1q, a1q_scale = minimax_moe_topk_sigmoid_quant( | ||
| hidden_states, | ||
| router_logits, | ||
| e_score_correction_bias, | ||
| top_k, | ||
| block_k, | ||
| ) | ||
| topk_id_mismatch = torch.count_nonzero(topk_ids != ref_topk_ids).item() | ||
| topk_weight_max_abs_err = _max_abs_err(topk_weights, ref_topk_weights) | ||
| a1q_max_abs_err = _max_abs_err(a1q.float(), ref_a1q.float()) | ||
| a1q_scale_max_abs_err = _max_abs_err(a1q_scale, ref_a1q_scale) | ||
|
|
||
| torch.testing.assert_close(topk_ids, ref_topk_ids, atol=0, rtol=0) | ||
| torch.testing.assert_close(topk_weights, ref_topk_weights, atol=1e-6, rtol=1e-6) | ||
| torch.testing.assert_close(a1q.float(), ref_a1q.float(), atol=0, rtol=0) | ||
| torch.testing.assert_close(a1q_scale, ref_a1q_scale, atol=1e-6, rtol=1e-6) | ||
| return ( | ||
| topk_id_mismatch, | ||
| topk_weight_max_abs_err, | ||
| a1q_max_abs_err, | ||
| a1q_scale_max_abs_err, | ||
| ) | ||
|
|
||
|
|
||
| def _run_one( | ||
| *, | ||
| mode: str, | ||
| m: int, | ||
| hidden_size: int, | ||
| num_experts: int, | ||
| top_k: int, | ||
| block_k: int, | ||
| check_correctness: bool, | ||
| ) -> BenchResult: | ||
| if hidden_size % block_k != 0: | ||
| raise ValueError( | ||
| "The current baseline per_token_group_quant_fp8 path requires " | ||
| f"hidden_size ({hidden_size}) to be divisible by block_k ({block_k})." | ||
| ) | ||
|
|
||
| device = torch.device("cuda") | ||
| set_random_seed(0) | ||
| hidden_states = torch.randn( | ||
| (m, hidden_size), dtype=torch.bfloat16, device=device | ||
| ) | ||
| router_logits = torch.randn((m, num_experts), dtype=torch.float32, device=device) | ||
| e_score_correction_bias = torch.randn( | ||
| (num_experts,), dtype=torch.float32, device=device | ||
| ) | ||
|
|
||
| accuracy: AccuracyResult = (None, None, None, None) | ||
| if check_correctness: | ||
| accuracy = _check_correctness( | ||
| hidden_states, | ||
| router_logits, | ||
| e_score_correction_bias, | ||
| top_k, | ||
| block_k, | ||
| ) | ||
|
|
||
| quantiles = [0.5, 0.2, 0.8] | ||
| topk_us = _bench_us( | ||
| lambda: _baseline_topk_sigmoid( | ||
| router_logits, | ||
| e_score_correction_bias, | ||
| top_k, | ||
| ), | ||
| mode, | ||
| quantiles, | ||
| ) | ||
| quant_us = _bench_us( | ||
| lambda: _baseline_quant(hidden_states, block_k), | ||
| mode, | ||
| quantiles, | ||
| ) | ||
| baseline_us = _bench_us( | ||
| lambda: _baseline_topk_quant( | ||
| hidden_states, | ||
| router_logits, | ||
| e_score_correction_bias, | ||
| top_k, | ||
| block_k, | ||
| ), | ||
| mode, | ||
| quantiles, | ||
| ) | ||
| fused_us = _bench_us( | ||
| lambda: minimax_moe_topk_sigmoid_quant( | ||
| hidden_states, | ||
| router_logits, | ||
| e_score_correction_bias, | ||
| top_k, | ||
| block_k, | ||
| ), | ||
| mode, | ||
| quantiles, | ||
| ) | ||
|
|
||
| speedup = baseline_us / fused_us if fused_us else math.inf | ||
| return BenchResult( | ||
| mode=mode, | ||
| m=m, | ||
| hidden_size=hidden_size, | ||
| num_experts=num_experts, | ||
| top_k=top_k, | ||
| block_k=block_k, | ||
| topk_us=topk_us, | ||
| quant_us=quant_us, | ||
| baseline_us=baseline_us, | ||
| fused_us=fused_us, | ||
| speedup=speedup, | ||
| delta_us=baseline_us - fused_us, | ||
| topk_id_mismatch=accuracy[0], | ||
| topk_weight_max_abs_err=accuracy[1], | ||
| a1q_max_abs_err=accuracy[2], | ||
| a1q_scale_max_abs_err=accuracy[3], | ||
| ) | ||
|
|
||
|
|
||
| def _format_optional_int(value: int | None) -> str: | ||
| return "n/a" if value is None else str(value) | ||
|
|
||
|
|
||
| def _format_optional_float(value: float | None) -> str: | ||
| return "n/a" if value is None else f"{value:.2e}" | ||
|
|
||
|
|
||
| def _print_header() -> None: | ||
| print( | ||
| "mode".ljust(10), | ||
| "M".rjust(6), | ||
| "H".rjust(6), | ||
| "E".rjust(5), | ||
| "K".rjust(3), | ||
| "blk".rjust(4), | ||
| "topk_us".rjust(10), | ||
| "quant_us".rjust(10), | ||
| "base_us".rjust(10), | ||
| "fused_us".rjust(10), | ||
| "speedup".rjust(9), | ||
| "delta_us".rjust(10), | ||
| "id_mis".rjust(8), | ||
| "w_err".rjust(10), | ||
| "q_err".rjust(10), | ||
| "s_err".rjust(10), | ||
| ) | ||
| print("-" * 147) | ||
|
|
||
|
|
||
| def _print_result(result: BenchResult) -> None: | ||
| print( | ||
| result.mode.ljust(10), | ||
| f"{result.m:6d}", | ||
| f"{result.hidden_size:6d}", | ||
| f"{result.num_experts:5d}", | ||
| f"{result.top_k:3d}", | ||
| f"{result.block_k:4d}", | ||
| f"{result.topk_us:10.3f}", | ||
| f"{result.quant_us:10.3f}", | ||
| f"{result.baseline_us:10.3f}", | ||
| f"{result.fused_us:10.3f}", | ||
| f"{result.speedup:9.3f}", | ||
| f"{result.delta_us:10.3f}", | ||
| _format_optional_int(result.topk_id_mismatch).rjust(8), | ||
| _format_optional_float(result.topk_weight_max_abs_err).rjust(10), | ||
| _format_optional_float(result.a1q_max_abs_err).rjust(10), | ||
| _format_optional_float(result.a1q_scale_max_abs_err).rjust(10), | ||
| ) | ||
|
|
||
|
|
||
| def parse_args(): | ||
| parser = FlexibleArgumentParser( | ||
| description=( | ||
| "Benchmark MiniMax-M2 fused MoE top-k sigmoid routing + FP8 " | ||
| "per-token group quantization against the unfused baseline." | ||
| ) | ||
| ) | ||
| parser.add_argument( | ||
| "--m-values", | ||
| type=int, | ||
| nargs="+", | ||
| default=[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024], | ||
| help="Token counts to benchmark. Decode batch size maps directly to M.", | ||
| ) | ||
| parser.add_argument( | ||
| "--hidden-sizes", | ||
| type=int, | ||
| nargs="+", | ||
| default=[3072], | ||
| help="Hidden sizes to benchmark.", | ||
| ) | ||
| parser.add_argument( | ||
| "--num-experts", | ||
| type=int, | ||
| nargs="+", | ||
| default=[256], | ||
| help="Number of experts to benchmark.", | ||
| ) | ||
| parser.add_argument( | ||
| "--top-k", | ||
| type=int, | ||
| nargs="+", | ||
| default=[8], | ||
| help="Top-k values to benchmark.", | ||
| ) | ||
| parser.add_argument("--block-k", type=int, default=128) | ||
| parser.add_argument( | ||
| "--modes", | ||
| choices=["eager", "cudagraph"], | ||
| nargs="+", | ||
| default=["eager", "cudagraph"], | ||
| help="Benchmark eager launches, CUDA graph replay, or both.", | ||
| ) | ||
| parser.add_argument( | ||
| "--check-correctness", | ||
| action=argparse.BooleanOptionalAction, | ||
| default=True, | ||
| help="Compare fused outputs with the baseline before timing each shape.", | ||
| ) | ||
| return parser.parse_args() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| if not current_platform.is_cuda(): | ||
| raise RuntimeError("CUDA device is required to run this benchmark.") | ||
|
|
||
| args = parse_args() | ||
| _print_header() | ||
|
|
||
| configs = itertools.product( | ||
| args.modes, | ||
| args.m_values, | ||
| args.hidden_sizes, | ||
| args.num_experts, | ||
| args.top_k, | ||
| ) | ||
| for mode, m, hidden_size, num_experts, top_k in configs: | ||
| result = _run_one( | ||
| mode=mode, | ||
| m=m, | ||
| hidden_size=hidden_size, | ||
| num_experts=num_experts, | ||
| top_k=top_k, | ||
| block_k=args.block_k, | ||
| check_correctness=args.check_correctness, | ||
| ) | ||
| _print_result(result) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use_ue8m0=FalseCould you also test withuse_ue8m0=True?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion. MiniMax-M2.7 uses the non-UE8M0 FP8 MoE path, and the fused kernel in this PR intentionally matches
per_token_group_quant_fp8(..., use_ue8m0=False). I also benchmarked standaloneper_token_group_quant_fp8withuse_ue8m0=TruevsFalseon the MiniMax-M2 shape(M, 3072), block_k=128. The latency is very close.Command and results
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this PR changes the behavior of e8m0 = True path? Please take a check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this can change the E8M0 path since the fused kernel matches
use_ue8m0=False. And I thinkFalseis more appropriate for MiniMax-M2.7, but I can add a guard to keep E8M0 unchanged if preferred.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As E8M0 is widely used currently, I am not sure if we really want this complicated optimization for non-e8m0 path. Also CC @mgoin