Skip to content

Add Intel XPU backend: Qwen decode on Arc Pro B60 via SYCL - #667

Closed
the-talos wants to merge 2 commits into
Niko1221:mainfrom
the-talos:intel-xpu-backend
Closed

the-talos wants to merge 2 commits into
Niko1221:mainfrom
the-talos:intel-xpu-backend

Conversation

@the-talos

Copy link
Copy Markdown

What this adds

A third backend next to CUDA and HIP, enabled with STRATA_ENABLE_XPU=ON (icpx/icx, oneAPI 2025.3). The CUDA kernels are reused unchanged: tools/xpu/rewrite_cuda.py rewrites launch syntax and declarations to SYCL at build time, and include/strata/xpu_compat/ implements the CUDA runtime API the engine leans on (graphs, streams, events, mapped memory, cublas via oneMKL) on SYCL/Level Zero. docs/INTEL_XPU.md documents the design and the build.

Numbers

Qwen3.8-Flash-Next GSQ-RCO Q2_0 on one Intel Arc Pro B60 (23.3 GB), expert cache auto (14,570 slots, 18.8 GiB), context 1024, --spec 2 --kv int8:

  • prefill 127 tokens at 73.9 tok/s, time to first token 2.1 s
  • greedy decode through the speculative verify window at 9.4 tok/s (64 tokens), exit 0
  • in-run gates pass: cache slot verified bit for bit, every layer doorbell rings

Porting notes worth reviewing

  • Graph execs deep-copy the captured op list; the engine destroys the capture right after instantiate, as CUDA allows.
  • cudaStreamQuery is an honest non-blocking idle check on the in-order queue; the verify loop polls it while spinning on doorbells.
  • Async copies from unpinned host pages (the mmap-ed GGUF shards) stage through a malloc_host bounce with an in-order host_task fill; Level Zero faults on raw pages and strands every later op on the stream.
  • The doorbell spin kernels poll host-mapped flags. Pure volatile read loops over host USM do not see CPU stores on this stack, so __nanosleep (called by every strata_spin_pause() site each iteration) issues a sycl::atomic_fence(seq_cst, system). Without it the verify window times out at the first flag wait, every run.
  • Performance is scalar so far: __dp4a and shuffles are correct subgroup implementations, XMX paths are future work.

I have run the greedy smoke test and the measurement above on the B60 container only; the CUDA and HIP builds are untouched (the new code compiles in only under STRATA_ENABLE_XPU).

Third backend alongside CUDA/HIP, enabled with STRATA_ENABLE_XPU=ON. The CUDA
kernels are reused as-is: tools/xpu/rewrite_cuda.py rewrites launch syntax and
declarations, include/strata/xpu_compat/ provides the CUDA runtime, graphs,
events, fp16 and cublas APIs on SYCL/Level Zero. Graphs replay as recorded op
lists; the exec is an independent copy of the capture, as CUDA semantics
require. Memory accounting, stream queries and memset are chunked/honest so the
expert-cache auto sizing works. Async copies from unpinned pages stage through a
malloc_host bounce. The doorbell spin kernels need a system-scope fence per
poll iteration (host USM reads do not snoop CPU stores on this stack), which
__nanosleep now issues. Qwen3.8-Flash-Next Q2_0 on one B60: prefill 73.9 tok/s,
decode 9.4 tok/s (spec window, kv int8, cache auto).

Co-authored-by: the-talos <talos.agent.22@gmail.com>
Graph instantiate now records the captured op list into an
ext_oneapi_graph on the capture stream's queue, and cudaGraphLaunch
submits it as one call (~1.2 us/op amortised vs ~31 us/op for the eager
per-op loop, measured with a 2000-kernel microbench). The eager loop
stays as the fallback: STRATA_XPU_EAGER_GRAPHS=1, or any recording
failure. STRATA_XPU_PROFILE brackets each op with barrier events and
prints per-kernel GPU time at the next launch (per-op waits would
deadlock on the doorbell spin kernels).

Qwen3.8-Flash-Next Q2_0, one B60, cache auto: prefill 73.9 -> 77.8
tok/s, decode 9.4 -> 10.2 tok/s. The profile shows the next levers:
~53% of GPU time is the scalar expert GEMV family (XMX rewrite), ~27%
is doorbell parked time while the CPU serves between rings.

Co-authored-by: the-talos <talos.agent.22@gmail.com>
@Niko1221

Niko1221 commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Thank you, this is a clean design: one source tree for all three backends, with a small compat layer. It is the direction we would like long-term. For 0.1.39 we merged #423's SYCL port as the experimental Intel engine instead, because it is much further along. It is measured at 70+ tok/s on a B70, has been run on B50, B580 and 2x B70 by other users, and changes no shared file. Two Intel backends would be twice the upkeep, so I'm closing this one.

Some things from the review, in case you continue:

  • The .cu -> SYCL rewrite runs in execute_process at configure time, so editing a kernel does not regenerate it. An add_custom_command with DEPENDS would fix that.
  • The MKL path is pinned to /opt/intel, and the build needs python3 (Linux only).
  • The changes to weights.cpp and native_dense.cpp error strings, and device.cu reporting cc 8.0, touch every backend.

The most useful finding here is the doorbell fence: a sycl::atomic_fence(seq_cst, system) in __nanosleep so the GPU sees the CPU's flag stores. The #423 port avoids that path with STRATA_VERIFY_NO_HOST=1, which only works when every expert fits in VRAM. Smaller cards (B580, B50) need the host-served path, and a B580 tester reported device loss. A PR that brings your fence to sycl/ (its spin-pause sites) and lets a 12-16 GB card run without NO_HOST would be very welcome.

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.

2 participants