[XPU] Enable XPU graph support (decode full-graph + prefill tc_piecewise) (#29053)

This commit is contained in:
Cao E
2026-07-02 13:24:35 +08:00
committed by GitHub
parent 4fffc6448b
commit 926140d789
25 changed files with 886 additions and 43 deletions
+4 -1
View File
@@ -23,9 +23,10 @@ from sglang.srt.compilation.compiler_interface import EagerAdapter, InductorAdap
from sglang.srt.compilation.cuda_piecewise_backend import CUDAPiecewiseBackend
from sglang.srt.compilation.npu_piecewise_backend import NPUPiecewiseBackend
from sglang.srt.compilation.pass_manager import PostGradPassManager
from sglang.srt.compilation.xpu_piecewise_backend import XPUPiecewiseBackend
from sglang.srt.environ import envs
from sglang.srt.platforms import current_platform
from sglang.srt.utils.common import is_npu
from sglang.srt.utils.common import is_npu, is_xpu
logger = logging.getLogger(__name__)
@@ -53,6 +54,8 @@ def make_backend(
if current_platform.is_out_of_tree():
backend_cls = current_platform.get_piecewise_backend_cls()
elif is_xpu():
backend_cls = XPUPiecewiseBackend
elif is_npu():
backend_cls = NPUPiecewiseBackend
else:
@@ -2,14 +2,16 @@ from typing import Any, Union
import torch
from sglang.srt.utils.common import is_cuda, is_hip, is_musa, is_npu
from sglang.srt.utils.common import is_cuda, is_hip, is_musa, is_npu, is_xpu
if is_cuda() or is_hip() or is_musa():
if is_cuda() or is_hip() or is_musa() or is_xpu():
from sgl_kernel import weak_ref_tensor
elif is_npu():
from torch_npu._C import _weak_ref_tensor as weak_ref_tensor
else:
raise NotImplementedError("weak_ref_tensor is implemented only for CUDA and NPU.")
raise NotImplementedError(
"weak_ref_tensor is implemented only for CUDA, XPU, and NPU."
)
def weak_ref_tensors(
@@ -0,0 +1,99 @@
from contextlib import ExitStack
from typing import Any
from unittest.mock import patch
import torch
from sglang.srt.compilation.compilation_counter import compilation_counter
from sglang.srt.compilation.compile_phase import (
get_pcg_capture_stream,
is_in_torch_compile_warmup,
)
from sglang.srt.compilation.cuda_piecewise_backend import (
CUDAPiecewiseBackend,
weak_ref_tensors,
)
class XPUPiecewiseBackend(CUDAPiecewiseBackend):
def __call__(self, *args) -> Any:
if not self.first_run_finished:
self.first_run_finished = True
self.check_for_ending_compilation()
return self.compiled_graph_for_general_shape(*args)
if len(self.sym_shape_indices) == 0:
return self.compiled_graph_for_general_shape(*args)
runtime_shape = args[self.sym_shape_indices[0]]
if runtime_shape not in self.concrete_size_entries:
return self.compiled_graph_for_general_shape(*args)
entry = self.concrete_size_entries[runtime_shape]
if entry.runnable is None:
entry.runnable = self.compiled_graph_for_general_shape
if entry.need_to_compile and not entry.compiled:
entry.compiled = True
self.to_be_compiled_sizes.remove(runtime_shape)
entry.runnable = self.sglang_backend.compiler_manager.compile(
self.graph,
args,
self.inductor_config,
graph_index=self.piecewise_compile_index,
num_graphs=self.total_piecewise_compiles,
runtime_shape=runtime_shape,
)
if self.is_last_graph and not self.to_be_compiled_sizes:
self.check_for_ending_compilation()
if is_in_torch_compile_warmup():
return entry.runnable(*args)
if entry.cudagraph is None:
if entry.num_finished_warmup < 1: # noqa
entry.num_finished_warmup += 1
return entry.runnable(*args)
stream = get_pcg_capture_stream()
assert (
stream is not None
), "PCG capture stream is not set, please check if runtime recompilation happened"
if self.compile_config.get_enable_debug_mode():
entry.input_addresses = [
x.data_ptr() for x in args if isinstance(x, torch.Tensor)
]
xpugraph = torch.xpu.XPUGraph()
with ExitStack() as stack:
if not self.is_first_graph:
stack.enter_context(patch("gc.collect", lambda: None))
stack.enter_context(patch("torch.xpu.empty_cache", lambda: None))
with torch.xpu.graph(
xpu_graph=xpugraph, pool=self.graph_pool, stream=stream
):
output = entry.runnable(*args)
if self.is_last_graph:
output = weak_ref_tensors(output)
entry.output = weak_ref_tensors(output)
entry.cudagraph = xpugraph
compilation_counter.num_cudagraph_captured += 1
return output
if self.compile_config.get_enable_debug_mode():
new_input_addresses = [
x.data_ptr() for x in args if isinstance(x, torch.Tensor)
]
assert new_input_addresses == entry.input_addresses, (
"Input addresses for cudagraphs are different during replay."
f" Expected {entry.input_addresses}, got {new_input_addresses}"
)
entry.cudagraph.replay()
return entry.output
@@ -517,7 +517,7 @@ class GroupCoordinator:
def graph_capture(
self,
graph_capture_context: Optional[GraphCaptureContext] = None,
stream: Optional[torch.cuda.Stream] = None,
stream=None,
):
if graph_capture_context is None:
if stream is None:
@@ -606,7 +606,15 @@ class GroupCoordinator:
return self.hpu_communicator.all_reduce(input_)
if self.xpu_communicator is not None and not self.xpu_communicator.disabled:
return self.xpu_communicator.all_reduce(input_)
# Route through inplace_all_reduce custom op so Dynamo treats this as
# an opaque call and does not decompose it into _c10d_functional primitives
# (which invoke sycl_event.wait() and break XPU graph capture).
# Keeps the operation in-place; the all-reduce is performed by
# _all_reduce_in_place, which for XPU falls through to
# torch.distributed.all_reduce on self.device_group (the same group
# used by xpu_communicator).
inplace_all_reduce(input_, group_name=self.unique_name)
return input_
if self.npu_communicator is not None and not self.npu_communicator.disabled:
return self.npu_communicator.all_reduce(input_)
@@ -1002,9 +1010,13 @@ class GroupCoordinator:
return envs.SGLANG_ENABLE_DETERMINISTIC_INFERENCE.get()
def all_gather_into_tensor(self, output: torch.Tensor, input: torch.Tensor):
if _is_npu or _is_xpu:
if _is_npu:
self._all_gather_into_tensor(output, input)
else:
# XPU and CUDA both go through reg_all_gather_into_tensor (custom_op) to
# stay opaque to Dynamo. Calling torch.distributed.all_gather_into_tensor
# directly causes Dynamo to rewrite it as _c10d_functional.all_gather_into_tensor
# + wait_tensor, which invokes sycl_event.wait() and breaks XPU graph capture.
reg_all_gather_into_tensor(output, input, group_name=self.unique_name)
def cp_all_gather_into_tensor_async(
@@ -1762,7 +1774,7 @@ def get_mooncake_transfer_engine():
@contextmanager
def graph_capture(stream: Optional[torch.cuda.Stream] = None):
def graph_capture(stream=None):
"""
`graph_capture` is a context manager which should surround the code that
is capturing the CUDA graph. Its main purpose is to ensure that the
@@ -0,0 +1,100 @@
"""FullXPUGraphBackend — Intel XPU full-graph capture (torch.xpu.XPUGraph).
Mirrors FullCudaGraphBackend with XPU-specific differences:
- Captures via torch.xpu.graph(xpu_graph=...) into torch.xpu.XPUGraph.
- Shares the global graph memory pool with the prefill backend so that
decode + prefill graphs occupy max(decode, prefill) rather than their sum.
- No set_graph_pool_id: SymmetricMemoryContext is never triggered on XPU
(oneCCL has no ncclMemAlloc equivalent; enable_symm_mem defaults False).
"""
from __future__ import annotations
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
import torch
from sglang.srt.model_executor.runner.shape_key import ShapeKey
from sglang.srt.model_executor.runner_backend.base_cuda_graph_backend import (
BaseCudaGraphBackend,
)
from sglang.srt.model_executor.runner_utils.pool import (
get_or_create_global_graph_memory_pool,
)
if TYPE_CHECKING:
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.model_executor.runner.base_cuda_graph_runner import (
BaseCudaGraphRunner,
)
class FullXPUGraphBackend(BaseCudaGraphBackend):
"""One torch.xpu.XPUGraph per shape for Intel XPU devices."""
def __init__(
self,
cuda_graph_runner: BaseCudaGraphRunner,
) -> None:
self._graphs: Dict[Any, torch.xpu.XPUGraph] = {}
self._outputs: Dict[Any, Any] = {}
self._pool = None
self._device_module = cuda_graph_runner.device_module
self._tp_group = cuda_graph_runner.model_runner.tp_group
self._capture_stream: Optional[torch.xpu.Stream] = None
@contextmanager
def capture_session(self, stream: torch.xpu.Stream):
if self._pool is None:
self._pool = get_or_create_global_graph_memory_pool(self._device_module)
self._capture_stream = stream
try:
yield
finally:
self._capture_stream = None
def capture_one(
self,
shape_key: ShapeKey,
forward_fn: Callable[[], Any],
dummies: Optional[Any] = None,
post_warmup_hook: Optional[Callable[[], None]] = None,
) -> None:
for _ in range(2):
self._device_module.synchronize()
self._tp_group.barrier()
forward_fn()
if post_warmup_hook is not None:
post_warmup_hook()
graph = torch.xpu.XPUGraph()
with self._device_module.graph(
xpu_graph=graph, pool=self._pool, stream=self._capture_stream
):
out = forward_fn()
self._graphs[shape_key] = graph
self._outputs[shape_key] = out
def can_run(self, forward_batch: ForwardBatch, shape_key: ShapeKey) -> bool:
return shape_key in self._graphs
@contextmanager
def replay_session(self):
yield
def replay(
self,
shape_key: ShapeKey,
static_forward_batch: ForwardBatch,
**kwargs,
) -> Any:
self._graphs[shape_key].replay()
return self._outputs[shape_key]
def cleanup(self) -> None:
self._graphs.clear()
self._outputs.clear()
self._pool = None
@@ -0,0 +1,173 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Run the model with xpu graph and torch.compile."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import torch
from torch.profiler import ProfilerActivity, profile
from sglang.srt.model_executor.runner import DecodeCudaGraphRunner
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.utils import register_xpu_device_properties_for_dynamo
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from sglang.srt.model_executor.model_runner import ModelRunner
_fake_ops_registered = False
def register_fake_ops():
"""Register fake/abstract implementations for XPU sgl_kernel ops so that
torch.compile (Dynamo) can trace through them using FakeTensors for shape
and dtype propagation, without executing the real GPU kernels.
"""
global _fake_ops_registered
if _fake_ops_registered:
return
_fake_ops_registered = True
@torch.library.register_fake("sgl_kernel::fwd")
def _(
q,
k,
v,
q_v,
cu_seqlens_q,
cu_seqlens_k,
max_seqlen_q,
max_seqlen_k,
page_table,
kv_batch_idx,
leftpad_k,
rotary_cos,
rotary_sin,
seqlens_rotary,
q_descale,
k_descale,
v_descale,
softmax_scale,
sinks,
is_causal,
window_size_left,
window_size_right,
softcap,
is_rotary_interleaved,
scheduler_metadata,
num_kv_splits,
pack_gqa,
sm_margin,
out=None,
):
total_q = q.shape[0]
num_heads_q = q.shape[1]
head_size_v = v.shape[-1]
if out is None:
out = q.new_empty(total_q, num_heads_q, head_size_v)
softmax_lse = q.new_empty(num_heads_q, total_q, dtype=torch.float32)
# out_accum and softmax_lse_accum are intermediate split-kv buffers;
# they are only read when num_kv_splits > 1, which is determined at
# runtime. Return empty tensors with correct rank so downstream ops
# that index into the list do not fail shape propagation.
out_accum = q.new_empty(0)
softmax_lse_accum = q.new_empty(0, dtype=torch.float32)
return (out, softmax_lse, out_accum, softmax_lse_accum)
@torch.library.register_fake("sgl_kernel::flash_mla_decode")
def _(
out,
q_nope,
q_pe,
kv_c_and_k_pe_cache,
seq_lens,
page_table,
workspace,
sm_scale,
num_kv_splits,
):
return
class XPUGraphRunner(DecodeCudaGraphRunner):
"""A XPUGraphRunner runs the forward pass of a model with xpu graph and torch.compile."""
@staticmethod
def _apply_xpu_compile_config() -> None:
"""Apply XPU-specific torch.compile / dynamo settings.
Called unconditionally before super().__init__() so that the settings
are in place regardless of whether --enable-torch-compile is passed.
The critical flag is suppress_errors: when the Intel IGC compiler
crashes with SIGFPE on certain reduction kernels (ocloc -device bmg
returns exit code 245), dynamo falls back to eager for that subgraph
instead of propagating the crash.
"""
import torch._dynamo.config
torch._dynamo.config.suppress_errors = True
def __init__(self, model_runner: ModelRunner):
assert (
not model_runner.server_args.enable_memory_saver
), "XPUGraphRunner does not support Torch Memory Saver yet."
register_fake_ops()
self._apply_xpu_compile_config()
register_xpu_device_properties_for_dynamo()
super().__init__(model_runner)
assert (
not self.enable_two_batch_overlap
), "XPUGraphRunner does not support two batch overlap yet."
assert (
not self.require_mlp_tp_gather
), "XPUGraphRunner does not support MLP TP gather yet."
assert (
not self.require_mlp_sync
), "XPUGraphRunner does not support MLP sync yet."
assert (
not self.require_gathered_buffer
), "XPUGraphRunner does not support gathered buffer yet."
assert (
model_runner.spec_algorithm == SpeculativeAlgorithm.NONE
), "XPUGraphRunner does not support speculative inference yet."
def _init_profile_context_and_memory_record(self):
profile_context = profile(
activities=[ProfilerActivity.CPU, ProfilerActivity.XPU],
record_shapes=True,
)
torch.xpu.memory._record_memory_history()
return profile_context
def _post_process_after_profile(self, prof_context):
torch.xpu.memory._dump_snapshot("xpu_graph_runner_memory_usage.pickle")
torch.xpu.memory._record_memory_history(enabled=None)
log_message = (
"Sorted by XPU Time:\n"
+ prof_context.key_averages(group_by_input_shape=True).table(
sort_by="self_xpu_time_total"
)
+ "\n\nSorted by CPU Time:\n"
+ prof_context.key_averages(group_by_input_shape=True).table(
sort_by="self_cpu_time_total"
)
+ "\n\nMemory Usage is saved to xpu_graph_runner_memory_usage.pickle\n"
)
logger.info(log_message)
@@ -6,6 +6,7 @@
# The models we train have hidden dim up to 8k anyway (e.g. Llama 70B), so this is fine.
from contextlib import nullcontext
from functools import lru_cache
import torch
@@ -254,7 +255,21 @@ def _layer_norm_fwd(
# Update grid to use rows_per_block
grid = (cdiv(M, rows_per_block), ngroups)
pdl_kwargs = {"USE_GDC": True, "launch_pdl": True} if is_arch_support_pdl() else {}
with device_context(x.device):
# Workaround for PyTorch <= 2.12: torch.xpu.device is not Dynamo-compatible
# in that release — it creates a DynamoConfigPatchProxy that
# SourcelessBuilder cannot wrap, causing a hard error under
# torch.compile(fullgraph=True). The device context is a functional no-op
# for Triton kernel launches (device is determined by the tensor, not the
# surrounding context), so we simply skip it when Dynamo is tracing.
# PyTorch main already has the proper fix (XPUDeviceVariable registered in
# torch/_dynamo/variables/ctx_manager.py analogous to CUDADeviceVariable).
# TODO: remove this branch once we upgrade from PyTorch 2.12.
device_ctx = (
nullcontext()
if x.device.type == "xpu" and torch.compiler.is_compiling()
else device_context(x.device)
)
with device_ctx:
_layer_norm_fwd_1pass_kernel[grid](
x,
out,
+224 -18
View File
@@ -104,6 +104,7 @@ class XPUAttentionBackend(AttentionBackend):
self.has_swa = (
self.sliding_window_size is not None and self.sliding_window_size > -1
)
self.is_encoder_decoder = model_runner.model_config.is_encoder_decoder
def init_forward_metadata(self, forward_batch: ForwardBatch):
"""Initialize forward metadata hence all layers in the forward pass can reuse it."""
@@ -411,22 +412,6 @@ class XPUAttentionBackend(AttentionBackend):
workspace_size, device=self.device, dtype=torch.uint8
)
# Translate full-pool indices to SWA-pool indices for hybrid models
if self.use_sliding_window_kv_pool:
# flash_attn_with_kvcache requires int32 page tables; the SWA index
# mapping is int64, so cast (matches flashattention_backend.py).
metadata.swa_page_table = (
self.token_to_kv_pool.translate_loc_from_full_to_swa(
metadata.page_table
).to(torch.int32)
)
if forward_batch.out_cache_loc is not None:
metadata.swa_out_cache_loc = (
self.token_to_kv_pool.translate_loc_from_full_to_swa(
forward_batch.out_cache_loc
)
)
# Convert the page table to a strided format which is needed by FA3 API
if self.page_size > 1:
self.strided_indices = torch.arange(
@@ -477,7 +462,10 @@ class XPUAttentionBackend(AttentionBackend):
if not self.use_mla:
self.token_to_kv_pool.set_kv_buffer(
layer,
KVWriteLoc(cache_loc, self.forward_metadata.swa_out_cache_loc),
KVWriteLoc(
cache_loc,
self.forward_metadata.swa_out_cache_loc,
),
k,
v,
layer.k_scale,
@@ -599,6 +587,19 @@ class XPUAttentionBackend(AttentionBackend):
k_descale=k_descale,
v_descale=v_descale,
return_softmax_lse=use_cascade_attn,
# Piecewise XPU graph for prefill requires a pre-allocated
# output buffer at a stable device address so the graph can
# record writes to the same storage on every replay.
# _attn_output is that fixed buffer; None falls back to a
# freshly allocated tensor (eager / cascade-attn path).
out=(
forward_batch._attn_output.view(
-1, layer.tp_q_head_num, layer.v_head_dim
)
if not use_cascade_attn
and getattr(forward_batch, "_attn_output", None) is not None
else None
),
**kwargs,
)
@@ -783,7 +784,10 @@ class XPUAttentionBackend(AttentionBackend):
if not self.use_mla:
self.token_to_kv_pool.set_kv_buffer(
layer,
KVWriteLoc(cache_loc, self.forward_metadata.swa_out_cache_loc),
KVWriteLoc(
cache_loc,
self.forward_metadata.swa_out_cache_loc,
),
k,
v,
layer.k_scale,
@@ -997,6 +1001,208 @@ class XPUAttentionBackend(AttentionBackend):
"""Get the fill value for sequence length in CUDA graph."""
return 1
def init_cuda_graph_state(self, max_bs: int, max_num_tokens: int):
"""Pre-allocate fixed-size tensors reused across XPU graph captures."""
max_num_pages = (self.max_context_len + self.page_size - 1) // self.page_size
self.decode_cuda_graph_metadata = {
"cache_seqlens": torch.zeros(max_bs, dtype=torch.int32, device=self.device),
"cu_seqlens_q": torch.arange(
0, max_bs + 1, dtype=torch.int32, device=self.device
),
"cu_seqlens_k": torch.zeros(
max_bs + 1, dtype=torch.int32, device=self.device
),
"page_table": torch.zeros(
max_bs, max_num_pages, dtype=torch.int32, device=self.device
),
"strided_indices": torch.arange(
0, self.max_context_len, self.page_size, device=self.device
),
}
if self.use_sliding_window_kv_pool:
self.decode_cuda_graph_metadata["swa_page_table"] = torch.zeros(
max_bs, max_num_pages, dtype=torch.int32, device=self.device
)
self.decode_cuda_graph_metadata["swa_out_cache_loc"] = torch.zeros(
max_num_tokens, dtype=torch.int64, device=self.device
)
if self.is_encoder_decoder:
self.encoder_metadata = {
"encoder_page_table": torch.zeros(
max_bs, self.max_context_len, dtype=torch.int32, device=self.device
),
"encoder_lens_int32": torch.zeros(
max_bs, dtype=torch.int32, device=self.device
),
"encoder_cu_seqlens_k": torch.zeros(
max_bs + 1, dtype=torch.int32, device=self.device
),
}
else:
self.encoder_metadata = {}
def init_forward_metadata_out_graph(
self,
forward_batch: ForwardBatch,
in_capture: bool = False,
):
"""New unified graph capture/replay entry point (replaces the legacy
init_forward_metadata_capture_cuda_graph /
init_forward_metadata_replay_cuda_graph pair).
Called by DecodeCudaGraphRunner:
- capture: in_capture=True → bind static metadata buffer slices, then fill
- replay: in_capture=False → update pre-allocated buffers in-place
- eager: via init_forward_metadata() default wrapper
"""
bs = forward_batch.batch_size
req_pool_indices = forward_batch.req_pool_indices
seq_lens = forward_batch.seq_lens
seq_lens_cpu = getattr(forward_batch, "seq_lens_cpu", None)
forward_mode = forward_batch.forward_mode
spec_info = forward_batch.spec_info
assert (
spec_info is None
), "XPUAttentionBackend does not support speculative decoding in XPU graph"
assert (
forward_mode.is_decode_or_idle()
), "XPUAttentionBackend XPU graph only supports decode mode"
if in_capture:
# Bind static-shape slices of the pre-allocated buffers so the
# captured graph always reads from the same storage addresses.
metadata = FlashAttentionMetadata()
metadata.cache_seqlens_int32 = self.decode_cuda_graph_metadata[
"cache_seqlens"
][:bs]
metadata.cu_seqlens_q = self.decode_cuda_graph_metadata["cu_seqlens_q"][
: bs + 1
]
metadata.cu_seqlens_k = self.decode_cuda_graph_metadata["cu_seqlens_k"][
: bs + 1
]
metadata.page_table = self.decode_cuda_graph_metadata["page_table"][:bs, :]
if self.use_sliding_window_kv_pool:
# Bind SWA page table slice so the graph captures the right tensor.
metadata.swa_page_table = self.decode_cuda_graph_metadata[
"swa_page_table"
][:bs, :]
if self.is_encoder_decoder and forward_batch.encoder_lens is not None:
encoder_bs = forward_batch.encoder_lens.numel()
metadata.encoder_lens_int32 = self.encoder_metadata[
"encoder_lens_int32"
][:encoder_bs]
metadata.encoder_cu_seqlens_k = self.encoder_metadata[
"encoder_cu_seqlens_k"
][: encoder_bs + 1]
metadata.encoder_page_table = self.encoder_metadata[
"encoder_page_table"
][:bs, :]
self.decode_cuda_graph_metadata[bs] = metadata
# Both capture and replay: fill data into the pre-allocated buffers.
seq_lens = seq_lens[:bs]
seq_lens_cpu = seq_lens_cpu[:bs] if seq_lens_cpu is not None else None
req_pool_indices = req_pool_indices[:bs]
metadata = self.decode_cuda_graph_metadata[bs]
max_len = (
seq_lens_cpu.max().item()
if seq_lens_cpu is not None
else seq_lens.max().item()
)
metadata.max_seq_len_k = max_len
metadata.cache_seqlens_int32.copy_(seq_lens.to(torch.int32))
metadata.cu_seqlens_k[0] = 0
metadata.cu_seqlens_k[1 : bs + 1].copy_(
torch.cumsum(seq_lens.to(torch.int32), dim=0)
)
if self.is_encoder_decoder and forward_batch.encoder_lens is not None:
encoder_lens = forward_batch.encoder_lens[:bs].to(torch.int32)
metadata.encoder_max_seq_len_k = int(encoder_lens.max().item())
metadata.encoder_lens_int32.copy_(encoder_lens)
metadata.encoder_cu_seqlens_k[0] = 0
metadata.encoder_cu_seqlens_k[1 : bs + 1].copy_(
torch.cumsum(encoder_lens, dim=0, dtype=torch.int32)
)
metadata.encoder_page_table[:bs, : metadata.encoder_max_seq_len_k].copy_(
self.req_to_token[
req_pool_indices, : metadata.encoder_max_seq_len_k
].to(torch.int32)
)
# Self-attention (text) page_table: decoder tokens start after encoder tokens.
text_max = metadata.max_seq_len_k
arange_text = torch.arange(text_max, device=req_pool_indices.device)
text_col = encoder_lens[:bs].long().unsqueeze(1) + arange_text.unsqueeze(0)
text_row = req_pool_indices.unsqueeze(1).expand(-1, text_max)
metadata.page_table[:bs, :text_max].copy_(
self.req_to_token[text_row, text_col].to(torch.int32)
)
metadata.page_table[:bs, text_max:].zero_()
else:
raw_page = self.req_to_token[
req_pool_indices[:, None],
self.decode_cuda_graph_metadata["strided_indices"][
: ((metadata.max_seq_len_k + self.page_size - 1) // self.page_size)
][None, :],
]
if self.page_size > 1:
raw_page = raw_page // self.page_size
metadata.page_table[:bs, : raw_page.shape[1]].copy_(
raw_page.to(torch.int32)
)
metadata.page_table[:bs, raw_page.shape[1] :].zero_()
if self.use_sliding_window_kv_pool:
if forward_batch.out_cache_loc is None:
raise ValueError(
f"out_cache_loc is None for hybrid SWA model in graph "
f"{'capture' if in_capture else 'replay'} "
f"(forward_mode={forward_batch.forward_mode}). This should not happen."
)
swa_out_cache_loc = self.decode_cuda_graph_metadata["swa_out_cache_loc"]
n = forward_batch.out_cache_loc.shape[0]
swa_out_cache_loc[n:].zero_()
swa_out_cache_loc[:n].copy_(
self.token_to_kv_pool.translate_loc_from_full_to_swa(
forward_batch.out_cache_loc
)
)
metadata.swa_out_cache_loc = swa_out_cache_loc[:n]
if not (self.is_encoder_decoder and forward_batch.encoder_lens is not None):
max_seq_pages = (
metadata.max_seq_len_k + self.page_size - 1
) // self.page_size
swa_page_table = self.decode_cuda_graph_metadata["swa_page_table"]
swa_page_table[:bs, max_seq_pages:].zero_()
swa_page_table[:bs, :max_seq_pages].copy_(
(
self.token_to_kv_pool.translate_loc_from_full_to_swa(raw_page)
if self.page_size == 1
else self.token_to_kv_pool.translate_loc_from_full_to_swa(
self.req_to_token[
req_pool_indices[:, None],
self.decode_cuda_graph_metadata["strided_indices"][
:max_seq_pages
][None, :],
]
)
// self.page_size
).to(torch.int32)
)
metadata.swa_page_table = swa_page_table[:bs, :]
self.forward_metadata = metadata
def init_forward_metadata_in_graph(self, forward_batch: ForwardBatch):
"""Graph-recordable ops for XPU graph (no-op: all metadata setup is
host-side and lives in init_forward_metadata_out_graph)."""
def _init_local_attn_metadata(
self,
forwardbatch: ForwardBatch,
@@ -112,6 +112,7 @@ from sglang.srt.eplb.lplb_solver import (
set_global_lplb_solver,
)
from sglang.srt.hardware_backend.npu.graph_runner.npu_graph_runner import NPUGraphRunner
from sglang.srt.hardware_backend.xpu.graph_runner.xpu_graph_runner import XPUGraphRunner
from sglang.srt.kv_canary.api import install_canary
from sglang.srt.kv_canary.runner.canary_manager import context_tuple
from sglang.srt.kv_canary.token_oracle.install import install_token_oracle_from_env
@@ -879,7 +880,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
if self.device == "cuda" or self.device == "musa":
self.init_cublas()
self.init_attention_backend()
elif self.device == "cpu":
elif self.device in ["cpu", "xpu"]:
self.init_attention_backend()
elif self.device == "npu":
self.init_attention_backend()
@@ -922,7 +923,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.graph_mem_usage = 0
if capture_decode_cuda_graph:
if self.device in ("cuda", "musa", "cpu", "npu"):
if self.device in ("cuda", "musa", "cpu", "npu", "xpu"):
self.init_decode_cuda_graph()
elif (
current_platform.is_out_of_tree()
@@ -2601,6 +2602,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
"musa": "CUDA graph",
"cpu": "CPU graph",
"npu": "NPU graph",
"xpu": "XPU graph",
},
)
role = "draft" if self.is_draft_worker else "target"
@@ -2636,6 +2638,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
{
"cpu": CPUGraphRunner,
"npu": NPUGraphRunner,
"xpu": XPUGraphRunner,
},
)
self.decode_cuda_graph_runner = graph_runners[self.device](self)
@@ -148,12 +148,17 @@ class TcPiecewiseCudaGraphBackend(BaseCudaGraphBackend):
enable_torch_compile_warmup to drive FX / inductor through
every shape without capturing cuda graphs yet."""
language_model = self._language_model
# Some multimodal models (e.g. Gemma4) store the inner transformer
# directly as `language_model` rather than wrapping it in a
# ForCausalLM that has a `.model` child. Fall back to the module
# itself when `.model` is absent.
inner_model = getattr(language_model, "model", language_model)
compiler = self._compile_config.compiler
with enable_tc_piecewise_cuda_graph():
try:
if compiler != "eager":
_toggle_multi_platform_ops(
language_model.model, reverse=False, num_tokens=16
inner_model, reverse=False, num_tokens=16
)
cuda_graph_runner._run_dummy_forward(
@@ -167,7 +172,7 @@ class TcPiecewiseCudaGraphBackend(BaseCudaGraphBackend):
set_graph_pool_id(self._pool)
self.install_compile(
language_model.model,
inner_model,
compile_config=self._compile_config,
graph_pool=self._pool,
)
@@ -196,9 +201,7 @@ class TcPiecewiseCudaGraphBackend(BaseCudaGraphBackend):
)
cuda_graph_runner._run_dummy_forward(num_tokens=num_tokens)
finally:
_toggle_multi_platform_ops(
language_model.model, reverse=True, num_tokens=16
)
_toggle_multi_platform_ops(inner_model, reverse=True, num_tokens=16)
@contextmanager
def capture_session(self, stream: torch.cuda.Stream):
@@ -78,6 +78,17 @@ def resolve_decode_backend(
return XPUCudaGraphBackend(cuda_graph_runner)
if model_runner.device == "xpu":
if backend_name not in (Backend.FULL, Backend.DISABLED):
raise ValueError(
f"XPU only supports cuda_graph_config decode backend 'full', got '{backend_name}'"
)
from sglang.srt.hardware_backend.xpu.graph_runner.xpu_full_graph_backend import (
FullXPUGraphBackend,
)
return FullXPUGraphBackend(cuda_graph_runner)
if backend_name == Backend.BREAKABLE:
return BreakableCudaGraphBackend(
cuda_graph_runner,
+7 -4
View File
@@ -3054,12 +3054,15 @@ class ServerArgs:
def _handle_xpu_backends(self):
if self.device == "xpu":
if self.cuda_graph_config.prefill.backend != Backend.DISABLED:
if self.cuda_graph_config.prefill.backend not in (
Backend.DISABLED,
Backend.TC_PIECEWISE,
):
logger.warning(
"XPU platform does not support piecewise CUDA graph, "
"disabling prefill cuda graph."
"XPU platform currently only supports prefill tc_piecewise CUDA graph; "
"disabling unsupported prefill backend."
)
self.cuda_graph_config.prefill.backend = Backend.DISABLED
self.cuda_graph_config.prefill.backend = Backend.DISABLED
# ------------------------------------------------------------------
# CUDA graph configuration resolution
+14 -3
View File
@@ -179,6 +179,17 @@ def is_xpu() -> bool:
return hasattr(torch, "xpu") and torch.xpu.is_available()
def register_xpu_device_properties_for_dynamo() -> None:
if not is_xpu():
return
import torch._dynamo.utils as dynamo_utils
xpu_props_type = getattr(torch.xpu, "_XpuDeviceProperties", None)
if xpu_props_type is not None:
dynamo_utils.common_constant_types.add(xpu_props_type)
@lru_cache(maxsize=1)
def is_npu() -> bool:
if not hasattr(torch, "npu"):
@@ -610,9 +621,9 @@ def get_available_gpu_memory(
if empty_cache:
empty_device_cache(torch.xpu)
used_memory = torch.xpu.memory_allocated()
total_gpu_memory = torch.xpu.get_device_properties(gpu_id).total_memory
free_gpu_memory = total_gpu_memory - used_memory
# Use mem_get_info() to reflect true OS-level free memory
# including graph pool reservations; avoids KV-cache over-allocation.
free_gpu_memory, total_gpu_memory = torch.xpu.mem_get_info(gpu_id)
elif device == "hpu":
num_gpus = torch.hpu.device_count()