moe: enable native Xe2 path for MXFP8 and block-FP8 - #488
Conversation
Refreshed P-B A/B artifact index (HAL B70, 2026-07-25)Combined path selection + native MXFP8/block-FP8 kernels vs
Headline (refreshed): micro MXFP8 +1547% / block-FP8 +1857%; E2E output tok/s 25.1 → 607.9 (+2322%), KEEP yes. Prior open-PR pack (26.19 → 611, +2233%) retained in the description for continuity. |
|
Please provide below data:
|
| @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16], | ||
| ids=format_tc) | ||
| @pytest.mark.parametrize("has_bias", [False, True]) | ||
| def test_xe_grouped_gemm_mxfp8(m, n, k, e, topk, dtype, has_bias): |
There was a problem hiding this comment.
Please also add UT for block fp8.
There was a problem hiding this comment.
Added test_xe_grouped_gemm_block_fp8 here (FP8 weights + float32 2D scales [E, K/128, N/128] vs a dequant+matmul gold) plus test_fused_moe.py::test_fused_moe_fp8block. Both pass on Arc Pro B70 — see the follow-up comment with the full 28-passed run and data.
| w2.data = w2_tmp | ||
| w13.xpu_fused_moe = True | ||
|
|
||
| # Block-FP8: one-time weight dequant into float32, then cast to the |
There was a problem hiding this comment.
Weights data type should not be changed.
Casting to fp32 will spend more memory.
You should realize really block fp8 kernels instead of casting in memory.
There was a problem hiding this comment.
Fixed in 8a4dee4: the fp32 weight promotion is removed. Block-FP8 weights stay float8_e4m3fn in memory and the float32 2D block scales are consumed inside the Xe2 grouped GEMM (W8A16, group_size=128), so no weight-sized fp32 buffer is allocated. Accuracy vs the ref path is cosine ≈ 1.0 (see follow-up comment).
Refreshed P-B A/B artifact index (HAL B70, 2026-07-25)Combined path selection (fork #9) + native MXFP8/block-FP8 kernels vs
SHA: measured on Headline (refreshed): micro MXFP8 +1547% / block-FP8 +1857%; E2E output tok/s 25.1 → 607.9 (+2322%), KEEP yes. Prior open-PR pack (26.19 → 611, +2233%) retained in the description for continuity. |
|
Hi @krisclarkdev, I wasn't aware of this PR when I opened #490. I noticed your block FP8 approach dequantizes weights to bf16/fp16 at init time and then runs the standard W16A16 grouped GEMM. In #490 I took a different approach that keeps weights in FP8 at runtime and uses a partial-accumulator kernel: Memory: weights stay as 1-byte FP8 — no doubling of weight memory from init-time dequant Maybe @mayuyuace can help to decide |
|
@krisclarkdev, I see you just pushed 8a4dee4 which keeps FP8 weights in memory and applies the float32 block scale in-kernel — nice, that eliminates the init-time dequant. A couple of observations on the kernel strategy: Your implementation routes block FP8 through the existing pre-multiply-B path in xe_gemm_4bits — meaning the FP8 weight gets converted to float, multiplied by the block scale, then cast to bf16 before entering DPAS. This effectively makes it W16A16 compute with FP8 storage. In #490 I use a partial-accumulator approach instead: The FP8 weight goes directly into DPAS as-is (bf16 × FP8 → fp32, native W8A16 hardware path) Worth benchmarking both to see which wins in practice — the partial accumulator adds a branch + scale multiply every group_size/tile_k tiles, but the 2× DPAS throughput should more than compensate. |
Review follow-up: no fp32 weight cast, block-FP8 UT, and requested dataAll three review points addressed. Measured on Intel Arc Pro B70, oneAPI 2026.0 / torch 2.13.0+xpu, PR tip 1. Weights stay FP8 (no fp32 cast in memory)The fp32 weight promotion in
2. Block-FP8 UTAdded 3a. Grouped-GEMM performance (TFLOPS / bandwidth)Median of 50 iters, 8 experts (tokens split round-robin),
Harness: 3b. Accuracy: ref path vs kernels pathFull GSM8K serve needs the vLLM-side MXFP8 MoE serve wiring (backend selection + block-scale transpose), which lives in vLLM and is out of scope for this kernels PR; that path is not available in the current test image. As the equivalent in-repo accuracy check, identical activations + weights are fed through the kernels path (
Cosine ≈ 1.0 across shapes; residual per-element error sits at the FP8 quantization noise floor, i.e. the kernels path matches the ref path. Harness: If you'd like a true end-to-end GSM8K number rather than the layer-level comparison, I can run it once the vLLM MXFP8 MoE serve wiring is in place (happy to point at the wiring branch), but that is a separate vLLM change from this kernels PR. |
|
Hi @kfojcik-intel — thanks, no worries on the overlap. You're right that One correction, since it affects what we'd measure: I don't think #490 hits native FP8 DPAS. The MMA atom is still Minor: your block-scale prefetch The PRs are complementary: yours routes MXFP8 to the ref path so #490 is block-FP8 only, while this one does both, and it carries the TFLOPS/bandwidth sweep and accuracy-vs-ref data mayuyuace asked for. Your group_size handling (32/64/128/256 + square check) is more general than my hard-coded 128, so I'd take yours there. Happy to rebase/combine however @mayuyuace prefers — and if you push the #490 kernel I'll run it through |
|
@krisclarkdev MMA atom: Correct, reorder(tBrB, tCrB) already converts FP8→bf16 before DPAS -- so the difference is ALU overhead only, not 2× DPAS throughput. I stand corrected. Prefetch: Fixed - removed the block FP8 scale prefetch. I still need to think this over but I think that SG_N / group_size yields at most 2 with current policies, which I believe is below make_block_2d_prefetch's minimum shape requirement. The scale tensor is only a few KB per expert so L2 handles it fine. All 32 test cases pass on BMG for me now. The updated kernel is at version #490 -- you can download it. I'll keep you updated with my performance comparison soon. |
|
Required companion of vLLM FA-in-graph / XPU graphs PR: vllm#50038. Please treat kernels #485–#489 as part of the graphs stack alongside vllm #48677 (torch/image) and #49813 (softcap/MXFP8). |
|
@kfojcik-intel |
|
@krisclarkdev |
8e86676 to
d7cb8ae
Compare
|
Fixed pre-commit in |
|
Correction: tip is pre-commit run --all-files --hook-stage manualAll hooks passed. CI self-hosted runs show |
|
@jikunshang |
Run MXFP8 (FP8 + E8M0 B-scales) and block-FP8 MoE through grouped GEMM by default instead of the Python expert-loop ref. Block-FP8 weights are dequantized once at init; MXFP8 stays W8A16 with act dequant in Python. Signed-off-by: Kristopher Clark <krisclarkdev@krisbox.org> Co-authored-by: Cursor <cursoragent@cursor.com>
Replace init-time fp32 weight promotion with Xe2 grouped-GEMM block-FP8 dispatch (float32 scales [E,K/128,N/128]). Add grouped-GEMM UT. Signed-off-by: Kristopher Clark <krisclarkdev@krisbox.org> Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Kristopher Clark <krisclarkdev@krisbox.org> Co-authored-by: Cursor <cursoragent@cursor.com>
544598b to
cc5644e
Compare
jikunshang
left a comment
There was a problem hiding this comment.
LGTM. thanks for contributing!
Purpose
Enable the native Xe2 grouped-GEMM MoE path for MXFP8 (FP8 weights + E8M0 block scales, group=32) and block-FP8, instead of defaulting to the slow Python
ref_fused_moeexpert loop.Scope / pairing (do not split)
This PR ships both pieces together (fork #9 path flip + kernels):
ref_fused_moe; default native with escape hatches below.A path-flip-only change without the kernel/dispatch work is incorrect and is not a supported split. All A/B numbers below are for this combined change vs
VLLM_XPU_FUSED_MOE_USE_REF=1.VLLM_XPU_FUSED_MOE_NATIVE_MXFP8=0or force ref withVLLM_XPU_FUSED_MOE_USE_REF=1.VLLM_XPU_FUSED_MOE_NATIVE_BLOCK_FP8=0.uint8orfloat8_e8m0fnu.Serve note: loading HF MXFP8 MoE (
weight_block_size=[1,32]) also needs vLLM to select the XPU MXFP8 MoE backend and transpose block scales with weights to[E,K/g,N]after the XPU[E,N,K]→[E,K,N]weight transpose. That wiring lives in vLLM (not this PR).Test Plan
Micro + E2E A/B (HAL B70):
USE_REFfor MXFP8 and block-FP8 (ab_moe_native_micro.py/pb_e2e_micro.txt)Test Result
Prior E2E pack (PR open)
Hardware: Intel Arc Pro B70 · Commit:
97c29caModel:
Qwen1.5-MoE-A2.7B-Chat-MXFP8-RTN(~15 GiB, FP8 +[1,32]/ue8m0)Recipe: random, 24 prompts, 256→128,
max_model_len=2048,gpu_util=0.85,--enforce-eagerFailed requests: 0 / 0 (24/24 both sides)
VLLM_XPU_FUSED_MOE_USE_REF=1VLLM_XPU_FUSED_MOE_NATIVE_MXFP8=1Δ tok/s: +2233% vs REF → E2E KEEP (≥10% bar).
Refreshed full scrape (P-B, 2026-07-25)
Source of truth:
ab_artifacts/hal_results/PB_E2E_FULL_VERDICT.{md,json}.Hardware: Intel Arc Pro B70 · Measured commit:
97c29ca· PR tip:97c29ca(aligned; no remount/remeasure needed)Model:
Qwen1.5-MoE-A2.7B-Chat-MXFP8-RTNRecipe: same as prior pack (random, 24 prompts, 256→128,
max_model_len=2048,gpu_util=0.85,--enforce-eager)Compare: combined path selection + kernels vs
USE_REF=1Failed requests: 0 / 0
Micro (op median ms)
E2E serve (median of 3 timed runs after warmup)
Per-run output tok/s: REF
[25.23, 25.10, 24.35]· Native[607.90, 614.93, 600.60]E2E KEEP (≥10% tok/s): yes.
The refreshed pack is within noise of the prior +2233% pack (same order of magnitude; both KEEP). Prior numbers are retained above for continuity.
Claim: combined path flip (fork #9) + Xe2 kernels vs the previous Python expert-loop REF on the same checkpoint. Not a claim vs W8A16, not vs CUDA/Marlin MXFP8 backends. Device W8A8 is out of scope for this PR.
Serve logs confirmed
XPUMxFp8LinearKernel+Using 'XPU' MxFp8 MoE backendon both sides.(Optional) Documentation Update
None in-tree. Escape hatches:
VLLM_XPU_FUSED_MOE_NATIVE_MXFP8,VLLM_XPU_FUSED_MOE_NATIVE_BLOCK_FP8,VLLM_XPU_FUSED_MOE_USE_REF.