Skip to content

feat: W4A8 ARK XPU MoE kernel (int4 weight / int8 compute) with prefill + decode - #2143

Open
a32543254 with Copilot wants to merge 124 commits into
mainfrom
copilot/copilotoptimize-int4-moe-performance
Open

feat: W4A8 ARK XPU MoE kernel (int4 weight / int8 compute) with prefill + decode#2143
a32543254 with Copilot wants to merge 124 commits into
mainfrom
copilot/copilotoptimize-int4-moe-performance

Conversation

Copilot AI commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a W4A8 MoE kernel to the ARK XPU backend covering both prefill and decode: int4 symmetric weights, int8 compute dtype, activations dynamically quantized per token to int8. Follows the W4A8 weight-only GEMM in zhenzhong/woqgemm_s8_update, and uses ARK's AUTO_S8 trick — re-scaling int4 group=32 weights into int8 group=-1 so the K loop needs a single full-width int32 accumulation instead of per-group folding.

Built on top of copilot/optimize-int4-moe-performance.

Numerics

AUTO_S8 re-scale, ported from packscale/unpackq in xpu_wrapper.hpp:

sxt[e][n][j] = max_{g in block j} |s[e][n][g]| * 8 / 127
w8           = round(w4 * s / sxt)

Default block = K (group=-1) → blks == 1. Since _pack_int4_sym divides by 7.0, max|w8| = 7 * 127/8 ≈ 111 ≤ 127, so the conversion never clips. Epilogue is out = acc_s32 * scale_b[col] * scale_a[row].

Kernelwrapper/include/sycl_tla_moe_w4a8.hpp (new)

  • One-shot moe_w4a8_prepack: [E,N,K/2] int4 → [E,N,K] int8 + [E,N,blks] fp32 scales. Costs E*N*K bytes (2× the packed int4).
  • Per-token activation quant producing x_s8 and scale_a.
  • Prefill: grouped DPAS GEMM on XE_DPAS_TT<8, int32_t, int8_t, int8_t>, tile ladder mirroring the reference (m<16 → 8x128, m<128 → 64x128, m<=1024 → 128x128, else 256x128). num_tokens_per_expert is a device tensor, so it reuses the existing persistent work-stealing scheduler.
  • Decode: GEMV, SG_SIZE=16 / N_TILE=16, one output column per lane.
  • ARK_MOE_W4A8_AUTO_S8 overrides the rescale block size; invalid values fall back to K. Shape gate: N % 16 == 0, K % 64 == 0, group_size % 8 == 0, K % group_size == 0.

Plumbing

  • sycl_tla_common.hpp: 4 public declarations.
  • ark.cpp: include, 2 wrappers, 4 m.def registrations.
  • auto_round_kernel/__init__.py: Python API plus a prepack cache. The cache key includes device type/index and the entry pins the source tensors, so a freed-and-reallocated weight buffer can't alias another layer's int8 weights.
from auto_round_extension.ark import auto_round_kernel as ark

out = ark.moe_w4a8(x, weights, scales, num_tokens_per_expert, group_size=32, phase="prefill")

# or manage the prepack lifetime explicitly
w_s8, w_scale = ark.moe_w4a8_prepack(weights, scales, group_size=32)
out = ark.moe_gemm_w4a8(x, w_s8, w_scale, num_tokens_per_expert, phase="decode")

Benchmarktest/test_moe_w4a8_perf.py (new)

Standalone perf + accuracy harness, runnable under pytest or directly. Qwen3-MoE shapes (E=128, hidden=2048, inter=768, top_k=8, group_size=32). Reports SNR/cosine/max-rel-err against a torch bf16 baseline and latency/TFLOPS/speedup against the W4A16 kernel, for prefill across batch×seq and decode across batch.

pytest auto_round_extension/ark/test/test_moe_w4a8_perf.py -v
python auto_round_extension/ark/test/test_moe_w4a8_perf.py --warmup 10 --iters 50

Type of Change

New feature (Performance)

Checklist Before Submitting

  • My code has been tested locally.
  • Documentation has been updated as needed.
  • New or updated tests are included where applicable.
  • The CUDA CI has passed. You can trigger it by commenting /azp run Unit-Test-CUDA-AutoRound.

Docs: test/README_MOE_W4A8.md + README_MOE_W4A8_CN.md.

W4A8 perf [prefill] (models=qwen3+minimax, group_size=32, act=bfloat16, rescale_group_size=-1) -- ark.moe_gemm_w4a8 vs W4A16 vs torch
shape             E      N      K  tokens  rows/E   torch(ms)   w4a16(ms)    w4a8(ms)    TFLOPS    W GB/s  DRAM GB/s   BW@100T   vs torch   vs w4a16  prepack(ms)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
qwen3 up        128   1536   2048   65536   512.0          --       3.760       2.856    144.36     141.0      399.4       277         --      1.32x         2.50
qwen3 down      128   2048    768   65536   512.0          --       2.561       2.098     98.25      95.9      319.8       326         --      1.22x         1.14
minimax up      192   1536   3072   65536   341.3          --       6.649       4.565    135.48     198.5      419.0       309         --      1.46x         4.75
minimax down    192   3072   1536   65536   341.3          --       6.529       4.263    145.07     212.5      401.4       277         --      1.53x         4.85
image

a32543254 and others added 30 commits July 31, 2026 11:16
Signed-off-by: Dong, Bo1 <bo1.dong@intel.com>
Signed-off-by: Dong, Bo1 <bo1.dong@intel.com>
Merge ec61621 accidentally widened the dpas_w8a16_policy_m_32 bucket in the
fp8 per-tensor (per-expert) prefill dispatch from A_avg_M <= 32 to <= 512,
routing large-M prefill through the small 32x64 tile instead of the large-M
128x128 default tile and regressing performance. Restore the <= 32 threshold.
Merge ec61621 also widened the dpas_w8a16_policy_m_32 bucket in the fp8
per-group prefill dispatch from A_avg_M <= 32 to <= 512, regressing large-M
group-size prefill for the same reason as the per-tensor path. Restore the
<= 32 threshold so large-M prefill uses the 128x128 default tile.
Revert commit f887763, restoring the fp8 per-group prefill dispatch threshold
to A_avg_M <= 512. The per-expert (per-tensor) fix from 93cde8c is retained.
Migrate the auto-dispatch logic from branch
copilot/update-phase-auto-dispatch-logic (commit 9605fe4): phase="auto" now
dispatches to decode when activations.shape[0] <= threshold (total tokens)
instead of inspecting num_tokens_per_expert.max(), avoiding a host-device
sync. Adds ARK_MOE_AUTO_DECODE_MAX_TOKENS env override (default 256) and
updates test_moe_unified.py accordingly.
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
The shared S4 DPAS grouped-GEMM (prefill path) already beats the scalar
GEMV decode kernel by ~2x at 256 tokens (bs32) and only loses at the
single-stream bs1 (8-token) extreme. Routing 256-token batches to decode
was leaving ~2x on the table, so lower the auto-dispatch default and
update coupled unified-dispatch tests and perf-test notes.

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…reshold tuning

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
… GEMV

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…mangled names

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…atch regression

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Copilot AI and others added 15 commits September 7, 2026 14:45
The dynamic quantization is a per-row absmax, so it has no expert
dependence. The up/gate projection is handed batch * top_k rows that are
top_k copies of batch distinct tokens, so the in-call pass does the work
8x over at the shipped top_k. Hoisting it above the permute reaches the
already-supported int8 contract with no kernel change and no int8
producer upstream, and halves the permute as a side effect: 1409 MB ->
923 MB end to end on qwen3 up.

Adds run_dedup_quant, which times both paths with the caller's permute
included -- the only comparison that distinguishes a real saving from a
relocated one -- plus the pytest entry point and a --dedup-quant flag.

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…ntizer

The 0.94x regression was the quantizer, not the deduplication. The
deduplicated path called _quantize_rows -- the eager-torch reference,
which upcasts to fp32 and walks the tensor once per operator -- while the
baseline used the fused SYCL quantizer. That is ~15x worse per row,
enough to eat an 8x reduction in rows.

Two defects made this invisible. The gemm column was derived as
total - permute, so it silently absorbed the quantization cost under a
label that said "gemm". And there was no measurement of what the fused
quantizer costs, so there was nothing to compare against.

Times each stage separately now, and measures the fused quantizer by
differencing the same GEMM with 16-bit and int8 input on the same shape
and weights -- the only differing work is the in-kernel quantization of
exactly those rows. Done at both T and batch rows, which cross-checks
linearity in rows; the test asserts the ratio is within 2x of top_k so a
noise difference cannot become a headline number.

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
B70 confirms the diagnosis and the fix. With the fused quantizer the
deduplicated path is 3.862 -> 2.637 ms, 1.46x end to end including the
caller's permute, against a traffic model that predicted 1.53x. With the
eager-torch quantizer it is 0.95x, which is the earlier regression
reproduced now that the stages are attributable.

Replaces the projected numbers with measured ones, records the fused
quantizer at 0.818 ms / 65536 rows and 0.136 ms / 8192 rows (6.0x for 8x
the rows, sublinear in the direction fixed launch cost predicts), and
corrects the claim in "What is left" that contract 1 needs an int8
producer upstream -- it does not, which is the whole point of the dedup.

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…ter it

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…hat explains it

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…h READMEs

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
… 1.2-1.6x slower

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…rectly

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…rencing

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
…k to differencing

Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
Co-authored-by: a32543254 <53296245+a32543254@users.noreply.github.com>
@a32543254

a32543254 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor
W4A8 perf [prefill] (models=qwen3+minimax, group_size=32, act=bfloat16, rescale_group_size=-1) -- ark.moe_gemm_w4a8 vs W4A16 vs torch
shape             E      N      K  tokens  rows/E   torch(ms)   w4a16(ms)    w4a8(ms)    TFLOPS    W GB/s  DRAM GB/s   BW@100T   vs torch   vs w4a16  prepack(ms)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
qwen3 up        128   1536   2048   65536   512.0          --       3.760       2.856    144.36     141.0      399.4       277         --      1.32x         2.50
qwen3 down      128   2048    768   65536   512.0          --       2.561       2.098     98.25      95.9      319.8       326         --      1.22x         1.14
minimax up      192   1536   3072   65536   341.3          --       6.649       4.565    135.48     198.5      419.0       309         --      1.46x         4.75
minimax down    192   3072   1536   65536   341.3          --       6.529       4.263    145.07     212.5      401.4       277         --      1.53x         4.85

@AutoRoundBot

This comment has been minimized.

@AutoRoundBot

Copy link
Copy Markdown
Collaborator

/azp run Unit-Test-CUDA-AutoRound

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines could not run because the pipeline triggers exclude this branch/path.

@a32543254

Copy link
Copy Markdown
Contributor
image

@AutoRoundBot

Copy link
Copy Markdown
Collaborator

/azp run Unit-Test-CUDA-AutoRound

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines could not run because the pipeline triggers exclude this branch/path.

@AutoRoundBot

Copy link
Copy Markdown
Collaborator

/azp run Unit-Test-CUDA-AutoRound

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines could not run because the pipeline triggers exclude this branch/path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants