Skip to content

disaggregation: Support Intel XPU for staging-buffer KV transfer - #40861

Open
kpjeeja wants to merge 1 commit into
sgl-project:mainfrom
kpjeeja:jeeja/add_xpu_staging_buffer
Open

kpjeeja wants to merge 1 commit into
sgl-project:mainfrom
kpjeeja:jeeja/add_xpu_staging_buffer

Conversation

@kpjeeja

@kpjeeja kpjeeja commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

PD disaggregation with staging-buffer KV transfer assumes CUDA in several places along the Encoder/Prefill/Decode path. On Intel XPU these assumptions either fail immediately (torch.cuda.* calls on a build without CUDA) or silently skip required synchronization because it is gated on is_cuda.

This PR makes the staging-buffer path device-agnostic and enables it end to end on Intel XPU, verified on 3x Intel Arc Pro B60.

Two of the fixes are not XPU-specific:

  • The encoder /send path synchronized only when mm_embedding.is_cuda. The sync is required on every accelerator because transfer_sync does not order against the device stream; without it, the transport can read a buffer before the device writes land.
  • Staging initialization hard-coded device = f"cuda:{gpu_id}", so a non-CUDA backend could silently allocate staging buffers on CUDA.

NIXL with staging was also broken on main for every device: prefill.py and decode.py pass slot_layer_ids= to set_kv_buffer_tensors, which NIXL did not accept, so manager init failed with unexpected keyword argument 'slot_layer_ids'.

Modifications

Device helpers (srt/utils/common.py)

  • Add create_device_event and current_device_stream, which resolve the device module from the given device instead of hard-coding torch.cuda.

Staging buffers (disaggregation/common/staging_buffer.py)

  • Device-aware allocation for CUDA, XPU and NPU; any other device type raises with a hint to unset SGLANG_DISAGG_STAGING_BUFFER.
  • An explicit index on the device string wins over gpu_id (with a warning on mismatch), so a caller passing "xpu:3" cannot allocate on a different device.
  • Custom memory pools stay CUDA-only; XPU/NPU use the default caching allocator, and a custom pool passed on those devices is ignored with a warning.
  • Triton staging kernels are restricted to CUDA. XPU data_ptr() values can set bit 63, which torch.tensor(..., dtype=torch.int64) rejects, and there is no Triton NPU backend; other supported accelerators take the torch gather/scatter path.
  • Gather and scatter assert that the KV buffers and the staging buffer are on the same device, so a cross-device pair fails with a clear message instead of faulting inside a kernel.
  • Allocation logs include ptr_type=XPU-kernel-space / XPU-user-space on XPU.

Stream ordering

  • The gather orders against the calling thread's current stream (StagingBuffer.producer_stream()), resolved per call, instead of torch.cuda.default_stream. The current stream is thread-local, and transfer workers run on their own threads.
  • The decode scatter waits on an event recorded on schedule_stream when the room is registered, right after pre-alloc writes its req_to_token row. Previously there was no ordering edge; XPU has no CUDA-style default-stream synchronization, and a dedicated CUDA stream does not implicitly sync with unrelated work either. Waiting on the stream itself would also queue each scatter behind any forward running there without overlap.
  • The decode scatter stream is now created eagerly, and release_room drains it only when the room still has an in-flight scatter. The stream is shared by all rooms, so draining unconditionally would stall the scheduler thread behind unrelated rooms on the success path. The prefill gather stream stays lazy, since the decode-side allocator's buffer never gathers.

Staging handler and transports (common/staging_handler.py, mooncake/conn.py, nixl/conn.py)

  • Use the runtime device module for Event and Stream creation. DecodeStagingHandler derives its device from the KV buffers and requires an indexed device.
  • Thread the runtime device_type from server_args.device into staging initialization.
  • NIXL: use np.uint64 for staged transfer descriptors so high device pointers are not truncated or read as signed. set_kv_buffer_tensors accepts slot_layer_ids (see Motivation); NIXL staging is pp_size == 1 only, so the value is stored but not read.

DCP pack (disaggregation/common/dcp_pack.py)

  • copy_mla_rows_into_pack is Triton-only, so off CUDA init_dcp_pack_buffers returns no buffers and DCP transfers fall back to per-token RDMA.
  • Order the gather against the staging buffer's producer stream instead of torch.cuda.default_stream, and enter it through the device-agnostic stream context.

Encoder (disaggregation/encoder/server.py, encoder/receiver.py)

  • TensorWrapper copies from any non-CPU device, not only CUDA.
  • /send synchronizes for every non-CPU accelerator instead of gating on is_cuda, and the embedding-cache paths synchronize through current_device_stream.
  • The receiver's RDMA buffers and EmbeddingPool allocate on the runtime device type instead of cuda:{gpu_id}.

Metadata buffers and QK normalization (disaggregation/utils.py, models/utils.py)

  • The INTRA_NODE_NVLINK metadata-buffer placement now also requires is_cuda(). The use_mem_pool context stays on torch.cuda, since custom_mem_pool is a torch.cuda.MemPool.
  • apply_qk_norm enters alt_stream through the device-agnostic stream context.

Docker (docker/xpu.Dockerfile)

  • Build the Mooncake transfer engine from a pinned upstream commit with icpx (-DUSE_XPU=ON -DUSE_TENT=ON -DWITH_STORE=OFF) and install sglang-router, which the PD tests launch between prefill and decode.

Breaking changes for out-of-tree backends

  • init_staging_buffers, init_staging_allocator and init_dcp_pack_buffers take a required keyword-only device_type. The old implicit "cuda" is intentionally gone, so omitting device_type fails loudly instead of allocating staging on the wrong accelerator.
  • gather_all_layers_to_staging no longer takes gpu_id; the device comes from the KV buffers.
  • EmbeddingPool takes keyword-only device_type, gpu_id and size_bytes.

Total: 19 files, +891/-123.

Accuracy Tests

Unit tests (CPU, no accelerator required)

New tests run under register_cpu_ci with stubs/mocks:

  • test_mooncake_transfer_batching.py (TestMooncakeStagingDeviceType): prefill staging buffers and the decode staging allocator receive the runtime device instead of an implicit "cuda". These cover the device-selection regression only, not end-to-end Mooncake staging transfer.
  • test_dcp_pack.py (TestInitDcpPackBuffers): a non-Triton device gets no DCP pack buffers and never registers memory; CUDA buffers use the bare device type and gpu_id.
  • test_encode_server.py: updated for the keyword-only EmbeddingPool signature.

test/registered/unit/disaggregation/: 487 passed.

XPU end-to-end

The registered XPU PD tests run on Mooncake. Mooncake's XPU support lives in its TENT engine, so the test classes set MC_USE_TENT=1 and MOONCAKE_PROTOCOL=tcp and need no RDMA NIC. They skip when mooncake.engine is not importable, so the nightly stays green on an image built before the Dockerfile step.

Verified on 3x Intel Arc Pro B60:

  • test/registered/xpu/test_disaggregation_staging_xpu.py (prefill TP 1, decode TP 2, so KV goes through the staging path): 4 passed, on nightly-xpu-4-gpu.
    • staging buffer allocated on prefill and on decode, including the XPU kernel-space pointer format
    • single completion and deterministic output
    • concurrent requests matching serial output
    • multi-chunk long-sequence generation
  • test/registered/disaggregation/test_disaggregation_xpu.py: 2 passed, on nightly-xpu-2-gpu.
  • The same Mooncake tests pass against a transfer-engine-only install of upstream Mooncake built with the Dockerfile steps.

The concurrent-vs-serial case matters most: it exercises enable_overlap together with a radix-cache prefix hit, and compares against the serial answers at temperature 0, so KV corruption shows up as a token mismatch. It needs the dependency PR above.

The staging e2e test lives under the xpu vendor tree because the registered-test taxonomy rejects new paths outside the <kind>/<subsystem> layout, while vendor directories are exempt.

The NIXL variants are manual tests under test/manual/kv_transfer/: test_disaggregation_nixl_xpu.py (basic completion) and test_disaggregation_staging_nixl_xpu.py (staging). They mirror the registered scenarios and skip when nixl is not installed.

The staging decode server (TP 2) runs with TORCHINDUCTOR_USE_STATIC_CUDA_LAUNCHER=0. On torch 2.13.0+xpu, inductor's static Triton launcher segfaults in load_kernel on the compiled vocab-parallel embedding mask, which only runs at TP > 1. This is not PD-specific: a plain Qwen2.5-1.5B or Llama-3.1-8B server at TP 2 crashes the same way. Turning off only the static launcher keeps torch.compile on, unlike TORCHDYNAMO_DISABLE=1. Likely pytorch/pytorch#199511; the override can go once the XPU torch pin includes the fix.

Performance

No CUDA-path performance regression is intended. The changes are mostly device selection and stream/event handling on initialization or correctness-critical paths.

  • The gather resolves the current stream per call instead of using a fixed stream, measured at about 0.3% overhead on the affected path. The per-call lookup is intentional, because the current stream is thread-local and transfer workers run on separate threads.
  • release_room now skips the scatter-stream drain for rooms with no in-flight scatter, which removes a scheduler-thread stall on the success path.

Checklist

  • [x ] Format your code according to the Format code with pre-commit.
  • Add unit tests according to the Run and add unit tests.
  • [x ] Provide accuracy and speed benchmark results according to Test the accuracy and Benchmark the speed.
  • [x ] Follow the SGLang code style guidance.

CI States

Latest PR Test (Base): ⏳ Run #38372317850
Latest PR Test (Extra): ❌ Run #38372317378
Latest PR Test (AMD ROCm 10): ⏳ Run #38372317893

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@siju-samuel

Copy link
Copy Markdown
Contributor

/tag-run-ci-label

@github-actions github-actions Bot added the run-ci CI: run the baseline test suite on this PR label Sep 23, 2026
Comment thread python/sglang/srt/disaggregation/common/staging_handler.py Outdated
Comment thread python/sglang/srt/disaggregation/common/staging_buffer.py Outdated
Comment thread python/sglang/srt/disaggregation/common/dcp_pack.py
Comment thread python/sglang/srt/disaggregation/common/staging_handler.py Outdated
Comment thread test/registered/xpu/test_disaggregation_staging_xpu.py Outdated
@kpjeeja
kpjeeja force-pushed the jeeja/add_xpu_staging_buffer branch from 1324bc3 to 9c59f13 Compare September 23, 2026 17:58
@kpjeeja
kpjeeja force-pushed the jeeja/add_xpu_staging_buffer branch from 9c59f13 to dd6caed Compare September 24, 2026 06:51
@kpjeeja

kpjeeja commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Below CI failures are not related to this patch: 1. PR Test (NPU) / base-a-test-1-npu-a2 / run (0) (pull_request)Failing after 5m:
ModuleNotFoundError: No module named 'sgl_kernel_npu.kvcacheio'; 'sgl_kernel_npu' is not a package not related to this PR.

  1. stage-a-test-1-gpu-xpu: File "/opt/venv/lib/python3.12/site-packages/sglang/kernels/ops/speculative/ngram_corpus.py", line 8, in
    import tvm_ffi
    ModuleNotFoundError: No module named 'tvm_ffi' -- not an error with this PR.

  2. PR Test (Xeon) / build-test (xeon-spr, spr, stage-a-test-cpu-intel,stage-b-test-cpu-intel, spr3, 2, 3) (pull_request)Failing after 37m
    : test/registered/cpu/test_subblock_sparse_attention.py: File "/usr/local/share/uv/python/cpython-3.12.14-linux-x86_64-gnu/lib/python3.12/unittest/mock.py", line 1437, in get_original
    raise AttributeError(
    AttributeError: <module 'sglang.multimodal_gen.configs.pipeline_configs.minimax_h3' from '/opt/.venv/lib/python3.12/site-packages/sglang/multimodal_gen/configs/pipeline_configs/minimax_h3.py'> does not have the attribute 'get_global_forced_attn_backend'

  3. PR Test (AMD) / stage-b-test-1-gpu-small-amd-mi35x (rocm10, linux-mi35x-gpu-1) (pull_request): sglang-checkout/test/registered/kernels/ops/kimi_k3/flydsl_ops/test_kimi_k3_kda_decode.py", line 16, in
    from aiter.ops.flydsl.utils import is_flydsl_available
    ModuleNotFoundError: No module named 'aiter.ops.flydsl.utils'

  4. PR Test (AMD) / stage-c-dsv4-flash-fp4-fp8-amd-mi35x (rocm10, linux-mi35x-gpu-8)-test/registered/hicache/test_hicache_storage_umbp_backend.py returned exit code 1
    So the root failure is not a broad AMD infra break; it is a single HiCache/UMBP E2E regression: the second request did not observe the expected storage-tier cache hit. - not related to this PR- HICache/UMBP

@yanbing-j

Copy link
Copy Markdown
Collaborator

@mingfeima
This PR enables the XPU staging-buffer path, but the only e2e test that exercises it is still disabled because it depends on upstream NIXL/UCX support (ai-dynamo/nixl#1536, openucx/ucx#11218). Local validation is useful, but for an XPU enablement PR we need a clear merge gate: either enable and rerun this coverage once those dependencies are available in CI, or keep the runtime path gated until the test can run.

@kpjeeja

kpjeeja commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

@mingfeima This PR enables the XPU staging-buffer path, but the only e2e test that exercises it is still disabled because it depends on upstream NIXL/UCX support (ai-dynamo/nixl#1536, openucx/ucx#11218). Local validation is useful, but for an XPU enablement PR we need a clear merge gate: either enable and rerun this coverage once those dependencies are available in CI, or keep the runtime path gated until the test can run.

@yanbing-j
i have added 2 e2e test with Mooncake backend these tests are part of nightly test suite. currently for XPU we don't have configuration in CI merge gate with 2 or 3 cards.

@kpjeeja
kpjeeja force-pushed the jeeja/add_xpu_staging_buffer branch 2 times, most recently from afb8c4d to d550a90 Compare October 7, 2026 05:20
Comment thread test/registered/unit/disaggregation/test_prefill_abort_result_cleanup.py Outdated
Comment thread python/sglang/srt/disaggregation/nixl/conn.py Outdated
Comment thread python/sglang/srt/disaggregation/common/staging_buffer.py Outdated
Comment thread test/registered/xpu/test_disaggregation_staging_xpu.py Outdated
Comment thread python/sglang/srt/disaggregation/common/staging_handler.py
Comment thread python/sglang/srt/disaggregation/encoder/receiver.py
@kpjeeja
kpjeeja force-pushed the jeeja/add_xpu_staging_buffer branch from d31fc0a to a3c6da3 Compare October 8, 2026 18:34

@Lzy17 Lzy17 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. Please split the NIXL early-send wait_event fix into its own PR. It fixes NIXL on CUDA/ROCm as well and doesn't depend on the XPU work. Also confirm the existing CUDA staging suite passes, since the stream/event handling there changed.

@kpjeeja
kpjeeja force-pushed the jeeja/add_xpu_staging_buffer branch from a3c6da3 to 3a82560 Compare October 9, 2026 06:15
@kpjeeja

kpjeeja commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the PR. Please split the NIXL early-send wait_event fix into its own PR. It fixes NIXL on CUDA/ROCm as well and doesn't depend on the XPU work. Also confirm the existing CUDA staging suite passes, since the stream/event handling there changed.

Thanks for the review. I have raised new PR for the early_send fix - #43299

@kpjeeja
kpjeeja force-pushed the jeeja/add_xpu_staging_buffer branch from 3a82560 to 9e14dda Compare October 9, 2026 06:29
Comment thread test/registered/xpu/test_disaggregation_staging_xpu.py Outdated
Comment thread test/registered/disaggregation/test_disaggregation_xpu.py Outdated
@kpjeeja
kpjeeja force-pushed the jeeja/add_xpu_staging_buffer branch from 9e14dda to 92fe74d Compare October 9, 2026 08:48

@siju-samuel siju-samuel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kpjeeja

kpjeeja commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

The CI failures are not related to this PR, below is the Validation Summary

AMD Failures

Job Status Failure Reason
pr-test-amd-finish ❌ Failed (3s) Aggregator job failed because downstream AMD tests failed.
stage-b-test-1-gpu-large-amd ❌ Failed (31m) test_compact_attention_hip.py failed on the DSv4 compact-attention kernel (gfx950) with exit code 1.
stage-b-test-1-gpu-small-amd ❌ Failed (24m) Same compact-attention kernel failure. Test summary: 30 passed, 1 failed.
stage-b-test-1-gpu-small-amd-mi35x (0) ❌ Failed (16m) Same compact-attention kernel failure.
stage-b-test-1-gpu-small-amd-mi35x (1) ❌ Failed (13m) Same compact-attention kernel failure.
stage-b-test-large-8-gpu-mi35x-disaggregation-amd ❌ Failed (17m) Likely the same AMD kernel/test instability. Although part of the disaggregation suite, logs currently point to infrastructure/kernel issues rather than code changes.
wait-for-stage-b-amd ❌ Failed (106m) Aggregator job failed because stage-b jobs failed.

NPU Failures

Job Status Failure Reason
base-b-test-1/2/4/16-npu-a3 ❌ Failed Not yet analyzed individually. Likely infrastructure or performance-related failures.
multimodal-gen-test-1-npu-a3 ❌ Failed (31m) test_diffusion_generation[minimax_h3_t2va_2npu] exceeded latency threshold (480s vs expected 155s).
multimodal-gen-test-4-npu-a3 (0) ❌ Failed (64m) Same latency regression as above.
multimodal-gen-test-4-npu-a3 (1) ❌ Failed (26m) Custom container execution failed. Self-hosted runner/infrastructure issue.
pr-test-npu-finish ❌ Failed (4s) Aggregator job failed because downstream NPU jobs failed.

Xeon Results

Job Status Failure Reason
build-test (spr1) ⚠️ Cancelled (33m) Cancelled, likely due to matrix execution or timeout.
build-test (spr2) ❌ Failed (32m) Failure details not yet reviewed.
build-test (spr3) ⚠️ Cancelled (33m) Cancelled, similar to spr1.

Replace hardcoded torch.cuda calls in the Encoder/Prefill/Decode path with
torch.get_device_module equivalents, so staging-buffer KV transfer works on
Intel XPU. Two of the fixes are not XPU-specific: the encoder /send
synchronize was gated on is_cuda and so was skipped on every other
accelerator, and staging init hardcoded a "cuda:{gpu_id}" device string.

Breaking change for out-of-tree backends: init_staging_buffers,
init_staging_allocator and init_dcp_pack_buffers take a required keyword-only
device_type. The "cuda" default was dropped so no backend can silently
allocate staging on CUDA.

The DCP pack kernel is Triton-only, so off CUDA init_dcp_pack_buffers returns
no buffers and DCP transfers fall back to per-token RDMA.

Adds create_device_event and current_device_stream to srt/utils/common.py.

The decode-side scatter now waits on an event recorded on schedule_stream when
the room is registered, right after pre-alloc writes its req_to_token row,
instead of having no ordering edge. Waiting on the stream itself would also
queue each scatter behind any forward running there without overlap.

The XPU PD tests run on Mooncake in CI (nightly-xpu-2-gpu and
nightly-xpu-4-gpu). The NIXL XPU tests are manual tests under
test/manual/kv_transfer/ (test_disaggregation_nixl_xpu.py and
test_disaggregation_staging_nixl_xpu.py): they mirror the registered scenarios,
skip when nixl is not installed.

docker/xpu.Dockerfile builds the Mooncake transfer
engine from a pinned upstream commit with icpx (-DUSE_XPU=ON -DUSE_TENT=ON
-DWITH_STORE=OFF) and installs sglang-router. The Mooncake classes skip when
mooncake.engine is not importable, so the nightly stays green on an image
built before that Dockerfile step.

Mooncake's XPU support lives in its TENT engine, so those classes set
MC_USE_TENT=1 and MOONCAKE_PROTOCOL=tcp and need no RDMA NIC. The staging
decode server (TP 2) runs with TORCHINDUCTOR_USE_STATIC_CUDA_LAUNCHER=0. On
torch 2.13.0+xpu, inductor's static Triton launcher segfaults in load_kernel on
the compiled vocab-parallel embedding mask, which only runs at TP > 1. This is
not PD-specific: a plain Qwen2.5-1.5B or Llama-3.1-8B server at TP 2 crashes
the same way, with a cold inductor cache too. Turning off only the static
launcher keeps torch.compile on, unlike TORCHDYNAMO_DISABLE=1. Likely
pytorch/pytorch#199511 (XPU static launcher ABI mismatch); drop the override
once the XPU torch pin includes it.

Verified on 3x Intel Arc Pro B60: xpu/test_disaggregation_staging_xpu.py
(prefill TP 1, decode TP 2, so KV goes through the staging path), 4 passed;
test/registered/unit/disaggregation, 487 passed. With Mooncake over TCP:
disaggregation/test_disaggregation_xpu.py 2 passed, staging 4 passed. The same
Mooncake tests pass against a transfer-engine-only install of upstream Mooncake
built with the Dockerfile steps.
@kpjeeja
kpjeeja force-pushed the jeeja/add_xpu_staging_buffer branch from b9f71a7 to 5e1ba04 Compare October 13, 2026 08:52

This branch has not been deployed

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

Labels

run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants