Skip to content

[XPU] Support decode context parallelism (DCP) on Intel XPU - #34355

Merged
mingfeima merged 2 commits into
sgl-project:mainfrom
AnuSajikumar6264:xpu-dcp-support
Oct 12, 2026
Merged

mingfeima merged 2 commits into
sgl-project:mainfrom
AnuSajikumar6264:xpu-dcp-support

Conversation

@AnuSajikumar6264

@AnuSajikumar6264 AnuSajikumar6264 commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Enable --dcp-size > 1 (decode context parallelism) on Intel XPU in the intel_xpu attention backend, as requested in review. DCP shards the KV cache across the ranks of a TP group by token position, and merges each rank's partial attention output by log-sum-exp. It grows the servable context when tp_size exceeds the model's KV heads, which TP would otherwise replicate.

Modifications

DCP lives in xpu_backend.py and uses only XPU-native kernels (sgl-kernel-xpu and torch ops). There is no Triton on the DCP path.

Step XPU implementation
KV write The position's owner (pos % dcp_size) stores it at loc // dcp_size. Rows other ranks own get index -1, which store_cache_xpu skips, so every layer uses the native store. The slots are built once per batch.
Decode All-gather Q across the DCP group, then run the paged FMHA (flash_attn_with_kvcache(return_softmax_lse=True)) over this rank's shard. Merge with one all_to_all_single plus sgl-kernel-xpu merge_state_v2. This is the new XPU default, --dcp-comm-backend a2a; ag_rs (cp_lse_ag_out_rs_mha) stays selectable.
Extend New tokens run causal (flash_attn_varlen_func). The sharded prefix runs over the gathered Q and is merged with cp_lse_ag_out_rs_mha. merge_state_v2 combines the two.
Page table A strided view of req_to_token, one gather and one divide.

Sharding and merge follow the shared DCP path the Triton backend runs on CUDA/ROCm, and a2a stays decode-only, as upstream's is.

Two XPU-specific details:

  • sgl-kernel-xpu's varlen prefill hangs at seqlen_q == 1, so a 1-token chunk uses the closed form (output v, LSE the single score).
  • The FMHA returns NaN for a row whose rank holds no keys for it, so those rows are zeroed before the merge.

Shared plumbing, all XPU-gated:

  • parallel_state admits XPU.
  • _alloc_page_size uses the physical allocator page.
  • _dcp_comm_backend_default resolves a2a on XPU; the help text and DCP docs are updated to match.

Startup checks (XPU only)

attention_hook._check_xpu_dcp refuses each of these at startup, naming the flag to change:

  • an attention backend other than intel_xpu;
  • MLA models;
  • unequal K and V head dims (those fall back to an indexed write that does not skip -1);
  • --dcp-size not dividing --tp-size;
  • models whose ranks would not share their DCP group's KV heads (everything but Qwen3.5 when total_num_kv_heads > tp / dcp);
  • a decode GQA group above 16;
  • speculative decoding;
  • chunked local attention;
  • encoder-decoder models.

xpu_backend refuses what only the loaded model settles: sliding-window layers, a quantized KV cache, and captured graphs.

No code path off XPU changes.

Changes since the previous revision

These address the review by @mingfeima:

  • Non-owned KV rows: they now get index -1, which store_cache_xpu skips, instead of slot 0, which a live token can map to. The parity test now places live tokens at local slot 0: the old code fails 10 of its subtests, the fix passes.
  • KV cache dtype: the DCP check reads the resolved dtype, so --kv-cache-dtype bf16 is accepted, and an fp8 cache (explicit, or an fp8 checkpoint under auto) is still refused.
  • a2a buffers: send and recv are cached on the backend and reused while the shape holds, so the decode layers after the first skip the allocation.
  • Kernel version check: removed, since main pins sglang-kernel-xpu 0.3.0.
  • Naming: replicated is now kv_head_layout_supported.

Earlier, from the review by @rahulvijayaraghavan:

  • Decode merge: XPU-native a2a, now the XPU default. Per layer on 2 GPUs (16 heads, batch 1-128) it takes 178-276 us, vs 411-462 us for ag_rs, at the same max error.
  • KV write: the native store, with slots built once per batch from the padded positions. This replaces the per-layer mask and the Triton masked write: ~12 vs 44-47 us per layer at decode sizes.
  • Extend merge: merge_state_v2, 2.3-5.4x faster than the torch merge. The fp32 casts now happen only where the merge needs them.
  • Page table: rebuilt with torch, 23 us vs 69-76 us.
  • Empty-row mask: built on the device, with no blocking copy. The host stall drops from ~16 ms to 0.2 ms.
  • forward_batch_info.py: no longer changed.
  • Tests: the parity test now writes the new tokens through a real KV pool, runs decode under both merges, and covers ranks without keys.

Rebased onto main 8ad24fb560.

Accuracy Tests

Setup: Qwen3.5-4B, 2x Arc Pro B60, TP=2, intel_xpu, sglang-kernel-xpu 0.3.0, --disable-radix-cache --chunked-prefill-size 256.

GSM8K, all 1319 questions, 5-shot, same workload:

dcp=1 dcp=2
This revision 0.8828 0.8858
Previous revision 0.8782

Greedy parity vs dcp=1 (12 requests, 64 new tokens each). The mean logprob gap over the agreeing tokens is 0.0028 with a2a and 0.0025 with ag_rs, vs 0.0027 before. Every divergence is a near-tie between two tokens, e.g. 0.29 vs 0.30.

Speed Tests and Profiling

Decode tok/s for Qwen3.5-4B, TP=2, 4096-token prompts, 64 new tokens; median of 5 interleaved runs:

batch dcp=1 dcp=2, previous revision dcp=2, this revision
1 15.16 13.03 13.49 (+3.5%)
4 65.42 52.50 54.82 (+4.4%)
16 253.16 209.41 213.88 (+2.1%)

Prefill is 0.2-0.8% faster. DCP trades decode speed for context. Qwen3.5-4B has 4 KV heads, so TP=2 does not replicate them: at dcp=2 each rank holds all 4 heads for half the positions, and the per-rank KV pool goes from 340,864 to 170,112 tokens. The total context is unchanged, so this model checks correctness, not the context gain. The gain shows when heads are replicated: GLM-4.1V-9B (2 KV heads) at TP=4 kept the same per-rank capacity at dcp=2, which doubles the servable context (measured on an earlier revision).

Tests

  • test/registered/xpu/test_xpu_dcp.py (1 XPU, stage-b-test-1-gpu-xpu). It runs decode (under ag_rs and a2a) and extend on threaded ranks against full attention at dcp 2 and 4, including ranks that hold no keys. The new tokens are written through the backend into a real MHATokenToKVPool, so a wrong KV slot shows up as wrong attention output. The file also covers the startup checks.
  • test/registered/xpu/test_xpu_dcp_gsm8k.py (2 XPUs, nightly-xpu-2-gpu): a GSM8K gate at 0.80 on Qwen3.5-4B, TP=2/DCP=2. Local run: 0.885, passed.
  • test_server_args gains an XPU comm-backend default case, and its platform-faked cases now pin is_xpu.
  • test_encoder_decoder_varlen_gather sets dcp_size on its stubbed backend.
  • test_xpu_int4_dense sets tp_group on its layer. Since [Refactor] Retain process groups in parallel linear layers #42588 the GPTQ XPU kernel reads tp_size from it, so the split-group test was failing XPU CI on main.

How to test

# one XPU
python -m pytest -q test/registered/xpu/test_xpu_dcp.py

# two XPUs
python -m pytest -q test/registered/xpu/test_xpu_dcp_gsm8k.py

Or by hand, launching once with --dcp-size 1 and once with --dcp-size 2:

python -m sglang.launch_server --model-path Qwen/Qwen3.5-4B --device xpu \
  --tp-size 2 --dcp-size 2 --attention-backend intel_xpu \
  --disable-radix-cache --chunked-prefill-size 256 --mem-fraction-static 0.60 --port 30000

python -m sglang.test.run_eval --base-url http://127.0.0.1:30000 --eval-name gsm8k \
  --api completion --num-examples 1319 --num-shots 5 --max-tokens 512 --num-threads 32

Not covered here

  • Graph mode with DCP: refused for now. The capture path doesn't build the DCP metadata, and all_to_all_single can't be captured on XPU (all_gather and all_reduce can). Planned as a follow-up.
  • Radix cache: Qwen3.5 on XPU needs --disable-radix-cache, independently of DCP.
  • dcp=4: covered by the unit tests and a 4-rank oneCCL check, but not on 4 separate GPUs.

Checklist


CI States

Latest PR Test (Base): 🚫 Run #38197433978
Latest PR Test (Extra): ❌ Run #38197433448
Latest PR Test (AMD ROCm 10): ❌ Run #38197433838

@siju-samuel

Copy link
Copy Markdown
Contributor

/tag-run-ci-label

Comment thread python/sglang/srt/layers/attention/xpu_backend.py
Comment thread python/sglang/srt/layers/attention/xpu_backend.py Outdated
Comment thread python/sglang/srt/layers/attention/xpu_backend.py Outdated
Comment thread python/sglang/srt/layers/attention/xpu_backend.py Outdated
Comment thread python/sglang/srt/layers/attention/xpu_backend.py
Comment thread python/sglang/srt/layers/attention/xpu_backend.py Outdated
Comment thread python/sglang/srt/layers/attention/xpu_backend.py
Comment thread python/sglang/srt/layers/attention/xpu_backend.py Outdated
Comment thread python/sglang/srt/layers/attention/xpu_backend.py
Comment thread python/sglang/srt/layers/attention/xpu_backend.py Outdated
Comment thread python/sglang/srt/arg_groups/attention_hook.py Outdated
Comment thread python/sglang/srt/arg_groups/attention_hook.py Outdated
Comment thread python/sglang/srt/layers/attention/xpu_backend.py Outdated
@mingfeima

Copy link
Copy Markdown
Collaborator

also please rebase to master.

@AnuSajikumar6264
AnuSajikumar6264 force-pushed the xpu-dcp-support branch 3 times, most recently from 432f7d6 to 0a0c1fa Compare October 8, 2026 17:01
Comment thread test/registered/xpu/test_xpu_dcp_gsm8k.py Outdated
Enable --dcp-size > 1 on Intel XPU with the intel_xpu attention backend. KV is
sharded by token position: the owner of a position (pos % dcp_size) stores it
at slot loc // dcp_size, every rank attends over its own shard, and the
partial outputs are merged by log-sum-exp. The DCP path runs only XPU-native
kernels (sgl-kernel-xpu and torch ops):

* Write: rows another rank owns get index -1, which store_cache_xpu skips, so
  each layer writes through the native store with no owner mask. The slots are
  built once per batch from the padded positions.
* Decode: all-gather the query heads across the DCP group and attend over this
  rank's shard through a per-rank page table. The partials are exchanged with
  one all_to_all_single and merged with sgl-kernel-xpu merge_state_v2, the new
  XPU default for --dcp-comm-backend; ag_rs stays selectable. Rows a rank holds
  no keys for are zeroed first, because the FMHA returns NaN there.
* Extend: the sharded prefix runs non-causal over the gathered queries and is
  merged with cp_lse_ag_out_rs_mha; the new tokens run causal off k/v, and
  merge_state_v2 combines the two.

sgl-kernel-xpu's varlen prefill hangs at seqlen_q == 1, so a 1-token chunk
uses the closed form (output v, LSE the single score).

The rest of the change enables the shared DCP plumbing on XPU: parallel_state
admits XPU, and the allocator uses the physical page size.

attention_hook refuses what DCP on XPU cannot run, at startup: a backend other
than intel_xpu, MLA models, unequal K and V head dims, dcp_size not dividing
tp_size, models whose ranks would not share a DCP group's KV heads (all but
Qwen3.5 when total_num_kv_heads > tp / dcp), a decode GQA group above the 16
the XPU kernel is built for, speculative decoding, chunked local attention and
encoder-decoder models. xpu_backend refuses what only the loaded model settles:
sliding-window layers, a quantized KV cache (including one auto resolves to
fp8) and captured graphs.

Docs: the XPU page documents DCP, and the --dcp-comm-backend help and the DCP
docs list the a2a default on XPU.

Tests: test_xpu_dcp.py checks the DCP decode (under both merges) and extend
paths, including the KV write through a real pool, across threaded ranks
against full attention on one XPU, plus the startup checks.
test_xpu_dcp_gsm8k.py gates GSM8K on Qwen3.5-4B at TP=2/DCP=2 on two XPUs.
test_server_args pins is_xpu in its platform-faked comm-backend tests and adds
the XPU default; test_encoder_decoder_varlen_gather sets dcp_size on its
stubbed backend.

test_xpu_int4_dense: since sgl-project#42588 the GPTQ XPU kernel reads tp_size from the
layer's tp_group rather than get_parallel(), so the split-group test sets
tp_group on its layer to get the --tp-size hint. This unblocks XPU CI.

Co-authored-by: Siju Samuel <siju.samuel@intel.com>
@mingfeima
mingfeima merged commit 8d491fb into sgl-project:main Oct 12, 2026
158 of 193 checks passed
@rahulvijayaraghavan

rahulvijayaraghavan commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

@AnuSajikumar6264
Nice work! Thanks for the contribution, addressing comments!

Liwansi added a commit to Liwansi/sglang_vans that referenced this pull request Oct 13, 2026
…n_allgather

* 'main' of https://github.com/sgl-project/sglang: (236 commits)
  [NPU] Stabilize HiCache and registered CI tests (sgl-project#43308)
  [Docs] Cookbook: refresh SemiAnalysis AgentX launches (GLM-5.2 B300, DeepSeek-V4.1 GB300) (sgl-project#43799)
  [Docs] Update GLM-5.3-Flash playground: B300 default, CP8, EP and PD (sgl-project#43802)
  [DSV4.1][PD][1/N] Support prefill node use cp parallel strategy (sgl-project#40323)
  [BCG][DSV4-Flash][CP] Support DeepSeek V4 prefill with interleave CP and breakable CUDA graphs (sgl-project#38603)
  [XPU] Support decode context parallelism (DCP) on Intel XPU (sgl-project#34355)
  Keep NIXL Mamba checkpoint keys separate by TP rank (sgl-project#43594)
  Capture KV host transfer buffers (sgl-project#43711)
  [CI] Move tests that do not need GB300 off the per-PR GB300 suite (sgl-project#43784)
  [HiCache] Preserve all MTP KV and recurrent state (sgl-project#40223)
  [HiCache] Support Mamba in buffer-only host memory mode (sgl-project#36345)
  [PD] Poison the deferred abort ack when a Mooncake send fails (sgl-project#43719)
  [Perf] Route BF16 mixed-batch decode tails through decode attention (sgl-project#43238)
  [Fix] Link cuda_ipc JIT against CUDA driver stubs dir (sgl-project#43764)
  docs: clarify prefill CP terminology and simplify wording (sgl-project#43787)
  [diffusion][xpu] FLUX.2: enable the bit-exact Triton SwiGLU on XPU (sgl-project#41940)
  [Fix] Adapt to transformers 5.18 / 5.19 changes (sgl-project#43688)
  [XPU CI] Optimize PR CI per-file timeouts and fail hanging tests fast (sgl-project#42048)
  [Deps] Bump FlashInfer to 0.7.1 (sgl-project#43494)
  docs: sync LMSYS SGLang blog cards (sgl-project#43462)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants