[BugFix][MoE] Make xe2 grouped-GEMM tile scheduler cudagraph-capture-safe#457
Open
AleksVidmantas wants to merge 2 commits into
Open
[BugFix][MoE] Make xe2 grouped-GEMM tile scheduler cudagraph-capture-safe#457AleksVidmantas wants to merge 2 commits into
AleksVidmantas wants to merge 2 commits into
Conversation
…safe The xe2 grouped GEMM (MoE::MoEGEMM) uses a persistent work-steal scheduler with a single global atomic tile counter that it resets from inside the kernel with no device-wide barrier. In eager submits the reset is ordered by launch timing, but under SYCL-graph (cudagraph) capture/replay the counter carries the previous replay's final value and the un-ordered reset vs. atomic tile-steals produce duplicate/inconsistent tile indices. That yields out-of-bounds tile coordinates and UR_RESULT_ERROR_DEVICE_LOST at batch>1, which blocks capturing decode graphs for concurrency > 1. Replace the atomic work-steal with a deterministic grid-stride partition: each workgroup processes tiles g, g + group_range, g + 2*group_range, ... There is no shared mutable global state and no in-kernel reset, so it is safe under graph capture/replay at any batch size. Tile coverage is identical to the previous scheduler (every tile computed exactly once), so single-stream output and performance are unchanged. Validated on Intel Arc Pro B70 (Xe2/BMG-G31): batch-1 output and ~89 t/s decode unchanged; PIECEWISE capture of batch sizes [1,2,4] now completes with zero DEVICE_LOST and coherent output; aggregate decode throughput at concurrency 4 rises from ~93 t/s (eager) to ~283 t/s (captured). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Aleks Vidmantas <aleks.vidmantas@gmail.com>
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.
Purpose
On Intel Arc (Xe2/BMG) the xe2 grouped GEMM (
MoE::MoEGEMM,csrc/xpu/grouped_gemm/xe_2/grouped_gemm_xe2.hpp) cannot be captured into a SYCL graph(cudagraph) at batch > 1: replaying a captured decode graph with more than one sequence raises
UR_RESULT_ERROR_DEVICE_LOST. Servers using these kernels must therefore pincudagraph_capture_sizesto[1]and fall back to eager execution for any concurrent batch,losing the graph-capture speedup exactly where batching helps most.
Root cause. The kernel uses a persistent work-steal scheduler with a single global atomic
tile counter that it resets from inside the kernel (the
atomic_buffer[0]store), guarded onlyby "group 0, lane 0" with no device-wide barrier. Workgroups then steal the next tile via
atomicAdd(atomic_buffer, 1)(workgroup-local fence only).happens to be correct (this is why the eager batched path works).
carries the previous replay's final value, and the un-ordered reset-vs-steal ordering yields
duplicated / inconsistent tile indices. A workgroup then computes an
m_coordoutside anexpert's real row range → out-of-bounds global load/store → GPU page fault → DEVICE_LOST.
within the initial grid, so the steal loop barely runs and capture at
[1]is safe; atbatch > 1 the steal loop is heavily exercised and replay corrupts. (Even at batch 1 the
unsynchronized reset-vs-steal is a latent race rather than a guaranteed-safe design; the
deterministic scheduler removes the hazard at every batch size.)
Fix. Replace the atomic work-steal with a deterministic grid-stride partition: workgroup
gprocesses tilesg, g + group_range, g + 2*group_range, .... There is no shared mutable globalstate and no in-kernel reset, so the kernel is safe to capture and replay at any batch size.
Coverage is identical to the previous scheduler (every tile computed exactly once), so
single-stream output and performance are unchanged. The launch grid was already occupancy-based and
batch-independent, so no launch change is needed.
atomic_buffer/ the SLM scratch are left in thesignature (unused,
(void)-cast) to keep the change minimal; removing them from the op/launchersignature is a mechanical follow-up.
Test Plan
Build (Intel Arc Pro B70, Xe2/BMG-G31, oneAPI 2025.3):
PIECEWISE capture, comparing the pinned-
[1]baseline againstcudagraph_capture_sizes=[1,2,4],max_num_seqs=4. (E2E uses a model-specific serving build; the unit test above needs only thestandard build shown here.)
Test Result
Unit test (fixed kernel):
144 passed, 16 skipped(the 16 skips are the pre-existingtest_grouped_gemmnon-xe2 case,skipifreason "Need Fix API" — unrelated to this change). Thepath this PR touches,
test_xe_grouped_gemm_int4, is 16/16 passed acrossM ∈ {1, 4, 16, 8192} × {bf16, fp16} × {bias, no-bias}, within rtol 1e-2. Sibling xe2 paths
(
_fp8,_block_fp8_w8a8,_mxfp4) also pass, confirming the scheduler rewrite did not disturbthe shared compute paths.
End-to-end (Qwen3.6-35B-A3B W4A16,
max-model-len 131072,max-num-seqs 4,cudagraph_capture_sizes=[1,2,4], non-spec):No regression (batch 1): capture
[1]boots clean, output coherent; warm single-stream decodeis unchanged vs. the pre-fix kernel (~89 t/s, short context). The rewrite has identical tile
coverage, so batch-1 output and rate are untouched.
Capture unblocked (the fix): capture of all three batch sizes completes with zero
DEVICE_LOST; 4 concurrent prompts return coherent, correct answers (previously: DEVICE_LOST at
batch > 1).
Throughput scaling (captured batch-4 vs single-stream), across context. Aggregate decode
measured from the vLLM
generation_tokens_totalcounter delta over the window where all 4 streamsare co-resident and decoding (median of 3 reps, ± s.d.):
Combined ctx = total tokens across the 4 co-resident streams (per-stream ≈ ÷4); single-stream
(batch-1) is 77–86 t/s across the same per-stream context range. The ~3× captured-batch-4 speedup
holds across the full 8k→120k range — capture removes the per-step launch overhead that
otherwise dominates batched decode — lifting memory-bandwidth utilization from ~50% (single-stream)
toward ~61% at batch-4.
The change is a scheduling rewrite with identical tile coverage; it does not alter numerics.
Batch-1 greedy output is unchanged (bit-for-bit tile coverage), and batch-4 concurrent greedy
prompts produce correct answers. No accuracy/eval regression expected or observed.
(Optional) Documentation Update
None — kernel-internal scheduling fix; no user-facing API, configuration, or documentation change.
Risk / limitations
load-balancing across experts with very uneven token counts. For highly skewed expert loads a
few % of tail imbalance is possible; in the decode regime measured here it was not material, and
the capture win dominates. A hybrid (static assignment + a small bounded steal reset out-of-kernel
with a memset graph node) could recover it in a follow-up if needed.
atomic_bufferand the SLM scratch are now unused in this kernel; removing them from theop/launcher signature is a mechanical cleanup left out to keep the diff focused.
Related
Independent of open PR #436 (block-FP8 W8A8 grouped-GEMM), which edits this same file: this fix was
developed alongside #436 but does not depend on it and applies cleanly with or without it — only a
textual (not functional) conflict is possible if #436 lands first, so happy to rebase in whichever
order suits. Prior art: #320 (same PIECEWISE capture-padding family) and #276 (torch.compile
umbrella).