Open
feat: W4A8 ARK XPU MoE kernel (int4 weight / int8 compute) with prefill + decode#2143
Conversation
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.
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>
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>
Contributor
|
This comment has been minimized.
This comment has been minimized.
Collaborator
|
/azp run Unit-Test-CUDA-AutoRound |
|
Azure Pipelines could not run because the pipeline triggers exclude this branch/path. |
Contributor
Collaborator
|
/azp run Unit-Test-CUDA-AutoRound |
|
Azure Pipelines could not run because the pipeline triggers exclude this branch/path. |
Collaborator
|
/azp run Unit-Test-CUDA-AutoRound |
|
Azure Pipelines could not run because the pipeline triggers exclude this branch/path. |
Zhenzhong1
approved these changes
Sep 11, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.

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'sAUTO_S8trick — 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/unpackqinxpu_wrapper.hpp:Default block = K (
group=-1) →blks == 1. Since_pack_int4_symdivides by 7.0,max|w8| = 7 * 127/8 ≈ 111 ≤ 127, so the conversion never clips. Epilogue isout = acc_s32 * scale_b[col] * scale_a[row].Kernel —
wrapper/include/sycl_tla_moe_w4a8.hpp(new)moe_w4a8_prepack:[E,N,K/2]int4 →[E,N,K]int8 +[E,N,blks]fp32 scales. CostsE*N*Kbytes (2× the packed int4).x_s8andscale_a.XE_DPAS_TT<8, int32_t, int8_t, int8_t>, tile ladder mirroring the reference (m<16 → 8x128,m<128 → 64x128,m<=1024 → 128x128, else256x128).num_tokens_per_expertis a device tensor, so it reuses the existing persistent work-stealing scheduler.SG_SIZE=16/N_TILE=16, one output column per lane.ARK_MOE_W4A8_AUTO_S8overrides 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, 4m.defregistrations.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.Benchmark —
test/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.Type of Change
New feature (Performance)
Checklist Before Submitting
/azp run Unit-Test-CUDA-AutoRound.Docs:
test/README_MOE_W4A8.md+README_MOE_W4A8_CN.md.