chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,947 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""FB-shared slot registry for the CUDA graph forward paths.
|
||||
|
||||
``CudaGraphBufferRegistry`` is the ForwardBatch → graph-resident buffer mirror
|
||||
used by capture / replay. It replaces the per-runner ``DecodeInputBuffers`` /
|
||||
``PrefillInputBuffers`` dataclasses and their hand-written
|
||||
``populate_from_forward_batch`` methods with a single ``GraphSlot``-driven
|
||||
registry.
|
||||
|
||||
Backend-private buffers (kernel workspaces, derived page tables, etc.) stay
|
||||
on ``AttentionBackend.cuda_graph_*`` — the registry only owns FB-shared
|
||||
slots (FB attribute name maps 1:1 to slot name).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.model_executor.input_buffers import share_input_buffer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
|
||||
|
||||
_has_foreach_copy = hasattr(torch, "_foreach_copy_")
|
||||
|
||||
|
||||
def _grouped_foreach_copy_(dsts: List[torch.Tensor], srcs: List[torch.Tensor]) -> None:
|
||||
"""Call torch._foreach_copy_ grouped by (dst_dtype, src_dtype) pairs
|
||||
(a single foreach call requires a uniform dtype pair)."""
|
||||
|
||||
def _foreach_copy(
|
||||
group_dsts: List[torch.Tensor], group_srcs: List[torch.Tensor]
|
||||
) -> None:
|
||||
if _has_foreach_copy:
|
||||
torch._foreach_copy_(group_dsts, group_srcs)
|
||||
else:
|
||||
for dst, src in zip(group_dsts, group_srcs):
|
||||
dst.copy_(src)
|
||||
|
||||
groups: Dict[Tuple[torch.dtype, torch.dtype], Tuple[List, List]] = {}
|
||||
for dst, src in zip(dsts, srcs):
|
||||
key = (dst.dtype, src.dtype)
|
||||
if key not in groups:
|
||||
groups[key] = ([], [])
|
||||
groups[key][0].append(dst)
|
||||
groups[key][1].append(src)
|
||||
for group_dsts, group_srcs in groups.values():
|
||||
_foreach_copy(group_dsts, group_srcs)
|
||||
|
||||
|
||||
class PaddingPolicy(Enum):
|
||||
"""How to handle ``raw_n < padded_n`` for a slot.
|
||||
|
||||
KEEP_PAD — Leave the padded region as-is (caller proves the
|
||||
padded tail will not be read).
|
||||
FILL_SENTINEL — Reset the padded region to ``slot.pad_value`` before
|
||||
copy (e.g. ``seq_lens`` filled with
|
||||
``seq_len_fill_value``).
|
||||
ZERO — Reset the padded region to ``0`` (e.g.
|
||||
``out_cache_loc`` / ``req_pool_indices`` — padded
|
||||
rows must point at slot 0 so dummy attention reads
|
||||
land harmlessly).
|
||||
FOREACH_COPY — Always copy ``raw_n`` from src; padded region is
|
||||
left as whatever the previous replay (or the init
|
||||
zeros) wrote. Caller is responsible for proving
|
||||
safety.
|
||||
FILL_ONCE — Fill the whole buffer to ``pad_value`` once at alloc;
|
||||
never reset per iter (e.g. ``encoder_lens`` init to
|
||||
``encoder_len_fill_value``, copied head-only with the
|
||||
tail kept).
|
||||
"""
|
||||
|
||||
KEEP_PAD = "keep_pad"
|
||||
FILL_SENTINEL = "fill_sentinel"
|
||||
ZERO = "zero"
|
||||
FOREACH_COPY = "foreach_copy"
|
||||
FILL_ONCE = "fill_once"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FillContext:
|
||||
"""Per-iteration shape context passed to ``GraphSlot.post_fill``.
|
||||
|
||||
Carries both the bs-axis and tokens-axis raw/padded counts so a hook can
|
||||
derive values regardless of its own slot's axis — e.g. the padded token
|
||||
count (``padded_num_tokens`` == padded_bs * num_tokens_per_bs), which the
|
||||
global-num-tokens fill and the local-num-token-non-padded transform need.
|
||||
"""
|
||||
|
||||
raw_bs: int
|
||||
padded_bs: int
|
||||
raw_num_tokens: int
|
||||
padded_num_tokens: int
|
||||
# Side inputs that are not ForwardBatch attributes but are needed by a
|
||||
# slot's source_fn — e.g. the pipeline-parallel proxy tensors, which the
|
||||
# replay path receives as a separate argument rather than off the FB.
|
||||
pp_proxy_tensors: Optional[Any] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GraphSlot:
|
||||
"""A single FB-mirrored buffer.
|
||||
|
||||
Each slot mirrors one ``ForwardBatch`` attribute. ``name`` MUST match
|
||||
the FB attribute name so ``fill_from`` can ``getattr(fb, name)`` and
|
||||
``extract_buffer`` can ``setattr`` the view back into a FB replace.
|
||||
|
||||
Fields:
|
||||
name — the FB attribute name mirrored by this slot.
|
||||
shape_fn — ``(max_bs, max_num_tokens) -> shape`` callable
|
||||
used at ``register_slot`` time to allocate the
|
||||
physical buffer.
|
||||
dtype — buffer dtype.
|
||||
axis — ``"bs"`` (slot is sliced ``[:bs]``) or
|
||||
``"tokens"`` (sliced ``[:num_tokens]``) or
|
||||
``"none"`` (no slicing — full buffer always
|
||||
exposed; used for scalar buffers and global
|
||||
counters).
|
||||
device — buffer device. ``None`` means use registry
|
||||
default; can be ``"cpu"`` for slots like
|
||||
``seq_lens_cpu`` that must live on host.
|
||||
padding_policy — see ``PaddingPolicy``.
|
||||
pad_value — sentinel for ``FILL_SENTINEL``.
|
||||
enabled — runtime gate; disabled slots are not allocated
|
||||
and skipped during fill / extract.
|
||||
copy_from_fb — when ``True`` (default), ``fill_from`` copies the
|
||||
same-named FB tensor into the buffer head. Set
|
||||
``False`` for computed slots whose value is not a
|
||||
straight FB copy (e.g. ``global_num_tokens_*``,
|
||||
filled by a ``post_fill`` instead).
|
||||
post_fill — optional ``(buffer, forward_batch, FillContext)
|
||||
-> None`` hook run after the grouped copy. Used for
|
||||
compute-then-write slots (local-num-token-non-padded
|
||||
transform, global-num-tokens fill).
|
||||
slice_fn — optional ``(buffer, padded_n) -> Tensor``
|
||||
override for slots with non-trivial slicing
|
||||
(e.g. ``mrope_positions`` shape ``[3, T]`` is
|
||||
sliced on axis 1 not 0).
|
||||
source_fn — optional ``(forward_batch, FillContext) -> Tensor |
|
||||
None`` override for the copy *source*. When set,
|
||||
``fill_from`` copies ``source_fn(fb, ctx)`` (instead of
|
||||
the same-named FB attribute) into
|
||||
``buffer[:src.shape[0]]`` — a source-length slice for
|
||||
structured / side-sourced fields whose data lives on a
|
||||
nested FB dataclass (``ngram_embedding_info.*``) or an
|
||||
out-of-band argument (``pp_proxy_tensors``, carried on
|
||||
``FillContext``). Returning ``None`` skips the copy for
|
||||
that iteration. Such slots use dotted names and are
|
||||
skipped by ``extract_buffer``.
|
||||
"""
|
||||
|
||||
name: str
|
||||
shape_fn: Callable[[int, int], Tuple[int, ...]]
|
||||
dtype: torch.dtype
|
||||
axis: str = "tokens"
|
||||
device: Optional[torch.device] = None
|
||||
padding_policy: PaddingPolicy = PaddingPolicy.FOREACH_COPY
|
||||
pad_value: Optional[Any] = None
|
||||
enabled: bool = True
|
||||
copy_from_fb: bool = True
|
||||
post_fill: Optional[Callable[[torch.Tensor, ForwardBatch, FillContext], None]] = (
|
||||
None
|
||||
)
|
||||
slice_fn: Optional[Callable[[torch.Tensor, int], torch.Tensor]] = None
|
||||
source_fn: Optional[
|
||||
Callable[[ForwardBatch, FillContext], Optional[torch.Tensor]]
|
||||
] = None
|
||||
|
||||
# runtime
|
||||
buffer: Optional[torch.Tensor] = field(default=None, repr=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.axis not in ("bs", "tokens", "none"):
|
||||
raise ValueError(
|
||||
f"GraphSlot {self.name!r}: axis must be one of "
|
||||
f"'bs'/'tokens'/'none', got {self.axis!r}"
|
||||
)
|
||||
|
||||
def _padded_n(self, padded_bs: int, padded_num_tokens: int) -> int:
|
||||
if self.axis == "bs":
|
||||
return padded_bs
|
||||
if self.axis == "tokens":
|
||||
return padded_num_tokens
|
||||
# axis == "none": no slicing
|
||||
return self.buffer.shape[0] if self.buffer is not None else 0
|
||||
|
||||
def _raw_n(self, raw_bs: int, raw_num_tokens: int) -> int:
|
||||
if self.axis == "bs":
|
||||
return raw_bs
|
||||
if self.axis == "tokens":
|
||||
return raw_num_tokens
|
||||
return self.buffer.shape[0] if self.buffer is not None else 0
|
||||
|
||||
def slice_for(self, padded_bs: int, padded_num_tokens: int) -> torch.Tensor:
|
||||
"""Return the ``[:padded_n]`` slice of the buffer consumed by callers.
|
||||
|
||||
This truncates the (full-length) buffer to the active region for the
|
||||
current iteration — it is a slice, not a tensor reshape.
|
||||
"""
|
||||
if self.buffer is None:
|
||||
raise RuntimeError(f"GraphSlot {self.name!r}: buffer not allocated")
|
||||
if self.slice_fn is not None:
|
||||
return self.slice_fn(
|
||||
self.buffer, self._padded_n(padded_bs, padded_num_tokens)
|
||||
)
|
||||
if self.axis == "none":
|
||||
return self.buffer
|
||||
return self.buffer[: self._padded_n(padded_bs, padded_num_tokens)]
|
||||
|
||||
def reset_padding(self, raw_n: int, padded_n: int) -> None:
|
||||
"""Reset the padded tail according to ``padding_policy``."""
|
||||
if self.buffer is None or raw_n >= padded_n:
|
||||
return
|
||||
if self.padding_policy in (
|
||||
PaddingPolicy.KEEP_PAD,
|
||||
PaddingPolicy.FOREACH_COPY,
|
||||
PaddingPolicy.FILL_ONCE,
|
||||
):
|
||||
return
|
||||
# slice_fn governs non-trivial layouts (e.g. mrope_positions [3, T]);
|
||||
# the pad region is the same axis the slot exposes via slice_for().
|
||||
if self.slice_fn is not None:
|
||||
# slice_fn returns the [:padded_n] portion already; we need the
|
||||
# tail [raw_n:padded_n]. We rely on slice_fn slicing the same
|
||||
# axis used by slice_for(): take the padded slice first, then index
|
||||
# the tail with the standard slice on axis 0 of the result.
|
||||
padded_slice = self.slice_fn(self.buffer, padded_n)
|
||||
tail = (
|
||||
padded_slice[..., raw_n:padded_n]
|
||||
if padded_slice.dim() > 1
|
||||
else padded_slice[raw_n:padded_n]
|
||||
)
|
||||
else:
|
||||
tail = self.buffer[raw_n:padded_n]
|
||||
if self.padding_policy == PaddingPolicy.FILL_SENTINEL:
|
||||
if self.pad_value is None:
|
||||
raise RuntimeError(
|
||||
f"GraphSlot {self.name!r}: FILL_SENTINEL requires pad_value"
|
||||
)
|
||||
tail.fill_(self.pad_value)
|
||||
elif self.padding_policy == PaddingPolicy.ZERO:
|
||||
tail.zero_()
|
||||
|
||||
|
||||
class CudaGraphBufferRegistry:
|
||||
"""FB → graph-resident buffer mirror, shared across eager / capture / replay.
|
||||
|
||||
The registry holds a dict of ``GraphSlot`` instances, each mirroring
|
||||
one ``ForwardBatch`` attribute. Slots are registered up-front (during
|
||||
runner init), allocated at ``register_slot``, then filled per-iter via
|
||||
``fill_from(fb, ...)`` and consumed via ``extract_buffer(template) ->
|
||||
ForwardBatch``. ``fill_from`` issues plain D2D copies on the caller's
|
||||
current stream; cross-stream correctness (stream handoff) is handled by
|
||||
the runners, not here.
|
||||
|
||||
Backend-private buffers (kernel workspace, derived page tables) are
|
||||
NOT managed here — backends keep them on ``self.cuda_graph_*`` and
|
||||
allocate via ``AttentionBackend.init_cuda_graph_state(...)``.
|
||||
|
||||
Usage::
|
||||
|
||||
registry = CudaGraphBufferRegistry(device=..., max_bs=..., max_num_tokens=...)
|
||||
registry.register_slot(GraphSlot(name="input_ids", ...))
|
||||
registry.register_slot(GraphSlot(name="seq_lens",
|
||||
padding_policy=PaddingPolicy.FILL_SENTINEL,
|
||||
pad_value=seq_len_fill_value, ...))
|
||||
# per-iter:
|
||||
registry.fill_from(fb, raw_bs=..., padded_bs=..., raw_num_tokens=...,
|
||||
padded_num_tokens=...)
|
||||
fb_view = registry.extract_buffer(padded_bs=..., padded_num_tokens=...,
|
||||
forward_batch_template=fb)
|
||||
attn_backend.init_forward_metadata(fb_view)
|
||||
model.forward(fb_view.input_ids, fb_view.positions, fb_view)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
device: torch.device,
|
||||
max_bs: int,
|
||||
max_num_tokens: int,
|
||||
share_pool: bool = False,
|
||||
) -> None:
|
||||
self.device = device
|
||||
self.max_bs = max_bs
|
||||
self.max_num_tokens = max_num_tokens
|
||||
# Coalesce allocated slot buffers through the global pool; only applies
|
||||
# when allocating (bind/source bypasses the pool).
|
||||
self.share_pool = share_pool
|
||||
self._slots: Dict[str, GraphSlot] = {}
|
||||
|
||||
# ---- registration ------------------------------------------------------
|
||||
|
||||
def register_slot(
|
||||
self, slot: GraphSlot, bind: Optional[torch.Tensor] = None
|
||||
) -> GraphSlot:
|
||||
"""Register a slot and allocate (or adopt) its physical buffer.
|
||||
|
||||
If ``bind`` is given, the slot adopts that existing tensor instead of
|
||||
allocating a fresh one (and skips the pool / sentinel init — the bound
|
||||
tensor is assumed already initialized). This lets a registry share
|
||||
storage with the legacy ``DecodeInputBuffers`` by adopting its fields,
|
||||
guaranteeing a stable, identical ``data_ptr`` for capture vs replay.
|
||||
|
||||
Returns the slot for caller convenience. Re-registering an existing
|
||||
name raises.
|
||||
"""
|
||||
if slot.name in self._slots:
|
||||
raise ValueError(
|
||||
f"GraphSlot {slot.name!r} already registered; "
|
||||
"use enable()/disable() to gate per-iter."
|
||||
)
|
||||
if not slot.enabled:
|
||||
# Even when disabled, keep the spec so callers can introspect
|
||||
# by name; just don't allocate.
|
||||
self._slots[slot.name] = slot
|
||||
return slot
|
||||
shape = slot.shape_fn(self.max_bs, self.max_num_tokens)
|
||||
device = slot.device if slot.device is not None else self.device
|
||||
if bind is not None:
|
||||
expected = tuple(shape)
|
||||
if tuple(bind.shape) != expected:
|
||||
raise ValueError(
|
||||
f"bind tensor for slot {slot.name!r} has shape "
|
||||
f"{tuple(bind.shape)}, expected {expected}."
|
||||
)
|
||||
if bind.dtype != slot.dtype:
|
||||
raise ValueError(
|
||||
f"bind tensor for slot {slot.name!r} has dtype {bind.dtype}, "
|
||||
f"expected {slot.dtype}."
|
||||
)
|
||||
slot.buffer = bind
|
||||
self._slots[slot.name] = slot
|
||||
return slot
|
||||
buffer = torch.zeros(shape, dtype=slot.dtype, device=device)
|
||||
if self.share_pool:
|
||||
# Coalesce with any same-named buffer (e.g. the legacy
|
||||
# DecodeInputBuffers field) so capture and replay see one
|
||||
# physical allocation with a stable data_ptr.
|
||||
buffer = share_input_buffer(slot.name, buffer)
|
||||
if (
|
||||
slot.padding_policy
|
||||
in (PaddingPolicy.FILL_SENTINEL, PaddingPolicy.FILL_ONCE)
|
||||
and slot.pad_value is not None
|
||||
):
|
||||
buffer.fill_(slot.pad_value)
|
||||
slot.buffer = buffer
|
||||
self._slots[slot.name] = slot
|
||||
return slot
|
||||
|
||||
def has_slot(self, name: str) -> bool:
|
||||
return name in self._slots and self._slots[name].enabled
|
||||
|
||||
def get_slot(self, name: str) -> GraphSlot:
|
||||
return self._slots[name]
|
||||
|
||||
def slot_names(self) -> List[str]:
|
||||
return [name for name, s in self._slots.items() if s.enabled]
|
||||
|
||||
# ---- per-iter ----------------------------------------------------------
|
||||
|
||||
def fill_from(
|
||||
self,
|
||||
forward_batch: ForwardBatch,
|
||||
*,
|
||||
raw_bs: int,
|
||||
padded_bs: int,
|
||||
raw_num_tokens: int,
|
||||
padded_num_tokens: int,
|
||||
pp_proxy_tensors: Optional[Any] = None,
|
||||
) -> None:
|
||||
"""Copy FB → registry buffers.
|
||||
|
||||
Phase 1 — reset the padded tail per slot ``padding_policy``.
|
||||
Phase 2 — grouped D2D copy of all enabled slots from FB (or from a
|
||||
slot's ``source_fn`` for structured / side-sourced fields).
|
||||
Phase 3 — run ``post_fill`` hooks for slots that need
|
||||
post-copy transforms.
|
||||
|
||||
``pp_proxy_tensors`` is the out-of-band pipeline-parallel input; it is
|
||||
not an FB attribute, so it reaches ``source_fn`` slots via
|
||||
``FillContext.pp_proxy_tensors``.
|
||||
|
||||
Slots whose FB attribute (or ``source_fn`` result) is ``None`` are
|
||||
silently skipped (the FB doesn't carry that field for the current
|
||||
request).
|
||||
"""
|
||||
ctx = FillContext(
|
||||
raw_bs=raw_bs,
|
||||
padded_bs=padded_bs,
|
||||
raw_num_tokens=raw_num_tokens,
|
||||
padded_num_tokens=padded_num_tokens,
|
||||
pp_proxy_tensors=pp_proxy_tensors,
|
||||
)
|
||||
|
||||
# Phase 1: reset padded regions where it matters.
|
||||
for slot in self._slots.values():
|
||||
if not slot.enabled or slot.buffer is None:
|
||||
continue
|
||||
raw_n = slot._raw_n(raw_bs, raw_num_tokens)
|
||||
padded_n = slot._padded_n(padded_bs, padded_num_tokens)
|
||||
slot.reset_padding(raw_n, padded_n)
|
||||
|
||||
# Phase 2: collect (dst, src) pairs and dispatch a grouped copy.
|
||||
gpu_dsts: List[torch.Tensor] = []
|
||||
gpu_srcs: List[torch.Tensor] = []
|
||||
cpu_dsts: List[torch.Tensor] = []
|
||||
cpu_srcs: List[torch.Tensor] = []
|
||||
for slot in self._slots.values():
|
||||
if not slot.enabled or slot.buffer is None or not slot.copy_from_fb:
|
||||
continue
|
||||
if slot.source_fn is not None:
|
||||
# Structured / side-sourced slot: source comes from a nested FB
|
||||
# dataclass or an out-of-band input, and the copy is sliced to
|
||||
# the source's own length rather than a bs/tokens axis.
|
||||
src = slot.source_fn(forward_batch, ctx)
|
||||
if src is None:
|
||||
continue
|
||||
dst = slot.buffer[: src.shape[0]]
|
||||
else:
|
||||
src = getattr(forward_batch, slot.name, None)
|
||||
if src is None:
|
||||
continue
|
||||
if not isinstance(src, torch.Tensor):
|
||||
# Non-tensor FB fields (e.g. dicts, dataclasses) are not
|
||||
# auto-copied — caller handles via source_fn or post_fill.
|
||||
continue
|
||||
raw_n = slot._raw_n(raw_bs, raw_num_tokens)
|
||||
if slot.slice_fn is not None:
|
||||
dst = slot.slice_fn(slot.buffer, raw_n)
|
||||
elif slot.axis == "none":
|
||||
dst = slot.buffer
|
||||
else:
|
||||
dst = slot.buffer[:raw_n]
|
||||
# foreach_copy_ requires same-device tensors per call — bucket
|
||||
# by device.
|
||||
if dst.device.type == "cpu":
|
||||
cpu_dsts.append(dst)
|
||||
cpu_srcs.append(src)
|
||||
else:
|
||||
gpu_dsts.append(dst)
|
||||
gpu_srcs.append(src)
|
||||
if gpu_dsts:
|
||||
_grouped_foreach_copy_(gpu_dsts, gpu_srcs)
|
||||
for dst, src in zip(cpu_dsts, cpu_srcs):
|
||||
dst.copy_(src)
|
||||
|
||||
# Phase 3: post-fill hooks (compute-then-write slots).
|
||||
for slot in self._slots.values():
|
||||
if not slot.enabled or slot.buffer is None or slot.post_fill is None:
|
||||
continue
|
||||
slot.post_fill(slot.buffer, forward_batch, ctx)
|
||||
|
||||
def extract_buffer(
|
||||
self,
|
||||
*,
|
||||
padded_bs: int,
|
||||
padded_num_tokens: int,
|
||||
forward_batch_template: ForwardBatch,
|
||||
) -> ForwardBatch:
|
||||
"""Return a FB view (``dataclasses.replace`` of ``forward_batch_template``)
|
||||
whose slot fields are buffer views and whose non-slot fields are carried
|
||||
from the template. A plain copy slot whose FB field is ``None`` this iter
|
||||
is carried (not exposed as a stale buffer); computed slots are always
|
||||
exposed.
|
||||
"""
|
||||
import dataclasses
|
||||
|
||||
replace_kwargs: Dict[str, Any] = {"batch_size": padded_bs}
|
||||
for slot in self._slots.values():
|
||||
if not slot.enabled or slot.buffer is None:
|
||||
continue
|
||||
# Structured slots use dotted names ("<field>.<sub>") and are not
|
||||
# top-level FB attributes — their data is consumed in place off the
|
||||
# adopted backing object, not re-attached to the FB view here.
|
||||
if "." in slot.name:
|
||||
continue
|
||||
is_computed = slot.post_fill is not None or not slot.copy_from_fb
|
||||
if (
|
||||
not is_computed
|
||||
and slot.source_fn is None
|
||||
and getattr(forward_batch_template, slot.name, None) is None
|
||||
):
|
||||
# Absent this iter (fill_from skipped it): carry the template.
|
||||
continue
|
||||
replace_kwargs[slot.name] = slot.slice_for(padded_bs, padded_num_tokens)
|
||||
return dataclasses.replace(forward_batch_template, **replace_kwargs)
|
||||
|
||||
|
||||
def build_decode_registry(
|
||||
*,
|
||||
device: torch.device,
|
||||
max_bs: int,
|
||||
max_num_token: int,
|
||||
seq_len_fill_value: int,
|
||||
cache_loc_dtype: torch.dtype,
|
||||
enable_mamba_track: bool = False,
|
||||
is_encoder_decoder: bool = False,
|
||||
encoder_len_fill_value: int = 0,
|
||||
encoder_lens_dtype: torch.dtype = torch.int32,
|
||||
enable_num_token_non_padded: bool = False,
|
||||
require_gathered_buffer: bool = False,
|
||||
enable_prefill_cp: bool = False,
|
||||
require_mlp_tp_gather: bool = False,
|
||||
dp_size: int = 1,
|
||||
register_global_num_tokens: bool = True,
|
||||
share_pool: bool = True,
|
||||
source: Optional[Any] = None,
|
||||
) -> CudaGraphBufferRegistry:
|
||||
"""Registry mirroring the always-on (+ mamba / mrope) FB-shared decode
|
||||
buffers, with the per-slot padding policy that resets the padded tail on
|
||||
each replay:
|
||||
|
||||
- ``seq_lens`` / ``seq_lens_cpu`` -> FILL_SENTINEL(seq_len_fill_value)
|
||||
- ``req_pool_indices`` / ``out_cache_loc`` / ``mamba_track_*`` -> ZERO
|
||||
- ``positions`` / ``mrope_positions`` -> ZERO: the flashinfer verify-path
|
||||
plan reads the padded tail, so leaving stale out-of-range values there
|
||||
triggers an illegal memory access (issue #24361).
|
||||
- ``input_ids`` -> FOREACH_COPY: head ``[:raw_n]`` is overwritten by the
|
||||
copy and the padded tail is not read.
|
||||
|
||||
``custom_mask`` / ``next_token_logits_buffer`` / ``input_embeds`` are not
|
||||
registered here — they are not per-replay FB copies (allocated and written
|
||||
elsewhere), so the runner keeps owning them.
|
||||
|
||||
When ``source`` is given (the decode buffer namespace from
|
||||
``_allocate_decode_buffers``), each slot adopts the same-named tensor off
|
||||
it instead of allocating, so the registry shares one physical allocation
|
||||
with that object. With ``source=None`` the registry allocates its own.
|
||||
"""
|
||||
reg = CudaGraphBufferRegistry(
|
||||
device=device,
|
||||
max_bs=max_bs,
|
||||
max_num_tokens=max_num_token,
|
||||
share_pool=share_pool,
|
||||
)
|
||||
|
||||
def _tokens(_bs: int, mt: int) -> Tuple[int, ...]:
|
||||
return (mt,)
|
||||
|
||||
def _bs(bs: int, _mt: int) -> Tuple[int, ...]:
|
||||
return (bs,)
|
||||
|
||||
slots = [
|
||||
GraphSlot("input_ids", _tokens, torch.int64, axis="tokens"),
|
||||
GraphSlot(
|
||||
"positions",
|
||||
_tokens,
|
||||
torch.int64,
|
||||
axis="tokens",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
),
|
||||
GraphSlot(
|
||||
"out_cache_loc",
|
||||
_tokens,
|
||||
cache_loc_dtype,
|
||||
axis="tokens",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
),
|
||||
GraphSlot(
|
||||
"req_pool_indices",
|
||||
_bs,
|
||||
torch.int64,
|
||||
axis="bs",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
),
|
||||
GraphSlot(
|
||||
"seq_lens",
|
||||
_bs,
|
||||
torch.int64,
|
||||
axis="bs",
|
||||
padding_policy=PaddingPolicy.FILL_SENTINEL,
|
||||
pad_value=seq_len_fill_value,
|
||||
),
|
||||
GraphSlot(
|
||||
"seq_lens_cpu",
|
||||
_bs,
|
||||
torch.int64,
|
||||
axis="bs",
|
||||
device=torch.device("cpu"),
|
||||
padding_policy=PaddingPolicy.FILL_SENTINEL,
|
||||
pad_value=seq_len_fill_value,
|
||||
),
|
||||
GraphSlot(
|
||||
"mrope_positions",
|
||||
lambda _bs2, mt: (3, mt),
|
||||
torch.int64,
|
||||
axis="tokens",
|
||||
slice_fn=lambda buf, n: buf[:, :n],
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
),
|
||||
]
|
||||
if enable_mamba_track:
|
||||
slots.append(
|
||||
GraphSlot(
|
||||
"mamba_track_indices",
|
||||
_bs,
|
||||
torch.int64,
|
||||
axis="bs",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
)
|
||||
)
|
||||
slots.append(
|
||||
GraphSlot(
|
||||
"mamba_track_mask",
|
||||
_bs,
|
||||
torch.bool,
|
||||
axis="bs",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
)
|
||||
)
|
||||
if is_encoder_decoder:
|
||||
# Initialized once to encoder_len_fill_value, copied head-only, never
|
||||
# reset per iter — matching the legacy DecodeInputBuffers behavior.
|
||||
slots.append(
|
||||
GraphSlot(
|
||||
"encoder_lens",
|
||||
_bs,
|
||||
encoder_lens_dtype,
|
||||
axis="bs",
|
||||
padding_policy=PaddingPolicy.FILL_ONCE,
|
||||
pad_value=encoder_len_fill_value,
|
||||
)
|
||||
)
|
||||
if enable_num_token_non_padded:
|
||||
from sglang.srt.model_executor.forward_batch_info import (
|
||||
compute_local_num_token_non_padded,
|
||||
)
|
||||
|
||||
def _num_token_non_padded_post_fill(buf, fb, ctx):
|
||||
# Gathered (DP) path overwrites the plain FB copy with this rank's
|
||||
# local count; the non-gathered path keeps the copied value.
|
||||
if require_gathered_buffer and not enable_prefill_cp:
|
||||
buf.copy_(
|
||||
compute_local_num_token_non_padded(
|
||||
global_num_token_non_padded=fb.num_token_non_padded,
|
||||
num_tokens_per_dp=ctx.padded_num_tokens,
|
||||
)
|
||||
)
|
||||
|
||||
slots.append(
|
||||
GraphSlot(
|
||||
"num_token_non_padded",
|
||||
lambda _bs, _mt: (1,),
|
||||
torch.int32,
|
||||
axis="none",
|
||||
post_fill=_num_token_non_padded_post_fill,
|
||||
)
|
||||
)
|
||||
|
||||
# Computed slots, always exposed by extract_buffer; callers that already set
|
||||
# global_num_tokens_* on the batch pass register_global_num_tokens=False.
|
||||
if register_global_num_tokens:
|
||||
|
||||
def _global_num_tokens_post_fill(buf, fb, ctx):
|
||||
# Only the gathered (DP) path writes a value; otherwise left as init.
|
||||
if require_gathered_buffer:
|
||||
buf.fill_(ctx.padded_num_tokens)
|
||||
|
||||
_global_shape = (
|
||||
(lambda _bs, _mt: (dp_size,))
|
||||
if require_mlp_tp_gather
|
||||
else (lambda _bs, _mt: (1,))
|
||||
)
|
||||
for _global_name in (
|
||||
"global_num_tokens_gpu",
|
||||
"global_num_tokens_for_logprob_gpu",
|
||||
):
|
||||
slots.append(
|
||||
GraphSlot(
|
||||
_global_name,
|
||||
_global_shape,
|
||||
torch.int32,
|
||||
axis="none",
|
||||
copy_from_fb=False,
|
||||
post_fill=_global_num_tokens_post_fill,
|
||||
)
|
||||
)
|
||||
|
||||
for slot in slots:
|
||||
bind = None
|
||||
if source is not None:
|
||||
bind = getattr(source, slot.name, None)
|
||||
if bind is None:
|
||||
raise ValueError(
|
||||
f"source is missing buffer {slot.name!r} required by the "
|
||||
"decode registry; cannot adopt."
|
||||
)
|
||||
reg.register_slot(slot, bind=bind)
|
||||
|
||||
# Structured slots whose backing storage still lives on the source object
|
||||
# (adopt-only during migration): registered only when the source actually
|
||||
# carries them. The per-replay copy source is a nested FB dataclass field,
|
||||
# supplied via source_fn; head is copied (source-length slice), tail kept.
|
||||
if source is not None:
|
||||
ngram = getattr(source, "ngram_embedding_info", None)
|
||||
if ngram is not None:
|
||||
|
||||
def _ngram_source(attr):
|
||||
def _fn(fb, _ctx):
|
||||
info = getattr(fb, "ngram_embedding_info", None)
|
||||
return None if info is None else getattr(info, attr)
|
||||
|
||||
return _fn
|
||||
|
||||
for _attr in ("column_starts", "req_lens"):
|
||||
backing = getattr(ngram, _attr)
|
||||
reg.register_slot(
|
||||
GraphSlot(
|
||||
name=f"ngram_embedding_info.{_attr}",
|
||||
shape_fn=lambda _bs, _mt, _s=tuple(backing.shape): _s,
|
||||
dtype=backing.dtype,
|
||||
axis="none",
|
||||
padding_policy=PaddingPolicy.KEEP_PAD,
|
||||
source_fn=_ngram_source(_attr),
|
||||
),
|
||||
bind=backing,
|
||||
)
|
||||
|
||||
# Pipeline-parallel proxy tensors: a dict of per-key buffers, sourced
|
||||
# from the out-of-band pp input on FillContext rather than the FB.
|
||||
pp = getattr(source, "pp_proxy_tensors", None)
|
||||
if pp is not None:
|
||||
|
||||
def _pp_source(key):
|
||||
def _fn(_fb, ctx):
|
||||
ppx = ctx.pp_proxy_tensors
|
||||
return None if ppx is None else ppx.tensors[key]
|
||||
|
||||
return _fn
|
||||
|
||||
for _key, _backing in pp.items():
|
||||
reg.register_slot(
|
||||
GraphSlot(
|
||||
name=f"pp_proxy_tensors.{_key}",
|
||||
shape_fn=lambda _bs, _mt, _s=tuple(_backing.shape): _s,
|
||||
dtype=_backing.dtype,
|
||||
axis="none",
|
||||
padding_policy=PaddingPolicy.KEEP_PAD,
|
||||
source_fn=_pp_source(_key),
|
||||
),
|
||||
bind=_backing,
|
||||
)
|
||||
|
||||
# KV-canary id buffers (off by default): plain bs-axis FB copies,
|
||||
# adopt-only when the source carries them. Head [:raw_bs] is copied;
|
||||
# the tail keeps its init (rids_int 0, bootstrap_room_ids_int -1).
|
||||
for _cname in ("rids_int", "bootstrap_room_ids_int"):
|
||||
canary = getattr(source, _cname, None)
|
||||
if canary is not None:
|
||||
reg.register_slot(
|
||||
GraphSlot(
|
||||
name=_cname,
|
||||
shape_fn=lambda _bs, _mt, _s=tuple(canary.shape): _s,
|
||||
dtype=canary.dtype,
|
||||
axis="bs",
|
||||
),
|
||||
bind=canary,
|
||||
)
|
||||
|
||||
return reg
|
||||
|
||||
|
||||
def build_prefill_registry(
|
||||
*,
|
||||
device: torch.device,
|
||||
max_bs: int,
|
||||
max_num_token: int,
|
||||
cache_loc_dtype: torch.dtype,
|
||||
is_multimodal: bool = False,
|
||||
hidden_size: int = 0,
|
||||
embed_dtype: Optional[torch.dtype] = None,
|
||||
enable_mamba_track: bool = False,
|
||||
enable_num_token_non_padded: bool = False,
|
||||
register_input_embeds: bool = True,
|
||||
share_pool: bool = True,
|
||||
source: Optional[Any] = None,
|
||||
) -> CudaGraphBufferRegistry:
|
||||
"""Registry mirroring the **token-axis** FB-shared buffers for the
|
||||
piecewise / breakable (prefill) cuda-graph runners.
|
||||
|
||||
``register_input_embeds`` (default ``True``) registers the multimodal
|
||||
``input_embeds`` slot; the eager extend path passes ``False`` so it is
|
||||
carried from the batch (a read input) rather than written in-graph.
|
||||
|
||||
Padding policies match the inline copy/zero in
|
||||
``PiecewiseCudaGraphRunner.load_batch``: ``input_ids`` / ``positions``
|
||||
/ ``out_cache_loc`` / ``mrope_positions`` / ``input_embeds`` reset their
|
||||
padded tail ``[raw_num_tokens:padded_num_tokens]`` to ``0`` (the padded
|
||||
tokens *are* processed by the graph, so they must be benign), then the head
|
||||
``[:raw_num_tokens]`` is copied from the FB. ``input_embeds`` is not an FB
|
||||
copy — the model writes the embeds into it inside the graph — so it is
|
||||
reset-only (``copy_from_fb=False``). ``mamba_track_*`` are bs-axis copies
|
||||
with no padding reset (bs is not padded on this path).
|
||||
|
||||
The piecewise / breakable runners pass ``source=None``, so the registry
|
||||
allocates (and owns) these buffers directly; ``share_pool`` then coalesces
|
||||
them through the process-wide pool. (A ``source`` object, if given, would be
|
||||
adopted instead — one shared allocation with stable ``data_ptr``.)
|
||||
"""
|
||||
reg = CudaGraphBufferRegistry(
|
||||
device=device,
|
||||
max_bs=max_bs,
|
||||
max_num_tokens=max_num_token,
|
||||
share_pool=share_pool,
|
||||
)
|
||||
|
||||
def _tokens(_bs: int, mt: int) -> Tuple[int, ...]:
|
||||
return (mt,)
|
||||
|
||||
def _bs(bs: int, _mt: int) -> Tuple[int, ...]:
|
||||
return (bs,)
|
||||
|
||||
slots = [
|
||||
GraphSlot(
|
||||
"input_ids",
|
||||
_tokens,
|
||||
torch.int64,
|
||||
axis="tokens",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
),
|
||||
GraphSlot(
|
||||
"positions",
|
||||
_tokens,
|
||||
torch.int64,
|
||||
axis="tokens",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
),
|
||||
GraphSlot(
|
||||
"out_cache_loc",
|
||||
_tokens,
|
||||
cache_loc_dtype,
|
||||
axis="tokens",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
),
|
||||
]
|
||||
if is_multimodal:
|
||||
slots.append(
|
||||
GraphSlot(
|
||||
"mrope_positions",
|
||||
lambda _bs2, mt: (3, mt),
|
||||
torch.int64,
|
||||
axis="tokens",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
slice_fn=lambda buf, n: buf[:, :n],
|
||||
)
|
||||
)
|
||||
if register_input_embeds:
|
||||
slots.append(
|
||||
GraphSlot(
|
||||
"input_embeds",
|
||||
lambda _bs2, mt: (mt, hidden_size),
|
||||
embed_dtype,
|
||||
axis="tokens",
|
||||
padding_policy=PaddingPolicy.ZERO,
|
||||
copy_from_fb=False,
|
||||
)
|
||||
)
|
||||
if enable_mamba_track:
|
||||
slots.append(GraphSlot("mamba_track_indices", _bs, torch.int64, axis="bs"))
|
||||
slots.append(GraphSlot("mamba_track_mask", _bs, torch.bool, axis="bs"))
|
||||
slots.append(GraphSlot("mamba_track_seqlens", _bs, torch.int32, axis="bs"))
|
||||
if enable_num_token_non_padded:
|
||||
slots.append(
|
||||
GraphSlot(
|
||||
"num_token_non_padded",
|
||||
lambda _bs2, _mt: (1,),
|
||||
torch.int32,
|
||||
axis="none",
|
||||
)
|
||||
)
|
||||
|
||||
for slot in slots:
|
||||
bind = None
|
||||
if source is not None:
|
||||
bind = getattr(source, slot.name, None)
|
||||
if bind is None:
|
||||
raise ValueError(
|
||||
f"source is missing buffer {slot.name!r} required by the "
|
||||
"prefill registry; cannot adopt."
|
||||
)
|
||||
reg.register_slot(slot, bind=bind)
|
||||
return reg
|
||||
|
||||
|
||||
def build_eager_registry(
|
||||
*,
|
||||
device: torch.device,
|
||||
max_bs: int,
|
||||
max_num_token: int,
|
||||
cache_loc_dtype: torch.dtype,
|
||||
enable_mamba_track: bool = False,
|
||||
is_encoder_decoder: bool = False,
|
||||
encoder_len_fill_value: int = 0,
|
||||
encoder_lens_dtype: torch.dtype = torch.int32,
|
||||
dp_size: int = 1,
|
||||
) -> CudaGraphBufferRegistry:
|
||||
"""One fixed-max input registry for the ``EagerRunner``, serving BOTH eager
|
||||
decode and eager prefill.
|
||||
|
||||
The decode slot set is a superset of eager prefill's needs (eager prefill
|
||||
carries ``input_embeds`` from the batch and reads the bs-axis fields live),
|
||||
so we reuse it, sized at ``(max_bs, max_num_token)`` where ``max_num_token``
|
||||
is the prefill token ceiling. ``seq_len_fill_value=0`` because eager never
|
||||
pads, so the sentinel tail is never read.
|
||||
|
||||
``share_pool=True`` so same-named / same-size slots coalesce through the
|
||||
process-wide pool. The ``EagerRunner`` is built before the cuda-graph runners
|
||||
(see ``ModelRunner.init_backends``), so its (largest) allocations are
|
||||
canonical and the cg runners' matching slots (prefill's token-axis at
|
||||
``max_num_token``, decode's bs-axis at ``max_bs``) adopt them.
|
||||
"""
|
||||
return build_decode_registry(
|
||||
device=device,
|
||||
max_bs=max_bs,
|
||||
max_num_token=max_num_token,
|
||||
seq_len_fill_value=0,
|
||||
cache_loc_dtype=cache_loc_dtype,
|
||||
enable_mamba_track=enable_mamba_track,
|
||||
is_encoder_decoder=is_encoder_decoder,
|
||||
encoder_len_fill_value=encoder_len_fill_value,
|
||||
encoder_lens_dtype=encoder_lens_dtype,
|
||||
enable_num_token_non_padded=False,
|
||||
register_global_num_tokens=False,
|
||||
require_gathered_buffer=False,
|
||||
require_mlp_tp_gather=False,
|
||||
dp_size=dp_size,
|
||||
share_pool=True,
|
||||
source=None,
|
||||
)
|
||||
@@ -0,0 +1,245 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""Phase / backend identifiers, the canonical default for
|
||||
cuda_graph_config, and the --cuda-graph-config JSON CLI parser.
|
||||
|
||||
Module-level imports are pure stdlib — no torch / sglang.srt deps — so
|
||||
ServerArgs can import everything here without pulling in backend
|
||||
classes. check_cuda_graph_backend lazy-imports get_server_args
|
||||
inside the function body to preserve that invariant.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
class Phase:
|
||||
"""The two phases of model forward."""
|
||||
|
||||
DECODE = "decode"
|
||||
PREFILL = "prefill"
|
||||
ALL = (DECODE, PREFILL)
|
||||
|
||||
|
||||
class Backend:
|
||||
"""CUDA graph capture backends a phase can use."""
|
||||
|
||||
FULL = "full"
|
||||
BREAKABLE = "breakable"
|
||||
TC_PIECEWISE = "tc_piecewise"
|
||||
DISABLED = "disabled"
|
||||
ALL = (FULL, BREAKABLE, TC_PIECEWISE, DISABLED)
|
||||
|
||||
|
||||
ALLOWED_BACKENDS_PER_PHASE = {
|
||||
Phase.DECODE: (
|
||||
Backend.FULL,
|
||||
Backend.BREAKABLE,
|
||||
Backend.TC_PIECEWISE,
|
||||
Backend.DISABLED,
|
||||
),
|
||||
# full for prefill captures one whole-forward graph per num_tokens
|
||||
# bucket (bs=1 only); replay pads num_tokens up to the nearest
|
||||
# captured bucket. Opt-in: the padding waste is the operator's call.
|
||||
Phase.PREFILL: (
|
||||
Backend.FULL,
|
||||
Backend.BREAKABLE,
|
||||
Backend.TC_PIECEWISE,
|
||||
Backend.DISABLED,
|
||||
),
|
||||
}
|
||||
|
||||
# Per-phase settings schema. Keys other than backend are runner-level
|
||||
# (read by any backend in that phase); tc_compiler is the lone
|
||||
# backend-specific knob (only meaningful when backend == tc_piecewise).
|
||||
# For prefill, bs carries the captured shape size (token count for
|
||||
# tc_piecewise, request count for breakable) — one shape knob per phase.
|
||||
# full_prefill_max_req is prefill-only and only meaningful when backend == full.
|
||||
ALLOWED_KEYS_PER_PHASE = {
|
||||
Phase.DECODE: ("backend", "max_bs", "bs", "tc_compiler"),
|
||||
Phase.PREFILL: ("backend", "max_bs", "bs", "tc_compiler", "full_prefill_max_req"),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhaseConfig:
|
||||
"""Per-phase CUDA graph settings."""
|
||||
|
||||
backend: str = Backend.DISABLED
|
||||
max_bs: Optional[int] = None
|
||||
bs: Optional[List[int]] = None
|
||||
# Only meaningful when backend == tc_piecewise; ignored otherwise.
|
||||
tc_compiler: str = "eager"
|
||||
# Only meaningful for the prefill phase with backend == full: max number of
|
||||
# request slots baked into each captured graph. Real bs <= full_prefill_max_req
|
||||
# reuses the graph (unused slots become zero-length sentinels); larger
|
||||
# batches fall back to eager. Ignored by BCG (bs=1 only) and TC_PIECEWISE
|
||||
# (bs-invariant via torch.compile). None auto-derives chunked_prefill_size // 512.
|
||||
full_prefill_max_req: Optional[int] = None
|
||||
|
||||
|
||||
def default_prefill_backend() -> str:
|
||||
"""BCG (breakable) is the prefill default on CUDA only; other platforms
|
||||
(HIP/NPU/...) keep tc_piecewise until BCG is validated there. Lazy import
|
||||
keeps this module's stdlib-only import invariant (see module docstring)."""
|
||||
from sglang.srt.utils import is_cuda
|
||||
|
||||
return Backend.BREAKABLE if is_cuda() else Backend.TC_PIECEWISE
|
||||
|
||||
|
||||
@dataclass
|
||||
class CudaGraphConfig:
|
||||
"""Top-level CUDA graph config: one PhaseConfig per phase."""
|
||||
|
||||
decode: PhaseConfig = field(
|
||||
default_factory=lambda: PhaseConfig(backend=Backend.FULL)
|
||||
)
|
||||
prefill: PhaseConfig = field(
|
||||
default_factory=lambda: PhaseConfig(backend=default_prefill_backend())
|
||||
)
|
||||
|
||||
def __getitem__(self, phase: str) -> PhaseConfig:
|
||||
"""Phase-string lookup; kept for migration ergonomics."""
|
||||
if phase not in Phase.ALL:
|
||||
raise KeyError(phase)
|
||||
return getattr(self, phase)
|
||||
|
||||
def to_dict(self) -> Dict[str, Dict[str, Any]]:
|
||||
# Diff-only, not asdict: the parser locks every (phase, key) it sees,
|
||||
# so emitting defaults would lock fields the caller never set.
|
||||
baseline = default_cuda_graph_config()
|
||||
return {
|
||||
Phase.DECODE: _diff_phase(self.decode, baseline.decode),
|
||||
Phase.PREFILL: _diff_phase(self.prefill, baseline.prefill),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: Optional[Dict[str, Dict[str, Any]]]) -> "CudaGraphConfig":
|
||||
"""Build from a (partial) dict of overrides, defaults fill the rest.
|
||||
Unknown phases / keys are silently dropped — the JSON-input
|
||||
validator (parse_cuda_graph_config_arg) rejects them upstream."""
|
||||
cfg = cls()
|
||||
if not raw:
|
||||
return cfg
|
||||
for phase, phase_settings in raw.items():
|
||||
if phase not in Phase.ALL or not isinstance(phase_settings, dict):
|
||||
continue
|
||||
phase_cfg = getattr(cfg, phase)
|
||||
allowed = ALLOWED_KEYS_PER_PHASE[phase]
|
||||
for key, value in phase_settings.items():
|
||||
if key in allowed:
|
||||
setattr(phase_cfg, key, value)
|
||||
return cfg
|
||||
|
||||
|
||||
def default_cuda_graph_config() -> CudaGraphConfig:
|
||||
"""Fresh CudaGraphConfig populated with canonical defaults."""
|
||||
return CudaGraphConfig()
|
||||
|
||||
|
||||
def _diff_phase(actual: PhaseConfig, baseline: PhaseConfig) -> Dict[str, Any]:
|
||||
"""Return only fields whose value differs from the per-phase default."""
|
||||
return {
|
||||
f.name: getattr(actual, f.name)
|
||||
for f in dataclasses.fields(actual)
|
||||
if getattr(actual, f.name) != getattr(baseline, f.name)
|
||||
}
|
||||
|
||||
|
||||
def check_cuda_graph_backend(phase: str, backend: str) -> bool:
|
||||
"""True if cuda_graph_config[phase].backend == backend on the
|
||||
global server args. Returns False if the global server args have not
|
||||
been initialized yet (e.g. unit tests, early startup)."""
|
||||
from sglang.srt.runtime_context import get_server_args
|
||||
|
||||
try:
|
||||
server_args = get_server_args()
|
||||
except ValueError:
|
||||
return False
|
||||
cfg = server_args.cuda_graph_config
|
||||
if cfg is None or phase not in Phase.ALL:
|
||||
return False
|
||||
return getattr(cfg, phase).backend == backend
|
||||
|
||||
|
||||
def cuda_graph_fully_disabled() -> bool:
|
||||
"""True iff cuda_graph_config has Backend.DISABLED on every phase.
|
||||
|
||||
Use at sites that ask the legacy server_args.disable_cuda_graph
|
||||
question ("no CG anywhere globally") — e.g., preallocating buffers
|
||||
that any captured graph would otherwise reuse, or one-shot init
|
||||
that's a no-op when CG is completely off.
|
||||
"""
|
||||
return check_cuda_graph_backend(
|
||||
Phase.DECODE, Backend.DISABLED
|
||||
) and check_cuda_graph_backend(Phase.PREFILL, Backend.DISABLED)
|
||||
|
||||
|
||||
def parse_cuda_graph_config_arg(raw: str) -> Dict[str, Dict[str, Any]]:
|
||||
"""argparse type for --cuda-graph-config: parse JSON dict of
|
||||
phase → settings dict. Each phase's settings dict is itself validated
|
||||
against ALLOWED_KEYS_PER_PHASE. Returns a plain dict — the
|
||||
precedence pipeline in ServerArgs converts to CudaGraphConfig
|
||||
after merging."""
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
raise argparse.ArgumentTypeError(f"--cuda-graph-config must be JSON: {e}")
|
||||
if not isinstance(parsed, dict):
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"--cuda-graph-config must be a JSON object, got {type(parsed).__name__}"
|
||||
)
|
||||
|
||||
result: Dict[str, Dict[str, Any]] = {}
|
||||
for phase, phase_settings in parsed.items():
|
||||
phase = str(phase)
|
||||
if phase not in Phase.ALL:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"--cuda-graph-config: unknown phase '{phase}', expected one of {Phase.ALL}"
|
||||
)
|
||||
if not isinstance(phase_settings, dict):
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"--cuda-graph-config['{phase}'] must be a JSON object, got "
|
||||
f"{type(phase_settings).__name__}"
|
||||
)
|
||||
allowed = ALLOWED_KEYS_PER_PHASE[phase]
|
||||
result[phase] = {}
|
||||
for key, value in phase_settings.items():
|
||||
if key not in allowed:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"--cuda-graph-config['{phase}']: unknown key '{key}', expected one of {allowed}"
|
||||
)
|
||||
result[phase][key] = value
|
||||
return result
|
||||
|
||||
|
||||
def explicit_keys_in(
|
||||
settings: Optional[Dict[str, Dict[str, Any]]],
|
||||
) -> set:
|
||||
"""Return the set of (phase, key) tuples present in settings
|
||||
(the raw dict form, as it arrives from CLI/SDK). Used by ServerArgs
|
||||
to track keys the user explicitly set so the auto-disable cascade can
|
||||
skip them."""
|
||||
out: set = set()
|
||||
if not settings:
|
||||
return out
|
||||
for phase, phase_settings in settings.items():
|
||||
if not isinstance(phase_settings, dict):
|
||||
continue
|
||||
for key in phase_settings.keys():
|
||||
out.add((phase, key))
|
||||
return out
|
||||
@@ -0,0 +1,225 @@
|
||||
# Mixin class for metadata management of Deepseek MHA forward (chunked prefix cache)
|
||||
# More details can be found in python/sglang/srt/models/deepseek_common/attention_forward_methods/forward_mha.py
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.kvcache.kv_indices import (
|
||||
create_chunked_prefix_cache_kv_indices,
|
||||
create_flashinfer_kv_indices_triton,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.model_executor.forward_context import (
|
||||
get_req_to_token_pool,
|
||||
get_token_to_kv_pool,
|
||||
)
|
||||
|
||||
|
||||
class ForwardBatchDeepSeekMHAMixin:
|
||||
# For MLA chunked prefix cache used in chunked prefill
|
||||
# Tell attention backend whether the kv cache needs to be attended in current pass
|
||||
attn_attend_prefix_cache: Optional[bool] = None
|
||||
# Number of prefix cache chunks
|
||||
num_prefix_chunks: Optional[int] = None
|
||||
# Index of current chunk, used by attention backend
|
||||
prefix_chunk_idx: Optional[int] = None
|
||||
# Maximum number of tokens in each chunk per sequence. Computed from maximum chunk capacity
|
||||
prefix_chunk_len: Optional[int] = None
|
||||
# Start positions of prefix cache for each chunk, (num_prefix_chunks, batch_size)
|
||||
prefix_chunk_starts: Optional[torch.Tensor] = None
|
||||
# Start positions of prefix cache for each chunk, (num_prefix_chunks, batch_size), need prefix_chunk_starts_cpu for dcp all gather kv cache
|
||||
prefix_chunk_starts_cpu: Optional[torch.Tensor] = None
|
||||
# length of prefix cache for each chunk, (num_prefix_chunks, batch_size)
|
||||
prefix_chunk_seq_lens_cpu: Optional[torch.Tensor] = None
|
||||
# Lengths of prefix cache for each chunk, (num_prefix_chunks, batch_size)
|
||||
prefix_chunk_seq_lens: Optional[torch.Tensor] = None
|
||||
# Accumulated lengths of prefix cache for each chunk, (num_prefix_chunks, batch_size + 1)
|
||||
prefix_chunk_cu_seq_lens: Optional[torch.Tensor] = None
|
||||
# Max lengths of prefix cache for each chunk, (num_prefix_chunks,)
|
||||
prefix_chunk_max_seq_lens: Optional[List[int]] = None
|
||||
# Per-chunk flag: True if any sequence has kv_len==0 in that chunk.
|
||||
# Precomputed on CPU to avoid GPU-CPU sync in the hot path.
|
||||
prefix_chunk_has_zero_kv: Optional[List[bool]] = None
|
||||
# Number of tokens in each prefix cache chunk, (num_prefix_chunks,)
|
||||
prefix_chunk_num_tokens: Optional[List[int]] = None
|
||||
# KV Indices for each chunk
|
||||
prefix_chunk_kv_indices: Optional[List[torch.Tensor]] = None
|
||||
# For MLA chunked prefix cache used in chunked prefill
|
||||
# Tell attention backend whether lse needs to be returned
|
||||
mha_return_lse: Optional[bool] = None
|
||||
# Whether to apply MHA_ONE_SHOT forward method
|
||||
mha_one_shot: Optional[bool] = None
|
||||
# KV Indices for MHA_ONE_SHOT forward method
|
||||
mha_one_shot_kv_indices: Optional[torch.Tensor] = None
|
||||
|
||||
def get_max_chunk_capacity(self):
|
||||
return envs.SGLANG_MAX_KV_CHUNK_CAPACITY.get()
|
||||
|
||||
def set_prefix_chunk_idx(self, idx: int):
|
||||
self.prefix_chunk_idx = idx
|
||||
|
||||
def set_attn_attend_prefix_cache(self, attn_attend_prefix_cache: bool):
|
||||
self.attn_attend_prefix_cache = attn_attend_prefix_cache
|
||||
|
||||
def prepare_chunked_kv_indices(self, device: torch.device):
|
||||
self.prefix_chunk_kv_indices = []
|
||||
req_to_token = get_req_to_token_pool().req_to_token
|
||||
for idx in range(self.num_prefix_chunks):
|
||||
chunk_starts = self.prefix_chunk_starts[idx]
|
||||
chunk_seq_lens = self.prefix_chunk_seq_lens[idx]
|
||||
chunk_cu_seq_lens = self.prefix_chunk_cu_seq_lens[idx]
|
||||
num_chunk_tokens = self.prefix_chunk_num_tokens[idx]
|
||||
|
||||
chunk_kv_indices = torch.empty(
|
||||
num_chunk_tokens, dtype=torch.int32, device=device
|
||||
)
|
||||
|
||||
create_chunked_prefix_cache_kv_indices[(self.batch_size,)](
|
||||
req_to_token,
|
||||
self.req_pool_indices,
|
||||
chunk_starts,
|
||||
chunk_seq_lens,
|
||||
chunk_cu_seq_lens,
|
||||
chunk_kv_indices,
|
||||
req_to_token.shape[1],
|
||||
)
|
||||
self.prefix_chunk_kv_indices.append(chunk_kv_indices)
|
||||
|
||||
# Here we suppose the length of each chunk is equal
|
||||
# For example, if we have 4 sequences with prefix length [256, 512, 768, 1024], prefix_chunk_len = 256
|
||||
# num_prefix_chunks = cdiv(1024, 256) = 4
|
||||
# prefix_chunk_starts = [[0, 0, 0, 0], [256, 256, 256, 256], [512, 512, 512, 512], [768, 768, 768, 768]]
|
||||
# prefix_chunk_ends = [[256, 256, 256, 256], [256, 512, 512, 512], [256, 512, 768, 768], [256, 512, 768, 1024]]
|
||||
# prefix_chunk_seq_lens = [[256, 256, 256, 256], [0, 256, 256, 256], [0, 0, 256, 256], [0, 0, 0, 256]]
|
||||
# TODO: Implement a better way to allocate chunk lengths that uses memory spaces more efficiently.
|
||||
def get_prefix_chunk_seq_lens(
|
||||
self, prefix_lens: torch.Tensor, num_prefix_chunks: int, prefix_chunk_len: int
|
||||
):
|
||||
device = prefix_lens.device
|
||||
prefix_chunk_starts = (
|
||||
torch.arange(num_prefix_chunks, device=device, dtype=torch.int32)
|
||||
.unsqueeze(1)
|
||||
.expand(-1, self.batch_size)
|
||||
* prefix_chunk_len
|
||||
)
|
||||
prefix_chunk_ends = torch.min(
|
||||
prefix_lens.unsqueeze(0),
|
||||
prefix_chunk_starts + prefix_chunk_len,
|
||||
).to(torch.int32)
|
||||
|
||||
prefix_chunk_seq_lens = (
|
||||
(prefix_chunk_ends - prefix_chunk_starts).clamp(min=0).to(torch.int32)
|
||||
)
|
||||
|
||||
return prefix_chunk_starts, prefix_chunk_seq_lens
|
||||
|
||||
# Called before each attention module if using chunked kv cache for prefill
|
||||
# Some of the codes are adapted from https://github.com/vllm-project/vllm/blob/main/vllm/v1/attention/backends/mla/common.py
|
||||
def prepare_chunked_prefix_cache_info(self, device: torch.device):
|
||||
|
||||
from sglang.srt.mem_cache.memory_pool import (
|
||||
HybridLinearKVPool,
|
||||
MLATokenToKVPool,
|
||||
)
|
||||
|
||||
token_to_kv_pool = get_token_to_kv_pool()
|
||||
assert isinstance(token_to_kv_pool, MLATokenToKVPool) or (
|
||||
isinstance(token_to_kv_pool, HybridLinearKVPool)
|
||||
and isinstance(token_to_kv_pool.full_kv_pool, MLATokenToKVPool)
|
||||
), "Currently chunked prefix cache can only be used by Deepseek models"
|
||||
|
||||
if not any(self.extend_prefix_lens_cpu):
|
||||
self.num_prefix_chunks = 0
|
||||
return
|
||||
|
||||
if self.prefix_chunk_len is not None:
|
||||
# Chunked kv cache info already prepared by prior modules
|
||||
return
|
||||
|
||||
self.prefix_chunk_idx = -1
|
||||
|
||||
# chunk_capacity is the maximum number of tokens in each chunk
|
||||
chunk_capacity = self.get_max_chunk_capacity()
|
||||
self.prefix_chunk_len = chunk_capacity // self.batch_size
|
||||
|
||||
self.num_prefix_chunks = (
|
||||
max(self.extend_prefix_lens_cpu) + self.prefix_chunk_len - 1
|
||||
) // self.prefix_chunk_len
|
||||
|
||||
# Here we compute chunk lens twice to avoid stream sync, once on gpu and once on cpu.
|
||||
prefix_chunk_starts_cuda, prefix_chunk_seq_lens_cuda = (
|
||||
self.get_prefix_chunk_seq_lens(
|
||||
self.extend_prefix_lens,
|
||||
self.num_prefix_chunks,
|
||||
self.prefix_chunk_len,
|
||||
)
|
||||
)
|
||||
prefix_chunk_starts_cpu, prefix_chunk_seq_lens_cpu = (
|
||||
self.get_prefix_chunk_seq_lens(
|
||||
torch.tensor(self.extend_prefix_lens_cpu),
|
||||
self.num_prefix_chunks,
|
||||
self.prefix_chunk_len,
|
||||
)
|
||||
)
|
||||
self.prefix_chunk_starts = prefix_chunk_starts_cuda
|
||||
self.prefix_chunk_seq_lens = prefix_chunk_seq_lens_cuda
|
||||
|
||||
# set prefix_chunk_starts_cpu and prefix_chunk_seq_lens_cpu for dcp to gather chunk kv cache with arbitrary lens
|
||||
self.prefix_chunk_starts_cpu = prefix_chunk_starts_cpu
|
||||
self.prefix_chunk_seq_lens_cpu = prefix_chunk_seq_lens_cpu
|
||||
# Metadata for attention backend
|
||||
self.prefix_chunk_cu_seq_lens = torch.zeros(
|
||||
self.num_prefix_chunks,
|
||||
self.batch_size + 1,
|
||||
device=device,
|
||||
dtype=torch.int32,
|
||||
)
|
||||
self.prefix_chunk_cu_seq_lens[:, 1:] = prefix_chunk_seq_lens_cuda.cumsum(
|
||||
dim=1
|
||||
).to(torch.int32)
|
||||
self.prefix_chunk_max_seq_lens = prefix_chunk_seq_lens_cpu.max(
|
||||
dim=1
|
||||
).values.tolist()
|
||||
|
||||
self.prefix_chunk_num_tokens = prefix_chunk_seq_lens_cpu.sum(dim=1).tolist()
|
||||
assert max(self.prefix_chunk_num_tokens) <= self.get_max_chunk_capacity()
|
||||
|
||||
# Per-chunk flag: does any sequence have kv_len == 0?
|
||||
# Pure CPU check (prefix_chunk_seq_lens_cpu is on CPU), no GPU sync.
|
||||
self.prefix_chunk_has_zero_kv = [
|
||||
bool((prefix_chunk_seq_lens_cpu[i] == 0).any())
|
||||
for i in range(self.num_prefix_chunks)
|
||||
]
|
||||
|
||||
# Precompute the kv indices for each chunk
|
||||
self.prepare_chunked_kv_indices(device)
|
||||
|
||||
def fetch_mha_one_shot_kv_indices(self):
|
||||
if self.mha_one_shot_kv_indices is not None:
|
||||
return self.mha_one_shot_kv_indices
|
||||
batch_size = self.batch_size
|
||||
paged_kernel_lens_sum = sum(self.seq_lens_cpu)
|
||||
kv_indices = torch.empty(
|
||||
paged_kernel_lens_sum,
|
||||
dtype=torch.int32,
|
||||
device=self.req_pool_indices.device,
|
||||
)
|
||||
kv_indptr = torch.zeros(
|
||||
batch_size + 1,
|
||||
dtype=torch.int32,
|
||||
device=self.req_pool_indices.device,
|
||||
)
|
||||
kv_indptr[1:] = torch.cumsum(self.seq_lens, dim=0)
|
||||
req_to_token = get_req_to_token_pool().req_to_token
|
||||
create_flashinfer_kv_indices_triton[(self.batch_size,)](
|
||||
req_to_token,
|
||||
self.req_pool_indices,
|
||||
self.seq_lens,
|
||||
kv_indptr,
|
||||
None,
|
||||
kv_indices,
|
||||
req_to_token.shape[1],
|
||||
)
|
||||
self.mha_one_shot_kv_indices = kv_indices
|
||||
return kv_indices
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
"""Per-forward-call control context.
|
||||
|
||||
Owns ForwardContext — a frozen dataclass holding control configs the model
|
||||
layer reads at depth via get_forward_context(). The only mandatory field
|
||||
today is attn_backend; pool refs are derived from attn_backend.*
|
||||
(every backend caches them at __init__), so a published ForwardContext
|
||||
is enough to resolve the active pools without a separate global.
|
||||
|
||||
ModelRunner._forward_raw publishes a fresh ForwardContext for the
|
||||
duration of each forward; callers that need a per-call override (PDmux
|
||||
per-stream backend, frozen-KV MTP draft loop, TBO per-child dispatch) use
|
||||
dataclasses.replace and wrap the override scope with forward_context().
|
||||
|
||||
Distinct from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph.TcPiecewiseForwardContext,
|
||||
which collects compilation-time refs for the piecewise CUDA graph backend.
|
||||
|
||||
Concurrency: _current is a plain module-level global, not thread-local.
|
||||
This matches the global_server_args precedent and is safe because each
|
||||
forward runs synchronously on a single Python thread per worker process. If
|
||||
worker threads ever share a process, migrate to contextvars.ContextVar.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
|
||||
from sglang.srt.mem_cache.memory_pool import KVCache, ReqToTokenPool
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ForwardContext:
|
||||
"""Per-forward-call control configs. Read via get_forward_context();
|
||||
extend by adding fields here. Frozen so accidental mutation raises at
|
||||
write time — use dataclasses.replace for per-call overrides."""
|
||||
|
||||
attn_backend: AttentionBackend
|
||||
|
||||
|
||||
_current: Optional[ForwardContext] = None
|
||||
|
||||
|
||||
def set_forward_context(ctx: Optional[ForwardContext]) -> Optional[ForwardContext]:
|
||||
"""Set the active context; return the previous one for explicit
|
||||
save/restore. Prefer the forward_context() context manager."""
|
||||
global _current
|
||||
prev, _current = _current, ctx
|
||||
return prev
|
||||
|
||||
|
||||
def has_forward_context() -> bool:
|
||||
return _current is not None
|
||||
|
||||
|
||||
def get_forward_context() -> ForwardContext:
|
||||
assert _current is not None, (
|
||||
"no forward context active — call forward_context(...) or set_forward_context(...) "
|
||||
"before reading get_forward_context()."
|
||||
)
|
||||
return _current
|
||||
|
||||
|
||||
def get_attn_backend() -> AttentionBackend:
|
||||
return get_forward_context().attn_backend
|
||||
|
||||
|
||||
def get_token_to_kv_pool() -> KVCache:
|
||||
return get_attn_backend().token_to_kv_pool
|
||||
|
||||
|
||||
def get_req_to_token_pool() -> ReqToTokenPool:
|
||||
return get_attn_backend().req_to_token_pool
|
||||
|
||||
|
||||
@contextmanager
|
||||
def forward_context(ctx: ForwardContext):
|
||||
prev = set_forward_context(ctx)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
set_forward_context(prev)
|
||||
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.model_executor.cuda_graph_config import Backend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
|
||||
|
||||
class GraphSharedOutput:
|
||||
"""``(max_rows, vocab)`` logits buffer, shared by every cuda-graph runner."""
|
||||
|
||||
_process_shared: Optional[GraphSharedOutput] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
device: torch.device,
|
||||
max_rows: int,
|
||||
) -> None:
|
||||
self.device = torch.device(device)
|
||||
self.max_rows = max_rows
|
||||
self._logits_buffers: Dict[int, torch.Tensor] = {}
|
||||
|
||||
@classmethod
|
||||
def create_for_model_runner(
|
||||
cls, model_runner: ModelRunner
|
||||
) -> Optional[GraphSharedOutput]:
|
||||
cuda_graph_config = model_runner.server_args.cuda_graph_config
|
||||
if cuda_graph_config is None:
|
||||
return None
|
||||
|
||||
max_rows = 0
|
||||
decode = cuda_graph_config.decode
|
||||
if decode.backend != Backend.DISABLED and decode.bs:
|
||||
max_rows = max(max_rows, model_runner.max_decode_logits_rows())
|
||||
|
||||
if max_rows <= 0:
|
||||
return None
|
||||
|
||||
device = torch.device(model_runner.device)
|
||||
shared = cls._process_shared
|
||||
if (
|
||||
shared is not None
|
||||
and shared.device == device
|
||||
and shared.max_rows >= max_rows
|
||||
):
|
||||
return shared
|
||||
cls._process_shared = cls(device=device, max_rows=max_rows)
|
||||
return cls._process_shared
|
||||
|
||||
def get_logits_buffer(self, vocab_size: int, *, rows: int) -> torch.Tensor:
|
||||
assert rows <= self.max_rows, (
|
||||
f"shared logits buffer holds {self.max_rows} rows but caller "
|
||||
f"needs {rows} (vocab_size={vocab_size})"
|
||||
)
|
||||
buffer = self._logits_buffers.get(vocab_size)
|
||||
if buffer is None:
|
||||
buffer = torch.zeros(
|
||||
(self.max_rows, vocab_size), dtype=torch.float, device=self.device
|
||||
)
|
||||
self._logits_buffers[vocab_size] = buffer
|
||||
return buffer[:rows]
|
||||
@@ -0,0 +1,82 @@
|
||||
import fnmatch
|
||||
import importlib
|
||||
import logging
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
import torch.nn as nn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_forward_hooks(model: nn.Module, hook_specs: List[dict[str, Any]]) -> None:
|
||||
"""
|
||||
hook_specs is a list of dicts from server_args.forward_hooks.
|
||||
Attaches forward hooks to the matching modules.
|
||||
"""
|
||||
name_to_module = dict(model.named_modules())
|
||||
|
||||
for spec in hook_specs:
|
||||
spec_name = spec.get("name", "")
|
||||
target_patterns = spec.get("target_modules", [])
|
||||
if not target_patterns:
|
||||
logger.warning(f"Hook spec '{spec_name}' has no 'target_modules', skipping")
|
||||
continue
|
||||
|
||||
hook_factory_path = spec.get("hook_factory")
|
||||
if not hook_factory_path:
|
||||
logger.warning(f"Hook spec '{spec_name}' has no 'hook_factory', skipping")
|
||||
continue
|
||||
|
||||
config = spec.get("config") or {}
|
||||
hook_factory = resolve_callable(hook_factory_path)
|
||||
|
||||
hook = hook_factory(config) if hook_factory else None
|
||||
if hook is None:
|
||||
logger.warning(
|
||||
f"Hook factory '{hook_factory_path}' for spec '{spec_name}' "
|
||||
"returned None, not registering any hook"
|
||||
)
|
||||
continue
|
||||
|
||||
# Resolve patterns like "model.layers.*.mlp"
|
||||
matched = []
|
||||
for name, module in name_to_module.items():
|
||||
if any(fnmatch.fnmatch(name, pattern) for pattern in target_patterns):
|
||||
matched.append((name, module))
|
||||
|
||||
if not matched:
|
||||
logger.warning(
|
||||
f"No modules matched hook spec '{spec_name}' "
|
||||
f"patterns={target_patterns}"
|
||||
)
|
||||
continue
|
||||
|
||||
for module_name, module in matched:
|
||||
_ = module.register_forward_hook(hook)
|
||||
logger.info(f"Registered forward hook '{spec_name}' " f"on {module_name}")
|
||||
|
||||
|
||||
def resolve_callable(path: Optional[str]) -> Optional[Callable]:
|
||||
if path is None:
|
||||
return None
|
||||
|
||||
if ":" in path:
|
||||
module_name, fn_name = path.split(":", 1)
|
||||
else:
|
||||
parts = path.split(".")
|
||||
if len(parts) < 2:
|
||||
raise ValueError(
|
||||
f"Invalid hook callable path '{path}'. "
|
||||
"Expected 'module.submodule:factory' or 'module.submodule.factory'."
|
||||
)
|
||||
*mod_parts, fn_name = parts
|
||||
module_name = ".".join(mod_parts)
|
||||
|
||||
module = importlib.import_module(module_name)
|
||||
try:
|
||||
return getattr(module, fn_name)
|
||||
except AttributeError as e:
|
||||
raise AttributeError(
|
||||
f"Module '{module_name}' has no attribute '{fn_name}' "
|
||||
f"(from hook path '{path}')"
|
||||
) from e
|
||||
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from dataclasses import dataclass, fields
|
||||
from typing import Dict, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.utils import is_npu
|
||||
|
||||
# Process-wide pool keyed by (name, numel, dtype, device); see share_input_buffer.
|
||||
_PoolKey = Tuple[str, int, torch.dtype, torch.device]
|
||||
_forward_input_buffer_pool: Dict[_PoolKey, torch.Tensor] = {}
|
||||
|
||||
|
||||
def share_input_buffer(name: str, new_buffer: torch.Tensor) -> torch.Tensor:
|
||||
"""Coalesce a buffer by ``(name, size, dtype, device)`` into the
|
||||
process-wide input-buffer pool.
|
||||
|
||||
Distinct callers that request the same field ``name`` with the same
|
||||
size/dtype/device share one physical allocation (and therefore one
|
||||
``data_ptr``): the first registrant's buffer becomes canonical and every
|
||||
later identical request is returned as a view aliased onto it. Requests
|
||||
that differ in size get their own allocation — they never reuse or displace
|
||||
an existing entry — so the sharing *structure* is independent of
|
||||
registration order and no already-captured buffer is ever repointed.
|
||||
|
||||
This pool is process-wide and governs *every* ``share_buffers()`` caller —
|
||||
including graph runners not yet on the registry (the speculative draft /
|
||||
draft-extend / frozen-kv-mtp / multi-layer-eagle runners), which register
|
||||
identically-named ``input_ids`` / ``positions`` / ``out_cache_loc`` /
|
||||
``mrope_positions``. Cross-runner sharing is safe because those buffers are
|
||||
filled immediately before each replay and the forwards that use them are
|
||||
sequential / mutually exclusive.
|
||||
"""
|
||||
key: _PoolKey = (name, new_buffer.numel(), new_buffer.dtype, new_buffer.device)
|
||||
canonical = _forward_input_buffer_pool.get(key, None)
|
||||
if canonical is None:
|
||||
_forward_input_buffer_pool[key] = new_buffer
|
||||
canonical = new_buffer
|
||||
return canonical.as_strided(new_buffer.size(), new_buffer.stride())
|
||||
|
||||
|
||||
def share_input_buffers_in(obj) -> None:
|
||||
"""Pool every tensor buffer on ``obj`` (dataclass / ``SimpleNamespace``)
|
||||
through the process-wide pool, in place. No-op on NPU; recurses into dict /
|
||||
dataclass buffer fields (``pp_proxy_tensors`` / ``ngram_embedding_info``)."""
|
||||
if is_npu():
|
||||
return
|
||||
|
||||
for name, buffer in list(vars(obj).items()):
|
||||
if buffer is None:
|
||||
continue
|
||||
if dataclasses.is_dataclass(buffer):
|
||||
buffer = vars(buffer)
|
||||
if isinstance(buffer, dict):
|
||||
for sub_name, sub_buffer in buffer.items():
|
||||
assert isinstance(
|
||||
sub_buffer, torch.Tensor
|
||||
), f"Field {name}.{sub_name} is expected to be a torch.Tensor, but got {type(sub_buffer)}."
|
||||
buffer[sub_name] = share_input_buffer(f"{name}.{sub_name}", sub_buffer)
|
||||
else:
|
||||
assert isinstance(
|
||||
buffer, torch.Tensor
|
||||
), f"Field {name} is expected to be a torch.Tensor, a dict of torch.Tensor, or a dataclass of torch.Tensor, but got {type(buffer)}."
|
||||
setattr(obj, name, share_input_buffer(name, buffer))
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForwardInputBuffers:
|
||||
|
||||
def _share_one_buffer(self, name: str, new_buffer: torch.Tensor) -> torch.Tensor:
|
||||
return share_input_buffer(name, new_buffer)
|
||||
|
||||
def share_buffers(self):
|
||||
# disable share input buffer on npu due to accuracy issue
|
||||
if is_npu():
|
||||
return
|
||||
|
||||
for f in fields(self):
|
||||
name = f.name
|
||||
buffer = getattr(self, name)
|
||||
|
||||
if buffer is None:
|
||||
continue
|
||||
|
||||
if dataclasses.is_dataclass(buffer):
|
||||
buffer = vars(buffer)
|
||||
|
||||
if isinstance(buffer, dict):
|
||||
for sub_name, sub_buffer in buffer.items():
|
||||
assert isinstance(
|
||||
sub_buffer, torch.Tensor
|
||||
), f"Field {name}.{sub_name} is expected to be a torch.Tensor, but got {type(sub_buffer)}."
|
||||
new_buffer = self._share_one_buffer(
|
||||
f"{name}.{sub_name}", sub_buffer
|
||||
)
|
||||
buffer[sub_name] = new_buffer
|
||||
else:
|
||||
assert isinstance(
|
||||
buffer, torch.Tensor
|
||||
), f"Field {name} is expected to be a torch.Tensor, a dict of torch.Tensor, or a dataclass of torch.Tensor, but got {type(buffer)}."
|
||||
new_buffer = self._share_one_buffer(name, buffer)
|
||||
setattr(self, name, new_buffer)
|
||||
@@ -0,0 +1,120 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the SGLang project
|
||||
"""ms_runner launch MindSpore distributed modules."""
|
||||
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import mindspore as ms
|
||||
import torch
|
||||
from mindspore._c_expression import GroupOptions
|
||||
from mindspore.communication import create_group
|
||||
|
||||
from sglang.srt.distributed.parallel_state import _groups
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _Tmp:
|
||||
def __init__(self):
|
||||
self.sched_p = None
|
||||
|
||||
def set_sched_process(self, p):
|
||||
self.sched_p = p
|
||||
|
||||
def __del__(self):
|
||||
if self.sched_p:
|
||||
self.sched_p.kill()
|
||||
|
||||
|
||||
_tmp = _Tmp()
|
||||
|
||||
|
||||
def _get_host_and_ip(distributed_init_method):
|
||||
try:
|
||||
_, ip_str, port_str = distributed_init_method.split(":")
|
||||
ip = ip_str.split("/")[-1]
|
||||
port = int(port_str)
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
"Cannot get host and port information from %s, error: %s!"
|
||||
% (distributed_init_method, str(e))
|
||||
)
|
||||
|
||||
return ip, port
|
||||
|
||||
|
||||
def run_scheduler_init(rank, local_rank, world_size, master_addr, master_port):
|
||||
with open(str(Path() / "schedule.log"), "w") as scheduler_f:
|
||||
# For Python outputs.
|
||||
sys.stdout = scheduler_f
|
||||
sys.stderr = scheduler_f
|
||||
# For C++ outputs.
|
||||
os.dup2(scheduler_f.fileno(), 1)
|
||||
os.dup2(scheduler_f.fileno(), 2)
|
||||
os.environ["DEVICE_ID"] = str(local_rank)
|
||||
os.environ["MS_WORKER_NUM"] = str(world_size)
|
||||
os.environ["MS_ROLE"] = "MS_SCHED"
|
||||
os.environ["MS_NODE_ID"] = str(rank)
|
||||
os.environ["MS_SCHED_HOST"] = str(master_addr)
|
||||
os.environ["MS_SCHED_PORT"] = str(master_port)
|
||||
# This function is blocked until the whole cluster exits.
|
||||
ms.communication.init()
|
||||
|
||||
|
||||
def set_ms_parallel_env(rank, local_rank, world_size, init_method):
|
||||
master_addr, master_port = _get_host_and_ip(init_method)
|
||||
# change port avoiding port conflicts with torch
|
||||
master_port = master_port + 35 if master_port < 65500 else master_port - 35
|
||||
if not os.getenv("MS_ROLE"):
|
||||
if rank == 0:
|
||||
# Create a subprocess for scheduler of MindSpore, just for internal collaboration, not for collective communication
|
||||
sched_p = mp.Process(
|
||||
target=run_scheduler_init,
|
||||
args=(rank, local_rank, world_size, master_addr, master_port),
|
||||
)
|
||||
sched_p.start()
|
||||
global _tmp
|
||||
_tmp.set_sched_process(sched_p)
|
||||
|
||||
os.environ["DEVICE_ID"] = str(local_rank)
|
||||
os.environ["MS_WORKER_NUM"] = str(world_size)
|
||||
os.environ["MS_ROLE"] = "MS_WORKER"
|
||||
os.environ["MS_NODE_ID"] = str(rank)
|
||||
os.environ["MS_SCHED_HOST"] = str(master_addr)
|
||||
os.environ["MS_SCHED_PORT"] = str(master_port)
|
||||
|
||||
|
||||
def reuse_hccl_comm():
|
||||
for group_name, group in _groups.items():
|
||||
# Torch ProcessGroupHccl
|
||||
device_group = group().device_group
|
||||
hccl_comm_handle = device_group._get_backend(torch.device("npu")).get_hccl_comm(
|
||||
group().local_rank
|
||||
)
|
||||
logger.info(
|
||||
f"MindSpore reuse torch group: {device_group}, group_name: {group_name}, local rank: {group().local_rank},"
|
||||
f"hccl communicator handle: {hex(hccl_comm_handle)}",
|
||||
)
|
||||
# Create MS communication group by hccl comm handle to reuse Torch group.
|
||||
group_options = GroupOptions()
|
||||
group_options.hccl_config = {"hccl_comm": hccl_comm_handle}
|
||||
create_group(group_name, group().ranks, group_options)
|
||||
|
||||
|
||||
def init_ms_distributed(world_size, rank, local_rank, server_args, port):
|
||||
if server_args.dist_init_addr:
|
||||
dist_init_method = f"tcp://{server_args.dist_init_addr}"
|
||||
else:
|
||||
dist_init_method = f"tcp://{server_args.host}:{port}"
|
||||
set_ms_parallel_env(rank, local_rank, world_size, dist_init_method)
|
||||
|
||||
ms.set_context(infer_boost="on", jit_level="O0")
|
||||
ms.set_context(mode=ms.context.PYNATIVE_MODE)
|
||||
ms.set_device("Ascend", local_rank)
|
||||
ms.communication.init("hccl")
|
||||
# After distributed job is initialized, reuse hccl comms for MindSpore.
|
||||
reuse_hccl_comm()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
"""Utilities for updating LongCat ngram embedding token tables."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.ngram_embedding import update_token_table
|
||||
|
||||
|
||||
def update_ngram_token_table_after_sampling(
|
||||
*,
|
||||
ngram_embedding_info,
|
||||
next_token_ids: torch.Tensor,
|
||||
req_pool_indices: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
batch_size: int,
|
||||
) -> bool:
|
||||
"""Update the ngram token table with sampled tokens.
|
||||
|
||||
Returns whether the token table was updated.
|
||||
"""
|
||||
skip_token_table_update = ngram_embedding_info.skip_token_table_update
|
||||
if skip_token_table_update is not None:
|
||||
# Skip chunked (not-yet-finished) prefill requests: their sampled token
|
||||
# is a pseudo prediction and must not pollute the token table.
|
||||
indices = (~skip_token_table_update).nonzero(as_tuple=True)[0]
|
||||
if indices.numel() == 0:
|
||||
return False
|
||||
update_token_table(
|
||||
ne_token_table=ngram_embedding_info.token_table,
|
||||
tokens=next_token_ids[indices].to(torch.int32),
|
||||
row_indices=req_pool_indices[indices],
|
||||
column_starts=seq_lens[indices].to(torch.int32),
|
||||
req_lens=torch.ones(
|
||||
indices.numel(), dtype=torch.int32, device=next_token_ids.device
|
||||
),
|
||||
ignore_tokens=None,
|
||||
)
|
||||
return True
|
||||
|
||||
ngram_embedding_info.out_column_starts[:batch_size] = seq_lens
|
||||
ngram_embedding_info.out_req_lens[:batch_size] = 1
|
||||
update_token_table(
|
||||
ne_token_table=ngram_embedding_info.token_table,
|
||||
tokens=next_token_ids.to(torch.int32),
|
||||
row_indices=req_pool_indices,
|
||||
column_starts=ngram_embedding_info.out_column_starts,
|
||||
req_lens=ngram_embedding_info.out_req_lens,
|
||||
ignore_tokens=None,
|
||||
)
|
||||
return True
|
||||
@@ -0,0 +1,794 @@
|
||||
"""Memory pool configurators for profiling and sizing KV cache pools.
|
||||
|
||||
Each model architecture has its own configurator that computes pool sizes
|
||||
from available GPU memory using a unified coeff+bias model:
|
||||
|
||||
available_bytes = max_tokens * coeff + bias
|
||||
max_tokens = (available_bytes - bias) / coeff
|
||||
|
||||
Two entry points, same core computation:
|
||||
- calculate_pool_sizes(available_bytes, page_size): profiling path
|
||||
- calculate_pool_sizes_from_max_tokens(max_tokens, page_size): constraint path
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.configs.model_config import (
|
||||
get_dsa_index_head_dim,
|
||||
get_minimax_sparse_attention_config,
|
||||
get_minimax_sparse_disable_value_layer_ids,
|
||||
get_minimax_sparse_layer_ids,
|
||||
is_deepseek_dsa,
|
||||
is_deepseek_v4,
|
||||
is_minimax_sparse,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.mem_cache.common import get_alloc_len_per_decode
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import get_compress_state_ring_size
|
||||
from sglang.srt.mem_cache.memory_pool import DSATokenToKVPool
|
||||
from sglang.srt.runtime_context import get_parallel
|
||||
from sglang.srt.utils.common import (
|
||||
ceil_align,
|
||||
ceil_div,
|
||||
is_float4_e2m1fn_x2,
|
||||
spec_decode_alloc_len_per_request,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryPoolConfig:
|
||||
"""Resolved memory pool config, shared between target and draft workers."""
|
||||
|
||||
max_total_num_tokens: int
|
||||
max_running_requests: Optional[int] = None
|
||||
full_max_total_num_tokens: Optional[int] = None
|
||||
swa_max_total_num_tokens: Optional[int] = None
|
||||
|
||||
# DSV4 compressed-attention pool sizes (target only; draft workers leave at 0).
|
||||
c4_max_total_num_tokens: int = 0
|
||||
c128_max_total_num_tokens: int = 0
|
||||
c4_state_pool_size: int = 0
|
||||
c128_state_pool_size: int = 0
|
||||
|
||||
mem_fraction_static: Optional[float] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.max_total_num_tokens <= 0:
|
||||
msg = "Not enough memory. Please try to increase --mem-fraction-static."
|
||||
if self.mem_fraction_static is not None:
|
||||
msg += f" Current value: mem_fraction_static={self.mem_fraction_static}"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_dsv4_compress_state_dtype_sizes() -> tuple[int, int]:
|
||||
dtype_name = envs.SGLANG_DSV4_COMPRESS_STATE_DTYPE.get().strip().lower()
|
||||
if dtype_name in ("float32", "fp32"):
|
||||
return 4, 4
|
||||
if dtype_name in ("bfloat16", "bf16"):
|
||||
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
||||
raise ValueError(
|
||||
"SGLANG_DSV4_COMPRESS_STATE_DTYPE=bf16 is not supported when "
|
||||
"SGLANG_OPT_USE_ONLINE_COMPRESS=1; online c128 state must stay float32."
|
||||
)
|
||||
return 2, 2
|
||||
raise ValueError(
|
||||
"Unsupported SGLANG_DSV4_COMPRESS_STATE_DTYPE="
|
||||
f"{dtype_name!r}. Expected one of: float32, fp32, bfloat16, bf16."
|
||||
)
|
||||
|
||||
|
||||
class MemoryPoolConfigurator:
|
||||
"""Base class for memory pool configurators.
|
||||
|
||||
Subclasses compute pool sizes for their architecture via coeff+bias model.
|
||||
Both entry points return MemoryPoolConfig (with max_running_requests=None,
|
||||
to be filled by the consumer).
|
||||
"""
|
||||
|
||||
def calculate_pool_sizes(
|
||||
self, available_bytes: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
"""Profiling path: compute pool sizes from available bytes."""
|
||||
raise NotImplementedError
|
||||
|
||||
def calculate_pool_sizes_from_max_tokens(
|
||||
self, max_total_num_tokens: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
"""Constraint path: recalculate pool sizes from a constrained max_tokens."""
|
||||
raise NotImplementedError
|
||||
|
||||
def finalize_with_max_running_requests(
|
||||
self, config: MemoryPoolConfig
|
||||
) -> MemoryPoolConfig:
|
||||
return config
|
||||
|
||||
|
||||
class DefaultPoolConfigurator(MemoryPoolConfigurator):
|
||||
"""Configurator for standard models: MHA, MLA, DSA, FP4.
|
||||
|
||||
coeff = cell_size (bytes per token across all layers)
|
||||
bias = 0
|
||||
"""
|
||||
|
||||
def __init__(self, mr: ModelRunner):
|
||||
# Determine effective number of layers for KV cache
|
||||
if mambaish := mr.mambaish_config:
|
||||
effective_layer_ids = [
|
||||
i
|
||||
for i in mambaish.full_attention_layer_ids
|
||||
if mr.start_layer <= i < mr.end_layer
|
||||
]
|
||||
num_layers = len(effective_layer_ids)
|
||||
else:
|
||||
num_layers = mr.num_effective_layers
|
||||
|
||||
self._cell_size = self._compute_cell_size(mr, num_layers)
|
||||
|
||||
# EAGLE/STANDALONE: scale cell_size to account for draft model KV cache.
|
||||
# Assumes draft and target share the same per-layer KV size (head_dim,
|
||||
# num_kv_heads, dtype), which holds for EAGLE/MTP draft models that
|
||||
# reuse the target architecture's attention config.
|
||||
if (
|
||||
mr.spec_algorithm.is_eagle() or mr.spec_algorithm.is_standalone()
|
||||
) and not mr.is_draft_worker:
|
||||
eagle_draft_num_layers = getattr(mr, "eagle_draft_num_layers", None)
|
||||
if (
|
||||
eagle_draft_num_layers is not None
|
||||
and int(eagle_draft_num_layers) > 0
|
||||
and int(num_layers) > 0
|
||||
):
|
||||
self._cell_size = int(
|
||||
self._cell_size
|
||||
* (1 + int(eagle_draft_num_layers) / int(num_layers))
|
||||
)
|
||||
|
||||
# DFLASH/DSPARK: scale cell_size to account for draft model KV cache
|
||||
if mr.spec_algorithm.is_dflash_family() and not mr.is_draft_worker:
|
||||
from sglang.srt.speculative.dflash_utils import (
|
||||
scale_kv_cell_size_per_token_for_dflash,
|
||||
)
|
||||
|
||||
draft_num_layers = mr.dflash_family_draft_num_layers
|
||||
if (
|
||||
draft_num_layers is not None
|
||||
and int(draft_num_layers) > 0
|
||||
and int(num_layers) > 0
|
||||
):
|
||||
self._cell_size = scale_kv_cell_size_per_token_for_dflash(
|
||||
target_cell_size_per_token=self._cell_size,
|
||||
target_num_layers=int(num_layers),
|
||||
draft_num_layers=int(draft_num_layers),
|
||||
)
|
||||
|
||||
def _compute_cell_size(self, mr: ModelRunner, num_layers: int) -> int:
|
||||
"""Compute per-token KV cache cost in bytes. Subclasses can override."""
|
||||
# args to config cell size
|
||||
model_config = mr.model_config
|
||||
kv_cache_dtype = mr.kv_cache_dtype
|
||||
from sglang.srt.layers.cp.utils import (
|
||||
get_glm_dsa_layer_split_effective_num_layers,
|
||||
)
|
||||
|
||||
effective_num_layers = get_glm_dsa_layer_split_effective_num_layers(
|
||||
mr, num_layers
|
||||
)
|
||||
|
||||
kv_size = torch._utils._element_size(kv_cache_dtype)
|
||||
tp_size = get_parallel().attn_tp_size
|
||||
|
||||
if mr.use_mla_backend:
|
||||
cell_size = (
|
||||
(model_config.kv_lora_rank + model_config.qk_rope_head_dim)
|
||||
* effective_num_layers
|
||||
* kv_size
|
||||
)
|
||||
if is_float4_e2m1fn_x2(kv_cache_dtype):
|
||||
# kv_scale_buffer
|
||||
scale_block_size = 16
|
||||
cell_size = (cell_size // 2) + (
|
||||
(
|
||||
(model_config.kv_lora_rank + model_config.qk_rope_head_dim)
|
||||
// scale_block_size
|
||||
)
|
||||
* effective_num_layers
|
||||
* kv_size
|
||||
)
|
||||
|
||||
# Add indexer KV cache overhead for DSA models (DeepSeek V3.2)
|
||||
if is_deepseek_dsa(model_config.hf_config):
|
||||
index_head_dim = get_dsa_index_head_dim(model_config.hf_config)
|
||||
indexer_size_per_token = (
|
||||
index_head_dim
|
||||
+ index_head_dim // DSATokenToKVPool.quant_block_size * 4
|
||||
)
|
||||
element_size = torch._utils._element_size(
|
||||
DSATokenToKVPool.index_k_with_scale_buffer_dtype
|
||||
)
|
||||
cell_size += (
|
||||
indexer_size_per_token * effective_num_layers * element_size
|
||||
)
|
||||
elif is_minimax_sparse(model_config.hf_config):
|
||||
# Mirrors MiniMaxSparseKVPool: main pool (K+V all layers) + indexer pool
|
||||
# (sparse-only, single-head; kv layers store K+V, k-only layers store K).
|
||||
sparse_cfg = get_minimax_sparse_attention_config(model_config.hf_config)
|
||||
dense_layer_ids, sparse_layer_ids = get_minimax_sparse_layer_ids(sparse_cfg)
|
||||
indexer_k_only_layer_ids = set(
|
||||
get_minimax_sparse_disable_value_layer_ids(sparse_cfg)
|
||||
)
|
||||
|
||||
local_dense_layer_ids = [
|
||||
l for l in dense_layer_ids if mr.start_layer <= l < mr.end_layer
|
||||
]
|
||||
local_sparse_layer_ids = [
|
||||
l for l in sparse_layer_ids if mr.start_layer <= l < mr.end_layer
|
||||
]
|
||||
num_dense = len(local_dense_layer_ids)
|
||||
num_sparse = len(local_sparse_layer_ids)
|
||||
num_indexer_k_only = sum(
|
||||
1 for l in local_sparse_layer_ids if l in indexer_k_only_layer_ids
|
||||
)
|
||||
num_indexer_kv = num_sparse - num_indexer_k_only
|
||||
|
||||
kv_heads = model_config.get_num_kv_heads(get_parallel().attn_tp_size)
|
||||
head_dim = model_config.head_dim
|
||||
indexer_head_dim = sparse_cfg["sparse_index_dim"]
|
||||
indexer_dtype_size = torch._utils._element_size(mr.dtype)
|
||||
|
||||
main_pool_bytes = (
|
||||
(num_dense + num_sparse) * 2 * kv_heads * head_dim * kv_size
|
||||
)
|
||||
indexer_bytes = (
|
||||
(num_indexer_kv * 2 + num_indexer_k_only)
|
||||
* indexer_head_dim
|
||||
* indexer_dtype_size
|
||||
)
|
||||
# FP4 scale buffer adjustment doesn't apply to MiniMax sparse:
|
||||
# cell_size is already a sum over heterogeneous sub-pools.
|
||||
return main_pool_bytes + indexer_bytes
|
||||
else:
|
||||
cell_size = (
|
||||
model_config.get_num_kv_heads(tp_size)
|
||||
* (model_config.head_dim + model_config.v_head_dim)
|
||||
* effective_num_layers
|
||||
* kv_size
|
||||
)
|
||||
|
||||
if is_float4_e2m1fn_x2(kv_cache_dtype):
|
||||
# kv_scale_buffer
|
||||
scale_block_size = 16
|
||||
n = model_config.get_num_kv_heads(tp_size)
|
||||
k = model_config.head_dim
|
||||
cell_size = (cell_size // 2) + (
|
||||
(n * k * effective_num_layers * 2 * kv_size) // scale_block_size
|
||||
)
|
||||
|
||||
return cell_size
|
||||
|
||||
def calculate_pool_sizes(
|
||||
self, available_bytes: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
max_total_num_tokens = available_bytes // self._cell_size
|
||||
max_total_num_tokens = max_total_num_tokens // page_size * page_size
|
||||
return MemoryPoolConfig(max_total_num_tokens=max_total_num_tokens)
|
||||
|
||||
def calculate_pool_sizes_from_max_tokens(
|
||||
self, max_total_num_tokens: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
max_total_num_tokens = max_total_num_tokens // page_size * page_size
|
||||
return MemoryPoolConfig(max_total_num_tokens=max_total_num_tokens)
|
||||
|
||||
|
||||
class HybridSWAPoolConfigurator(MemoryPoolConfigurator):
|
||||
"""Configurator for hybrid sliding window attention models (Gemma2, Command-R, MiMo).
|
||||
|
||||
Splits available memory between full attention and SWA pools.
|
||||
Does NOT inherit DefaultPoolConfigurator — different coeff model.
|
||||
"""
|
||||
|
||||
def __init__(self, mr: ModelRunner):
|
||||
model_config = mr.model_config
|
||||
kv_cache_dtype = mr.kv_cache_dtype
|
||||
kv_size = torch._utils._element_size(kv_cache_dtype)
|
||||
tp_size = get_parallel().attn_tp_size
|
||||
|
||||
self._full_layers_num = len(model_config.full_attention_layer_ids)
|
||||
self._swa_layers_num = len(model_config.swa_attention_layer_ids)
|
||||
assert (
|
||||
self._swa_layers_num > 0
|
||||
), "Hybrid SWA model must have at least one SWA layer"
|
||||
|
||||
self._swa_full_tokens_ratio = mr.server_args.swa_full_tokens_ratio
|
||||
|
||||
# Full layer per-token memory (bytes)
|
||||
self._full_per_token = (
|
||||
model_config.get_num_kv_heads(tp_size)
|
||||
* (model_config.head_dim + model_config.v_head_dim)
|
||||
* kv_size
|
||||
)
|
||||
|
||||
# SWA layer per-token memory (bytes)
|
||||
self._swa_per_token = (
|
||||
model_config.get_swa_num_kv_heads(tp_size)
|
||||
* (model_config.swa_head_dim + model_config.swa_v_head_dim)
|
||||
* kv_size
|
||||
)
|
||||
|
||||
# EAGLE/STANDALONE draft KV pool inherits max_total tokens with its
|
||||
# full-attn layers; budget into the full term.
|
||||
self._draft_full_layers_num = 0
|
||||
if (
|
||||
mr.spec_algorithm.is_eagle() or mr.spec_algorithm.is_standalone()
|
||||
) and not mr.is_draft_worker:
|
||||
draft_layers = getattr(mr, "eagle_draft_num_layers", None)
|
||||
if draft_layers is not None and int(draft_layers) > 0:
|
||||
self._draft_full_layers_num = int(draft_layers)
|
||||
|
||||
# Bytes per token of max_total_num_tokens.
|
||||
#
|
||||
# Hybrid (full_layers > 0): max_total = full_tokens, so cell_size accounts
|
||||
# for both pools: F*nf + r*S*ns (where swa_tokens = full_tokens * r).
|
||||
#
|
||||
# All-SWA (full_layers == 0): max_total = swa_tokens directly. The ratio
|
||||
# is meaningless here -- there is no full pool to relate to, and every
|
||||
# token beyond the sliding window can be evicted. So cell_size = S*ns,
|
||||
# with no ratio factor applied.
|
||||
if self._full_layers_num == 0:
|
||||
self._cell_size = (
|
||||
self._swa_per_token * self._swa_layers_num
|
||||
+ self._full_per_token * self._draft_full_layers_num
|
||||
)
|
||||
else:
|
||||
self._cell_size = (
|
||||
self._full_per_token
|
||||
* (self._full_layers_num + self._draft_full_layers_num)
|
||||
+ self._swa_full_tokens_ratio
|
||||
* self._swa_per_token
|
||||
* self._swa_layers_num
|
||||
)
|
||||
|
||||
def _solve_pool_sizes(
|
||||
self, max_total_num_tokens: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
"""Core computation: split max_total_num_tokens into full/swa pool sizes."""
|
||||
|
||||
def align_page_size(x: int) -> int:
|
||||
return (x // page_size) * page_size
|
||||
|
||||
if self._full_layers_num == 0:
|
||||
# All-SWA: no full pool, max_total = actual SWA pool size.
|
||||
# Ratio is not applied -- see __init__ comment.
|
||||
swa_tokens = align_page_size(max_total_num_tokens)
|
||||
logger.info(
|
||||
f"Use sliding window memory pool (all SWA). "
|
||||
f"swa_layer_tokens={swa_tokens}"
|
||||
)
|
||||
return MemoryPoolConfig(
|
||||
max_total_num_tokens=swa_tokens,
|
||||
full_max_total_num_tokens=0,
|
||||
swa_max_total_num_tokens=swa_tokens,
|
||||
)
|
||||
|
||||
# Hybrid: full_tokens = max_total_num_tokens, swa_tokens = full_tokens * ratio
|
||||
full_tokens = align_page_size(max_total_num_tokens)
|
||||
swa_tokens = align_page_size(int(full_tokens * self._swa_full_tokens_ratio))
|
||||
|
||||
logger.info(
|
||||
f"Use sliding window memory pool. "
|
||||
f"full_layer_tokens={full_tokens}, swa_layer_tokens={swa_tokens}"
|
||||
)
|
||||
|
||||
return MemoryPoolConfig(
|
||||
max_total_num_tokens=full_tokens,
|
||||
full_max_total_num_tokens=full_tokens,
|
||||
swa_max_total_num_tokens=swa_tokens,
|
||||
)
|
||||
|
||||
def calculate_pool_sizes(
|
||||
self, available_bytes: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
max_total_num_tokens = int(available_bytes // self._cell_size)
|
||||
return self._solve_pool_sizes(max_total_num_tokens, page_size)
|
||||
|
||||
def calculate_pool_sizes_from_max_tokens(
|
||||
self, max_total_num_tokens: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
return self._solve_pool_sizes(max_total_num_tokens, page_size)
|
||||
|
||||
|
||||
class SWAChunkCapPoolConfigurator(HybridSWAPoolConfigurator):
|
||||
"""Hybrid SWA configurator with the SWA pool sized from a fixed token cap.
|
||||
|
||||
When max_running_requests is explicit, the SWA pool's worst-case
|
||||
footprint is bounded per request. The SWA pool is sized tightly from that
|
||||
cap and the freed memory is redirected to the full pool, instead of sizing
|
||||
both pools by swa_full_tokens_ratio.
|
||||
"""
|
||||
|
||||
def __init__(self, mr: ModelRunner):
|
||||
super().__init__(mr)
|
||||
assert self._full_layers_num > 0
|
||||
|
||||
sa = mr.server_args
|
||||
page_size = mr.page_size
|
||||
window = mr.sliding_window_size
|
||||
draft_tokens = sa.speculative_num_draft_tokens or 1
|
||||
eviction_interval = max(1, envs.SGLANG_SWA_EVICTION_INTERVAL.get())
|
||||
|
||||
"""
|
||||
__________[padding][eviction_interval][window]
|
||||
Padding to make sure eviction point is page-aligned.
|
||||
"""
|
||||
trailing_tokens = window + eviction_interval * draft_tokens + page_size
|
||||
if sa.speculative_algorithm is None:
|
||||
decode_alloc = page_size
|
||||
elif sa.disable_overlap_schedule:
|
||||
# spec-v1: new_tokens_required_next_decode per request.
|
||||
decode_alloc = spec_decode_alloc_len_per_request(sa)
|
||||
else:
|
||||
# spec-v2: the overlap allocator keeps 2 * alloc_len outstanding
|
||||
# (eagle_utils.eagle_prepare_for_decode: kv_committed_len + 2 * alloc_len).
|
||||
decode_alloc = 2 * get_alloc_len_per_decode(sa)
|
||||
per_request = trailing_tokens + decode_alloc
|
||||
|
||||
num_reqs = sa.max_running_requests // mr.dp_size
|
||||
if sa.disaggregation_mode == "decode":
|
||||
self._swa_cap = (
|
||||
per_request * num_reqs
|
||||
+ (window + page_size) * sa.disaggregation_decode_extra_slots
|
||||
)
|
||||
else:
|
||||
chunks_in_flight = 1 if sa.disable_overlap_schedule else 2
|
||||
self._swa_cap = (
|
||||
per_request * num_reqs
|
||||
+ chunks_in_flight * sa.chunked_prefill_size
|
||||
+ page_size
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_applicable(mr: ModelRunner) -> bool:
|
||||
"""True when SWAChunkCache can be sized from explicit max requests."""
|
||||
sa = mr.server_args
|
||||
if sa.max_running_requests is None:
|
||||
return False
|
||||
if not sa.disable_radix_cache:
|
||||
return False
|
||||
if sa.chunked_prefill_size is None:
|
||||
return False
|
||||
if mr.sliding_window_size is None:
|
||||
return False
|
||||
return len(mr.model_config.full_attention_layer_ids) > 0
|
||||
|
||||
def calculate_pool_sizes(
|
||||
self, available_bytes: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
# SWA pool sized tightly from the cap; the rest of the budget goes to full.
|
||||
swa_tokens = ceil_align(self._swa_cap, page_size)
|
||||
fixed_swa_bytes = swa_tokens * self._swa_per_token * self._swa_layers_num
|
||||
full_cell_size = self._full_per_token * (
|
||||
self._full_layers_num + self._draft_full_layers_num
|
||||
)
|
||||
full_tokens = (
|
||||
int((available_bytes - fixed_swa_bytes) // full_cell_size) // page_size
|
||||
) * page_size
|
||||
if full_tokens <= 0:
|
||||
raise RuntimeError(
|
||||
f"SWA pool cap ({swa_tokens} tokens, "
|
||||
f"{fixed_swa_bytes / (1 << 30):.2f} GiB) leaves no room for the full "
|
||||
f"KV pool within the available {available_bytes / (1 << 30):.2f} GiB. "
|
||||
f"Reduce --max-running-requests, lower SGLANG_SWA_EVICTION_INTERVAL, "
|
||||
f"or increase --mem-fraction-static."
|
||||
)
|
||||
return MemoryPoolConfig(
|
||||
max_total_num_tokens=full_tokens,
|
||||
full_max_total_num_tokens=full_tokens,
|
||||
swa_max_total_num_tokens=swa_tokens,
|
||||
)
|
||||
|
||||
def calculate_pool_sizes_from_max_tokens(
|
||||
self, max_total_num_tokens: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
# Constrained max_total goes to the full pool; SWA stays at its cap.
|
||||
swa_tokens = ceil_align(self._swa_cap, page_size)
|
||||
full_tokens = (max_total_num_tokens // page_size) * page_size
|
||||
return MemoryPoolConfig(
|
||||
max_total_num_tokens=full_tokens,
|
||||
full_max_total_num_tokens=full_tokens,
|
||||
swa_max_total_num_tokens=min(swa_tokens, max_total_num_tokens),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _DSV4PoolSizes:
|
||||
full_max_total_num_tokens: int
|
||||
swa_max_total_num_tokens: int
|
||||
c4_max_total_num_tokens: int
|
||||
c128_max_total_num_tokens: int
|
||||
c4_state_pool_size: int
|
||||
c128_state_pool_size: int
|
||||
|
||||
|
||||
class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
"""Configurator for DSV4 compressed-attention models.
|
||||
|
||||
Splits available memory across full / swa / c4 / c128 + c4_state / c128_state
|
||||
pools. coeff is bytes_per_full_token (inflated by (T+D)/T when speculative
|
||||
decode reserves a draft worker, mirroring dflash's cell_size scaling); bias = 0.
|
||||
"""
|
||||
|
||||
def __init__(self, mr: ModelRunner):
|
||||
cfg = mr.model_config
|
||||
self.qk_nope_head_dim = cfg.qk_nope_head_dim
|
||||
self.qk_rope_head_dim = cfg.qk_rope_head_dim
|
||||
self.indexer_head_dim = cfg.index_head_dim
|
||||
self.context_len = mr.model_config.context_len
|
||||
# PP-local slice; matches DeepSeekV4TokenToKVPool's stage_ratios.
|
||||
self.compression_ratios = cfg.compress_ratios[mr.start_layer : mr.end_layer]
|
||||
if mr.pp_size > 1:
|
||||
logger.info(
|
||||
f"DSV4 pool PP slice: rank={mr.pp_group.rank_in_group} "
|
||||
f"layers=[{mr.start_layer},{mr.end_layer}) "
|
||||
f"local={len(self.compression_ratios)}/{len(cfg.compress_ratios)}"
|
||||
)
|
||||
self.swa_page_size = cfg.window_size
|
||||
self.swa_ratio = mr.server_args.swa_full_tokens_ratio
|
||||
self.is_speculative = mr.server_args.speculative_algorithm is not None
|
||||
self.online_c128_mtp_max_draft_tokens = (
|
||||
mr.server_args.max_speculative_num_draft_tokens or 0
|
||||
)
|
||||
self.requested_max_running_requests_per_worker = (
|
||||
mr.server_args.max_running_requests // mr.dp_size
|
||||
if mr.server_args.max_running_requests is not None
|
||||
else None
|
||||
)
|
||||
self.disaggregation_mode = mr.server_args.disaggregation_mode
|
||||
self.disaggregation_decode_extra_slots = (
|
||||
mr.server_args.disaggregation_decode_extra_slots or 0
|
||||
)
|
||||
if mr.enable_hisparse:
|
||||
from sglang.srt.mem_cache.sparsity import parse_hisparse_config
|
||||
|
||||
self.c4_shrink_factor = parse_hisparse_config(
|
||||
mr.server_args
|
||||
).host_to_device_ratio
|
||||
else:
|
||||
self.c4_shrink_factor = 1
|
||||
assert self.c4_shrink_factor >= 1
|
||||
if self.c4_shrink_factor > 1:
|
||||
logger.info(f"HiSparse c4 host-to-device ratio = {self.c4_shrink_factor}")
|
||||
|
||||
self.c4_ring_size = get_compress_state_ring_size(4, self.is_speculative)
|
||||
self.c128_ring_size = get_compress_state_ring_size(128, self.is_speculative)
|
||||
|
||||
self.num_layers_total = len(self.compression_ratios)
|
||||
self.num_layers_ca4 = sum(1 for r in self.compression_ratios if r == 4)
|
||||
self.num_layers_ca128 = sum(1 for r in self.compression_ratios if r == 128)
|
||||
|
||||
self.bytes_per_full_token = self._get_bytes_per_full_token()
|
||||
if self.is_speculative:
|
||||
# Reserve memory for the speculative draft worker by inflating
|
||||
# per-token bytes by (target+draft)/target. Equivalent to dflash's
|
||||
# scale_kv_cell_size_per_token_for_dflash but applied to
|
||||
# bytes_per_full_token: tokens = avail / (bpft * (T+D)/T).
|
||||
draft_layers = 1
|
||||
target_layers = self.num_layers_total
|
||||
self.bytes_per_full_token *= (target_layers + draft_layers) / target_layers
|
||||
|
||||
# Online c128 keeps a single in-progress (max, sum, kv) state per index
|
||||
# and assumes a strict forward-only schedule. Speculative decode (MTP)
|
||||
# would need rollback / replay across draft and verify, which the
|
||||
# online path doesn't support yet.
|
||||
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
||||
allow_experimental_online_c128_mtp = (
|
||||
envs.SGLANG_EXPERIMENTAL_ONLINE_C128_MTP.get()
|
||||
and mr.spec_algorithm.is_eagle()
|
||||
)
|
||||
assert mr.spec_algorithm.is_none() or allow_experimental_online_c128_mtp, (
|
||||
"SGLANG_OPT_USE_ONLINE_COMPRESS does not support speculative decode "
|
||||
"(MTP) yet, except the experimental EAGLE topk=1 path gated by "
|
||||
"SGLANG_EXPERIMENTAL_ONLINE_C128_MTP=1"
|
||||
)
|
||||
if allow_experimental_online_c128_mtp:
|
||||
assert self.online_c128_mtp_max_draft_tokens > 0, (
|
||||
"SGLANG_EXPERIMENTAL_ONLINE_C128_MTP requires "
|
||||
"speculative_num_draft_tokens to be set."
|
||||
)
|
||||
logger.warning(
|
||||
"DSV4 compressed attention: experimental online c128 + MTP enabled "
|
||||
f"(EAGLE topk=1 only, "
|
||||
f"draft_banks={self.online_c128_mtp_max_draft_tokens}). "
|
||||
"Validate correctness carefully."
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"DSV4 compressed attention: online c128 enabled (ring_size=1)"
|
||||
)
|
||||
|
||||
def _get_bytes_per_full_token(self) -> float:
|
||||
kv_bytes = self.qk_nope_head_dim + self.qk_rope_head_dim * 2 + 8
|
||||
|
||||
quant_block_size = 128
|
||||
indexer_bytes = (
|
||||
self.indexer_head_dim + self.indexer_head_dim // quant_block_size * 4
|
||||
)
|
||||
|
||||
attn_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
||||
c4_state_dtype_size, c128_state_dtype_size = (
|
||||
_get_dsv4_compress_state_dtype_sizes()
|
||||
)
|
||||
c4_state_bytes = 2 * 2 * attn_head_dim * c4_state_dtype_size
|
||||
# Online c128 stores (max, sum, kv) per slot (3*head_dim) instead of
|
||||
# raw (kv, score) (2*head_dim). Combined with ring_size=1 this still
|
||||
# nets a large reduction (~3/256x) but the per-slot bytes go up.
|
||||
c128_online = envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get()
|
||||
c128_state_bytes = (
|
||||
(3 if c128_online else 2 * 1) * attn_head_dim * c128_state_dtype_size
|
||||
)
|
||||
c4_indexer_state_bytes = 2 * 2 * self.indexer_head_dim * c4_state_dtype_size
|
||||
|
||||
c4_state_ratio = self.c4_ring_size / self.swa_page_size
|
||||
# C128 state is request-scoped and is finalized after
|
||||
# max_running_requests is known, so it should not scale with
|
||||
# full-token capacity here.
|
||||
c128_state_ratio = 0
|
||||
|
||||
c4_frac = 1 / (4 * self.c4_shrink_factor)
|
||||
return (
|
||||
self.swa_ratio * kv_bytes * self.num_layers_total
|
||||
+ c4_frac * kv_bytes * self.num_layers_ca4
|
||||
+ 1 / 128 * kv_bytes * self.num_layers_ca128
|
||||
+ 1 / 4 * indexer_bytes * self.num_layers_ca4
|
||||
+ self.swa_ratio * c4_state_ratio * c4_state_bytes * self.num_layers_ca4
|
||||
+ c128_state_ratio * c128_state_bytes * self.num_layers_ca128
|
||||
+ self.swa_ratio
|
||||
* c4_state_ratio
|
||||
* c4_indexer_state_bytes
|
||||
* self.num_layers_ca4
|
||||
)
|
||||
|
||||
def _compute_dsv4_sizes(self, full_token: int, page_size: int) -> _DSV4PoolSizes:
|
||||
full_token = full_token // page_size * page_size
|
||||
swa_tokens = int(full_token * self.swa_ratio) // page_size * page_size
|
||||
return _DSV4PoolSizes(
|
||||
full_max_total_num_tokens=full_token,
|
||||
swa_max_total_num_tokens=swa_tokens,
|
||||
c4_max_total_num_tokens=full_token // (4 * self.c4_shrink_factor),
|
||||
c128_max_total_num_tokens=full_token // 128,
|
||||
c4_state_pool_size=swa_tokens // self.swa_page_size * self.c4_ring_size,
|
||||
c128_state_pool_size=0,
|
||||
)
|
||||
|
||||
def _get_num_req_slots(self, max_running_requests: int) -> int:
|
||||
if self.disaggregation_mode == "decode":
|
||||
return max_running_requests + self.disaggregation_decode_extra_slots + 1
|
||||
return max_running_requests + 1
|
||||
|
||||
def _get_c128_state_fixed_bytes(self, max_running_requests: int) -> int:
|
||||
if self.num_layers_ca128 == 0:
|
||||
return 0
|
||||
|
||||
_, c128_state_dtype_size = _get_dsv4_compress_state_dtype_sizes()
|
||||
attn_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
||||
num_req_slots = self._get_num_req_slots(max_running_requests)
|
||||
|
||||
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
||||
state_rows = num_req_slots + self.c128_ring_size + 1
|
||||
state_rows *= 1 + self.online_c128_mtp_max_draft_tokens
|
||||
state_last_dim = 3 * attn_head_dim
|
||||
else:
|
||||
state_pool_size = num_req_slots * self.c128_ring_size
|
||||
state_rows = state_pool_size + self.c128_ring_size + 1
|
||||
state_rows = ceil_div(state_rows, 128) * 128
|
||||
state_last_dim = 2 * attn_head_dim
|
||||
|
||||
return (
|
||||
state_rows * state_last_dim * c128_state_dtype_size * self.num_layers_ca128
|
||||
)
|
||||
|
||||
def _get_c128_state_fixed_bytes_for_token_capacity(
|
||||
self, token_capacity: int
|
||||
) -> int:
|
||||
if self.requested_max_running_requests_per_worker is not None:
|
||||
return self._get_c128_state_fixed_bytes(
|
||||
self.requested_max_running_requests_per_worker
|
||||
)
|
||||
|
||||
estimated = int(token_capacity / self.context_len * 512)
|
||||
estimated = max(min(estimated, 4096), 2048)
|
||||
max_running_requests = min(estimated, token_capacity // 2)
|
||||
return self._get_c128_state_fixed_bytes(max_running_requests)
|
||||
|
||||
def _to_config(self, sizes: _DSV4PoolSizes) -> MemoryPoolConfig:
|
||||
full = sizes.full_max_total_num_tokens
|
||||
swa = sizes.swa_max_total_num_tokens
|
||||
logger.info(
|
||||
f"DSV4 pool sizes: full={full}, swa={swa}, "
|
||||
f"c4={sizes.c4_max_total_num_tokens}, "
|
||||
f"c128={sizes.c128_max_total_num_tokens}, "
|
||||
f"c4_state={sizes.c4_state_pool_size}, "
|
||||
f"c128_state={sizes.c128_state_pool_size}"
|
||||
)
|
||||
return MemoryPoolConfig(
|
||||
max_total_num_tokens=full,
|
||||
full_max_total_num_tokens=full,
|
||||
swa_max_total_num_tokens=swa,
|
||||
c4_max_total_num_tokens=sizes.c4_max_total_num_tokens,
|
||||
c128_max_total_num_tokens=sizes.c128_max_total_num_tokens,
|
||||
c4_state_pool_size=sizes.c4_state_pool_size,
|
||||
c128_state_pool_size=sizes.c128_state_pool_size,
|
||||
)
|
||||
|
||||
def finalize_with_max_running_requests(
|
||||
self, config: MemoryPoolConfig
|
||||
) -> MemoryPoolConfig:
|
||||
assert config.max_running_requests is not None
|
||||
num_req_slots = self._get_num_req_slots(config.max_running_requests)
|
||||
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
||||
config.c128_state_pool_size = num_req_slots
|
||||
else:
|
||||
config.c128_state_pool_size = num_req_slots * self.c128_ring_size
|
||||
return config
|
||||
|
||||
def calculate_pool_sizes(
|
||||
self, available_bytes: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
assert (
|
||||
page_size % 128 == 0
|
||||
), "page_size must be multiple of 128 for compressed attention"
|
||||
|
||||
if self.requested_max_running_requests_per_worker is not None:
|
||||
c128_state_fixed_bytes = self._get_c128_state_fixed_bytes(
|
||||
self.requested_max_running_requests_per_worker
|
||||
)
|
||||
else:
|
||||
full_token = int(available_bytes / self.bytes_per_full_token)
|
||||
c128_state_fixed_bytes = (
|
||||
self._get_c128_state_fixed_bytes_for_token_capacity(full_token)
|
||||
)
|
||||
|
||||
available_bytes_for_tokens = max(available_bytes - c128_state_fixed_bytes, 0)
|
||||
full_token = int(available_bytes_for_tokens / self.bytes_per_full_token)
|
||||
|
||||
sizes = self._compute_dsv4_sizes(full_token, page_size)
|
||||
logger.info(
|
||||
f"DSV4 memory calculation: "
|
||||
f"bytes_per_full_token={self.bytes_per_full_token:.2f}, "
|
||||
f"available_bytes={available_bytes / (1 << 30):.2f} GB, "
|
||||
f"c128_state_fixed={c128_state_fixed_bytes / (1 << 30):.2f} GB, "
|
||||
f"full_token={sizes.full_max_total_num_tokens}"
|
||||
)
|
||||
return self._to_config(sizes)
|
||||
|
||||
def calculate_pool_sizes_from_max_tokens(
|
||||
self, max_total_num_tokens: int, page_size: int
|
||||
) -> MemoryPoolConfig:
|
||||
assert (
|
||||
page_size % 128 == 0
|
||||
), "page_size must be multiple of 128 for compressed attention"
|
||||
sizes = self._compute_dsv4_sizes(max_total_num_tokens, page_size)
|
||||
return self._to_config(sizes)
|
||||
|
||||
|
||||
def create_memory_pool_configurator(
|
||||
mr: ModelRunner,
|
||||
) -> MemoryPoolConfigurator:
|
||||
"""Factory: select the right configurator for the model architecture."""
|
||||
if is_deepseek_v4(mr.model_config.hf_config) and mr.is_hybrid_swa:
|
||||
return DSV4PoolConfigurator(mr)
|
||||
if mr.is_hybrid_swa:
|
||||
if SWAChunkCapPoolConfigurator.is_applicable(mr):
|
||||
return SWAChunkCapPoolConfigurator(mr)
|
||||
return HybridSWAPoolConfigurator(mr)
|
||||
# Future: MambaPoolConfigurator
|
||||
return DefaultPoolConfigurator(mr)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Phase-aware CUDA graph runners.
|
||||
|
||||
One concrete runner per phase. Each runner owns its phase-specific
|
||||
shape semantics (decode → batch size; prefill → token count) and
|
||||
delegates capture/replay mechanics to a pluggable
|
||||
BaseCudaGraphBackend chosen via cuda_graph_config.
|
||||
|
||||
Public API:
|
||||
- BaseRunner — minimal abstract base shared by the cuda-graph runners
|
||||
and the eager runner (shared __init__ + warmup + abstract
|
||||
can_run_graph/load_batch/execute).
|
||||
- BaseCudaGraphRunner — abstract cuda-graph base; bucket padding +
|
||||
capture-loop scaffolding on top of BaseRunner.
|
||||
- DecodeCudaGraphRunner — concrete decode-phase runner.
|
||||
- PrefillCudaGraphRunner — concrete prefill-phase runner.
|
||||
- EagerRunner — no-cuda-graph runner; runs model.forward live (the
|
||||
eager dual of the cuda-graph runners), mode-dispatched over decode +
|
||||
extend + idle.
|
||||
- Buffer dataclasses, capture-mode flags, the global memory pool,
|
||||
and the DeepEP adapter live in
|
||||
sglang.srt.model_executor.runner_utils; they are
|
||||
re-exported here for the EAGLE / multi-step draft cuda graph
|
||||
runners that were authored against the legacy public surface.
|
||||
"""
|
||||
|
||||
from sglang.srt.model_executor.runner.base_cuda_graph_runner import ( # noqa: F401
|
||||
BaseCudaGraphRunner,
|
||||
freeze_gc,
|
||||
get_batch_sizes_to_capture,
|
||||
)
|
||||
from sglang.srt.model_executor.runner.base_runner import BaseRunner # noqa: F401
|
||||
from sglang.srt.model_executor.runner.decode_cuda_graph_runner import (
|
||||
DecodeCudaGraphRunner,
|
||||
)
|
||||
from sglang.srt.model_executor.runner.eager_runner import EagerRunner # noqa: F401
|
||||
from sglang.srt.model_executor.runner.prefill_cuda_graph_runner import ( # noqa: F401
|
||||
PrefillCudaGraphRunner,
|
||||
)
|
||||
from sglang.srt.model_executor.runner.shape_key import ShapeKey # noqa: F401
|
||||
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph import ( # noqa: F401
|
||||
TCPCG_FAILURE_HINT,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_utils import ( # noqa: F401
|
||||
DecodeInputBuffers,
|
||||
DeepEPCudaGraphRunnerAdapter,
|
||||
PrefillInputBuffers,
|
||||
_grouped_foreach_copy_,
|
||||
_set_capture_lora_variant,
|
||||
compile_in_capture_mode,
|
||||
get_capture_lora_variant,
|
||||
get_global_graph_memory_pool,
|
||||
get_is_capture_mode,
|
||||
model_capture_mode,
|
||||
set_global_graph_memory_pool,
|
||||
)
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""Shared scaffolding for the prefill and decode CUDA graph runners."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bisect
|
||||
import gc
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Any, List, Sequence, Tuple
|
||||
|
||||
from sglang.srt.model_executor.runner.base_runner import BaseRunner
|
||||
from sglang.srt.runtime_context import get_flags, get_parallel
|
||||
from sglang.srt.utils import require_gathered_buffer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.input_buffers import ForwardInputBuffers
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
from sglang.srt.model_executor.runner_backend.base_cuda_graph_backend import (
|
||||
BaseCudaGraphBackend,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def freeze_gc(enable_cudagraph_gc: bool):
|
||||
"""Optimize garbage collection during CUDA graph capture.
|
||||
|
||||
Clean up first, then freeze remaining objects from being included in
|
||||
future collections if GC is disabled during capture.
|
||||
"""
|
||||
gc.collect()
|
||||
should_freeze = not enable_cudagraph_gc
|
||||
if should_freeze:
|
||||
gc.freeze()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if should_freeze:
|
||||
gc.unfreeze()
|
||||
gc.collect()
|
||||
|
||||
|
||||
def get_batch_sizes_to_capture(
|
||||
model_runner: ModelRunner, num_tokens_per_bs: int = 1
|
||||
) -> Tuple[List[int], List[int]]:
|
||||
"""Build the (capture_bs, compile_bs) lists for the decode runner.
|
||||
|
||||
Filters cuda_graph_config[decode].bs by attention-tp/cp alignment
|
||||
constraints and clamps to req_to_token_pool.size.
|
||||
"""
|
||||
|
||||
server_args = model_runner.server_args
|
||||
capture_bs = list(server_args.cuda_graph_config.decode.bs)
|
||||
num_max_requests = model_runner.req_to_token_pool.size
|
||||
|
||||
mul_base = 1
|
||||
if server_args.enable_two_batch_overlap:
|
||||
mul_base *= 2
|
||||
num_tokens_per_bs = 1
|
||||
|
||||
if require_gathered_buffer(server_args):
|
||||
mul_base *= get_parallel().attn_tp_size
|
||||
|
||||
if mul_base % get_parallel().attn_cp_size != 0:
|
||||
mul_base *= get_parallel().attn_cp_size
|
||||
|
||||
# pad `num_max_requests` to avoid being filtered out
|
||||
num_max_requests = (num_max_requests + mul_base - 1) // mul_base * mul_base
|
||||
if max(capture_bs) > num_max_requests:
|
||||
# In some cases (e.g., with a small GPU or --max-running-requests), the #max-running-requests
|
||||
# is very small. We add more values here to make sure we capture the maximum bs.
|
||||
capture_bs += [num_max_requests]
|
||||
|
||||
# Model input token count = bs * num_tokens_per_bs; must be a multiple of attn_tp_size.
|
||||
capture_bs = [bs for bs in capture_bs if bs * num_tokens_per_bs % mul_base == 0]
|
||||
capture_bs = [bs for bs in capture_bs if bs <= num_max_requests]
|
||||
capture_bs = list(sorted(set(capture_bs)))
|
||||
|
||||
assert len(capture_bs) > 0 and capture_bs[0] > 0, f"{capture_bs=}"
|
||||
compile_bs = (
|
||||
[bs for bs in capture_bs if bs <= server_args.torch_compile_max_bs]
|
||||
if get_flags().capture.enable_torch_compile
|
||||
else []
|
||||
)
|
||||
return capture_bs, compile_bs
|
||||
|
||||
|
||||
class BaseCudaGraphRunner(BaseRunner):
|
||||
"""Abstract base for phase-specific cuda-graph runners.
|
||||
|
||||
A subclass (DecodeCudaGraphRunner / PrefillCudaGraphRunner) owns one
|
||||
phase and plugs in a BaseCudaGraphBackend that handles the
|
||||
capture / replay mechanics. The runner orchestrates bucket
|
||||
selection, static buffer population, attention metadata init,
|
||||
replay dispatch, and output slicing.
|
||||
|
||||
Adds the capture/shape machinery on top of BaseRunner:
|
||||
- capture_prepare(size, ...) — build the dummy ForwardBatch and
|
||||
per-shape local state needed by capture_one_shape.
|
||||
- capture() — one-time setup; iterates over shapes and calls
|
||||
capture_one_shape for each.
|
||||
- capture_one_shape(size, ...) — drive one model forward at this
|
||||
shape into the backend's captured artifact.
|
||||
- _pad_to_bucket(...) — round a raw shape up to the nearest captured
|
||||
bucket.
|
||||
|
||||
Inherits from BaseRunner: __init__ and the abstract
|
||||
can_run_graph / load_batch / execute.
|
||||
|
||||
Notes:
|
||||
- buffers and backend are populated by the subclass before
|
||||
capture(); the base only declares them.
|
||||
"""
|
||||
|
||||
# Subclasses populate before calling capture().
|
||||
buffers: ForwardInputBuffers
|
||||
backend: BaseCudaGraphBackend
|
||||
|
||||
@staticmethod
|
||||
def _pad_to_bucket(raw_size: int, buckets: Sequence[int]) -> int:
|
||||
"""Return the smallest buckets[i] >= raw_size.
|
||||
|
||||
Caller's can_run_graph must reject raw_size > max(buckets) before
|
||||
reaching load_batch; this assertion makes the contract
|
||||
explicit (bisect_left returns len(buckets) when the value
|
||||
exceeds all buckets, which would otherwise IndexError below
|
||||
with no diagnostic).
|
||||
"""
|
||||
assert raw_size <= buckets[-1], (
|
||||
f"size {raw_size} exceeds max captured bucket {buckets[-1]}; "
|
||||
f"can_run_graph should have rejected this batch"
|
||||
)
|
||||
index = bisect.bisect_left(buckets, raw_size)
|
||||
return buckets[index]
|
||||
|
||||
@abstractmethod
|
||||
def capture_prepare(self, size: int, *args, **kwargs) -> Any: ...
|
||||
|
||||
@abstractmethod
|
||||
def capture(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def capture_one_shape(self, size: int, *args, **kwargs) -> Any: ...
|
||||
@@ -0,0 +1,608 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""Base class shared by EagerRunner and BaseCudaGraphRunner."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Any, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.batch_overlap.two_batch_overlap import TboCudaGraphRunnerPlugin
|
||||
from sglang.srt.compilation.torch_compile_decoration import set_torch_compile_config
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers import deep_gemm_wrapper
|
||||
from sglang.srt.layers.dp_attention import (
|
||||
DpPaddingMode,
|
||||
set_dp_buffer_len,
|
||||
set_is_extend_in_batch,
|
||||
)
|
||||
from sglang.srt.model_executor.forward_batch_info import (
|
||||
CaptureHiddenMode,
|
||||
ForwardBatch,
|
||||
ForwardMode,
|
||||
NgramEmbeddingInfo,
|
||||
PPProxyTensors,
|
||||
)
|
||||
from sglang.srt.model_executor.forward_context import ForwardContext, forward_context
|
||||
from sglang.srt.model_executor.runner.flashinfer_autotune import (
|
||||
run_flashinfer_autotune_forward,
|
||||
should_run_flashinfer_autotune,
|
||||
)
|
||||
from sglang.srt.runtime_context import get_flags, get_parallel
|
||||
from sglang.srt.speculative.spec_info import create_dummy_verify_input
|
||||
from sglang.srt.utils import (
|
||||
empty_context,
|
||||
log_info_on_rank0,
|
||||
require_attn_tp_gather,
|
||||
require_gathered_buffer,
|
||||
require_mlp_tp_gather,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _allocate_decode_buffers(
|
||||
*,
|
||||
device: torch.device,
|
||||
max_bs: int,
|
||||
max_num_token: int,
|
||||
hidden_size: int,
|
||||
vocab_size: int,
|
||||
dtype: torch.dtype,
|
||||
dp_size: int,
|
||||
pp_size: int,
|
||||
is_encoder_decoder: bool,
|
||||
require_mlp_tp_gather: bool,
|
||||
seq_len_fill_value: int,
|
||||
encoder_len_fill_value: int,
|
||||
num_tokens_per_bs: int,
|
||||
cache_loc_dtype: torch.dtype,
|
||||
enable_mamba_track: bool,
|
||||
ne_token_table: Optional[torch.Tensor] = None,
|
||||
hc_hidden_size: Optional[int] = None,
|
||||
pp_proxy_topk_size: Optional[int] = None,
|
||||
) -> SimpleNamespace:
|
||||
"""Allocate the FB-shared decode buffers."""
|
||||
with torch.device(device):
|
||||
input_ids = torch.zeros((max_num_token,), dtype=torch.int64)
|
||||
input_embeds = torch.zeros((max_num_token, hidden_size), dtype=dtype)
|
||||
req_pool_indices = torch.zeros((max_bs,), dtype=torch.int64)
|
||||
seq_lens = torch.full((max_bs,), seq_len_fill_value, dtype=torch.int64)
|
||||
out_cache_loc = torch.zeros((max_num_token,), dtype=cache_loc_dtype)
|
||||
positions = torch.zeros((max_num_token,), dtype=torch.int64)
|
||||
mrope_positions = torch.zeros((3, max_num_token), dtype=torch.int64)
|
||||
num_token_non_padded = torch.zeros((1,), dtype=torch.int32)
|
||||
custom_mask = torch.ones(
|
||||
(max_bs * seq_len_fill_value + max_num_token) * num_tokens_per_bs,
|
||||
dtype=torch.bool,
|
||||
)
|
||||
next_token_logits_buffer = torch.zeros(
|
||||
(max_num_token, vocab_size),
|
||||
dtype=torch.float,
|
||||
)
|
||||
mamba_track_indices = (
|
||||
torch.zeros((max_bs,), dtype=torch.int64) if enable_mamba_track else None
|
||||
)
|
||||
mamba_track_mask = (
|
||||
torch.zeros((max_bs,), dtype=torch.bool) if enable_mamba_track else None
|
||||
)
|
||||
|
||||
if pp_size > 1:
|
||||
# mHC (e.g. DSV4) flattens residual into hidden_states (size = hc_hidden_size).
|
||||
is_mhc = hc_hidden_size is not None
|
||||
hs = hc_hidden_size if is_mhc else hidden_size
|
||||
pp_proxy_tensors = {
|
||||
"hidden_states": torch.zeros((max_bs, hs), dtype=dtype),
|
||||
}
|
||||
if not is_mhc:
|
||||
pp_proxy_tensors["residual"] = torch.zeros(
|
||||
(max_bs, hidden_size), dtype=dtype
|
||||
)
|
||||
if pp_proxy_topk_size is not None:
|
||||
pp_proxy_tensors["topk_indices"] = torch.zeros(
|
||||
(max_num_token, pp_proxy_topk_size), dtype=torch.int32
|
||||
)
|
||||
else:
|
||||
pp_proxy_tensors = None
|
||||
|
||||
if is_encoder_decoder:
|
||||
encoder_lens = torch.full(
|
||||
(max_bs,), encoder_len_fill_value, dtype=torch.int32
|
||||
)
|
||||
else:
|
||||
encoder_lens = None
|
||||
|
||||
if require_mlp_tp_gather:
|
||||
global_num_tokens_gpu = torch.zeros((dp_size,), dtype=torch.int32)
|
||||
global_num_tokens_for_logprob_gpu = torch.zeros(
|
||||
(dp_size,), dtype=torch.int32
|
||||
)
|
||||
else:
|
||||
global_num_tokens_gpu = torch.zeros((1,), dtype=torch.int32)
|
||||
global_num_tokens_for_logprob_gpu = torch.zeros((1,), dtype=torch.int32)
|
||||
|
||||
ngram_embedding_info = (
|
||||
NgramEmbeddingInfo(
|
||||
token_table=ne_token_table,
|
||||
column_starts=torch.zeros([max_bs], dtype=torch.int32),
|
||||
req_lens=torch.ones([max_bs], dtype=torch.int32),
|
||||
out_column_starts=torch.zeros([max_bs], dtype=torch.int32),
|
||||
out_req_lens=torch.ones([max_bs], dtype=torch.int32),
|
||||
skip_token_table_update=torch.zeros([max_bs], dtype=torch.bool),
|
||||
)
|
||||
if ne_token_table is not None
|
||||
else None
|
||||
)
|
||||
|
||||
if envs.SGLANG_KV_CANARY_ENABLE_TOKEN_ORACLE.get():
|
||||
rids_int = torch.zeros((max_bs,), dtype=torch.int64)
|
||||
bootstrap_room_ids_int = torch.full((max_bs,), -1, dtype=torch.int64)
|
||||
else:
|
||||
rids_int = None
|
||||
bootstrap_room_ids_int = None
|
||||
|
||||
seq_lens_cpu = torch.full(
|
||||
(max_bs,),
|
||||
seq_len_fill_value,
|
||||
dtype=torch.int64,
|
||||
device="cpu",
|
||||
)
|
||||
|
||||
return SimpleNamespace(
|
||||
input_ids=input_ids,
|
||||
input_embeds=input_embeds,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
seq_lens_cpu=seq_lens_cpu,
|
||||
out_cache_loc=out_cache_loc,
|
||||
positions=positions,
|
||||
mrope_positions=mrope_positions,
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
custom_mask=custom_mask,
|
||||
next_token_logits_buffer=next_token_logits_buffer,
|
||||
mamba_track_indices=mamba_track_indices,
|
||||
mamba_track_mask=mamba_track_mask,
|
||||
encoder_lens=encoder_lens,
|
||||
global_num_tokens_gpu=global_num_tokens_gpu,
|
||||
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob_gpu,
|
||||
pp_proxy_tensors=pp_proxy_tensors,
|
||||
ngram_embedding_info=ngram_embedding_info,
|
||||
rids_int=rids_int,
|
||||
bootstrap_room_ids_int=bootstrap_room_ids_int,
|
||||
)
|
||||
|
||||
|
||||
class BaseRunner(ABC):
|
||||
def __init__(self, model_runner: ModelRunner) -> None:
|
||||
self.model_runner = model_runner
|
||||
self.device = model_runner.device
|
||||
self.device_module = torch.get_device_module(self.device)
|
||||
self.tp_size = model_runner.server_args.tp_size
|
||||
self.dp_size = model_runner.server_args.dp_size
|
||||
self.pp_size = model_runner.server_args.pp_size
|
||||
self.enable_pdmux = model_runner.server_args.enable_pdmux
|
||||
self.enable_return_hidden_states = (
|
||||
model_runner.server_args.enable_return_hidden_states
|
||||
)
|
||||
self.attn_tp_size = get_parallel().attn_tp_size
|
||||
self.attn_tp_rank = get_parallel().attn_tp_rank
|
||||
self.tbo_plugin = TboCudaGraphRunnerPlugin()
|
||||
|
||||
def warmup(self) -> None:
|
||||
"""Run kernel warmup + autotune once, gated by mr._kernel_warmed_up."""
|
||||
mr = self.model_runner
|
||||
if getattr(mr, "_kernel_warmed_up", False):
|
||||
return
|
||||
mr._kernel_warmed_up = True
|
||||
|
||||
if mr.device != "cuda":
|
||||
return
|
||||
|
||||
self._pre_initialize_flashinfer_allreduce_workspace()
|
||||
|
||||
if should_run_flashinfer_autotune(self.model_runner):
|
||||
buffers, batch_size = self._autotune_buffers()
|
||||
assert (
|
||||
buffers is not None
|
||||
), "_autotune_buffers() must return a reusable buffer set for autotune"
|
||||
self._flashinfer_autotune(buffers=buffers, batch_size=batch_size)
|
||||
|
||||
if (
|
||||
envs.SGLANG_PP_PARALLEL_DEEPGEMM_WARMUP.get()
|
||||
and deep_gemm_wrapper.ENABLE_JIT_DEEPGEMM
|
||||
and mr.pp_size > 1
|
||||
and not mr.spec_algorithm.is_speculative()
|
||||
):
|
||||
from sglang.srt.layers.deep_gemm_wrapper.compile_utils import (
|
||||
pp_parallel_deep_gemm_warmup,
|
||||
)
|
||||
|
||||
pp_parallel_deep_gemm_warmup(self)
|
||||
|
||||
def _pre_initialize_flashinfer_allreduce_workspace(self):
|
||||
"""Allocate flashinfer allreduce workspaces; must run before CG capture
|
||||
to keep broadcasts/barriers outside the capture context (else deadlock
|
||||
with custom_all_reduce.register_graph_buffers).
|
||||
"""
|
||||
mr = self.model_runner
|
||||
if mr.server_args.flashinfer_allreduce_fusion_backend is None:
|
||||
return
|
||||
|
||||
from sglang.srt.layers.communicator import FUSE_ALLREDUCE_MAX_BATCH_SIZE
|
||||
from sglang.srt.layers.flashinfer_comm_fusion import pre_initialize_workspaces
|
||||
|
||||
pre_initialize_workspaces(
|
||||
max_token_num=FUSE_ALLREDUCE_MAX_BATCH_SIZE,
|
||||
hidden_dim=mr.model_config.hidden_size,
|
||||
dtype=mr.dtype,
|
||||
)
|
||||
|
||||
def _flashinfer_autotune(self, *, buffers, batch_size):
|
||||
"""Run flashinfer autotune.
|
||||
|
||||
buffers / batch_size: a prepared static decode-buffer set and its bs,
|
||||
reused for the dummy forward instead of allocating a throwaway set.
|
||||
Supplied by warmup() (the decode runner's captured buffers when a graph
|
||||
runner exists; a freshly-allocated dummy set in the eager path).
|
||||
"""
|
||||
mr = self.model_runner
|
||||
canary_run_ctx = (
|
||||
c.with_active_single_forward_manager(0)
|
||||
if (c := mr.canary_manager) is not None
|
||||
else empty_context()
|
||||
)
|
||||
|
||||
def forward_fn():
|
||||
self._dummy_run(
|
||||
batch_size=batch_size,
|
||||
buffers=buffers,
|
||||
run_ctx=canary_run_ctx,
|
||||
)
|
||||
|
||||
run_flashinfer_autotune_forward(self.model_runner, forward_fn, skip_logits=True)
|
||||
|
||||
def _alloc_dummy_decode_buffers(self, max_bs: int, *, num_tokens_per_bs: int = 1):
|
||||
"""Allocate one static decode-buffer set for a dummy forward, sized to
|
||||
(max_bs, max_bs * num_tokens_per_bs).
|
||||
|
||||
The PP-parallel DeepGEMM warmup sweeps batch sizes far larger than any
|
||||
runner's max_bs (up to ~n_sms*block_m), so no pre-allocated runner buffer
|
||||
set fits; it builds one here and hands it to _dummy_run (reused across the
|
||||
sweep; _dummy_run slices it per shape). Eager FlashInfer autotune also
|
||||
allocates decode-shaped scratch buffers here. Decode cuda-graph autotune
|
||||
reuses the captured runner buffers instead.
|
||||
"""
|
||||
mr = self.model_runner
|
||||
return _allocate_decode_buffers(
|
||||
device=mr.device,
|
||||
max_bs=max_bs,
|
||||
max_num_token=max_bs * num_tokens_per_bs,
|
||||
hidden_size=mr.model_config.hidden_size,
|
||||
vocab_size=mr.model_config.vocab_size,
|
||||
dtype=mr.model_config.dtype,
|
||||
dp_size=mr.server_args.dp_size,
|
||||
pp_size=mr.server_args.pp_size,
|
||||
is_encoder_decoder=mr.model_config.is_encoder_decoder,
|
||||
require_mlp_tp_gather=require_mlp_tp_gather(mr.server_args),
|
||||
seq_len_fill_value=mr.attn_backend.get_cuda_graph_seq_len_fill_value(),
|
||||
encoder_len_fill_value=(
|
||||
getattr(mr.model_config.hf_config, "max_source_positions", 0)
|
||||
if mr.model_config.is_encoder_decoder
|
||||
else 0
|
||||
),
|
||||
num_tokens_per_bs=num_tokens_per_bs,
|
||||
cache_loc_dtype=torch.int64,
|
||||
enable_mamba_track=False,
|
||||
ne_token_table=mr.token_table if mr.use_ngram_embedding else None,
|
||||
hc_hidden_size=getattr(mr.model_config, "hc_hidden_size", None),
|
||||
pp_proxy_topk_size=mr.get_pp_proxy_topk_size(),
|
||||
)
|
||||
|
||||
def _dummy_run(
|
||||
self,
|
||||
batch_size: int,
|
||||
run_ctx=None,
|
||||
forward_mode_override: Optional[ForwardMode] = None,
|
||||
*,
|
||||
buffers,
|
||||
):
|
||||
"""Run a dummy forward pass for warmup/profiling.
|
||||
|
||||
forward_mode_override forces EXTEND/DECODE regardless of
|
||||
is_generation (used by the PP-parallel DeepGEMM warmup).
|
||||
|
||||
buffers: a prepared static buffer set (or lightweight adapter exposing
|
||||
the same fields), sized >= this dummy shape, which _dummy_run slices to
|
||||
(batch_size, num_tokens). The caller owns the shape and the allocation --
|
||||
the flashinfer autotune reuses an existing runner's buffers via
|
||||
_autotune_buffers (the eager input registry, or the decode cuda-graph
|
||||
runner's captured buffers); the PP-DeepGEMM warmup builds one via
|
||||
_alloc_dummy_decode_buffers. _dummy_run never allocates and never re-pads
|
||||
(autotune must run at the reused shape; the PP warmup pre-pads and sizes
|
||||
its buffer to match). next_token_logits_buffer is optional -- a live
|
||||
autotune forward returns logits fresh, so the eager-reuse path passes
|
||||
None (only the PP warmup set still carries one).
|
||||
"""
|
||||
mr = self.model_runner
|
||||
if forward_mode_override is not None:
|
||||
capture_forward_mode = forward_mode_override
|
||||
elif mr.is_generation:
|
||||
capture_forward_mode = ForwardMode.DECODE
|
||||
else:
|
||||
capture_forward_mode = ForwardMode.EXTEND
|
||||
capture_hidden_mode = CaptureHiddenMode.NULL
|
||||
num_tokens_per_bs = 1
|
||||
if mr.spec_algorithm.is_speculative():
|
||||
if mr.is_draft_worker:
|
||||
if not mr.spec_algorithm.supports_target_verify_for_draft():
|
||||
raise RuntimeError("This should not happen")
|
||||
capture_forward_mode = ForwardMode.TARGET_VERIFY
|
||||
num_tokens_per_bs = (
|
||||
mr.spec_algorithm.get_num_tokens_per_bs_for_target_verify(
|
||||
mr.server_args.speculative_num_draft_tokens, mr.is_draft_worker
|
||||
)
|
||||
)
|
||||
|
||||
if mr.server_args.enable_return_hidden_states:
|
||||
capture_hidden_mode = CaptureHiddenMode.FULL
|
||||
|
||||
num_tokens = batch_size * num_tokens_per_bs
|
||||
|
||||
# Caller owns the shape: passes a static buffer >= the dummy shape; no
|
||||
# allocation, no re-padding (would overflow the reused buffers).
|
||||
assert (
|
||||
buffers is not None
|
||||
and num_tokens <= buffers.input_ids.shape[0]
|
||||
and batch_size <= buffers.seq_lens.shape[0]
|
||||
), (
|
||||
f"_dummy_run needs a static buffer >= (num_tokens={num_tokens}, "
|
||||
f"batch_size={batch_size}); got "
|
||||
+ (
|
||||
"None"
|
||||
if buffers is None
|
||||
else f"(input_ids={buffers.input_ids.shape[0]}, "
|
||||
f"seq_lens={buffers.seq_lens.shape[0]})"
|
||||
)
|
||||
)
|
||||
|
||||
seq_len_fill_value = mr.attn_backend.get_cuda_graph_seq_len_fill_value()
|
||||
|
||||
if get_flags().capture.enable_torch_compile:
|
||||
set_torch_compile_config()
|
||||
should_disable_torch_compile = not getattr(
|
||||
mr.model, "_can_torch_compile", True
|
||||
)
|
||||
if should_disable_torch_compile:
|
||||
log_info_on_rank0(
|
||||
logger,
|
||||
"Transformers backend model reports it is not torch.compile "
|
||||
"compatible (e.g. dynamic rope scaling). Disabling torch.compile.",
|
||||
)
|
||||
get_flags().capture.enable_torch_compile = False
|
||||
|
||||
# NOTE: aux hidden state capture (eagle3/dflash) is already
|
||||
# configured by init_aux_hidden_state_capture() in initialize().
|
||||
|
||||
require_mlp_tp_gather_ = require_mlp_tp_gather(mr.server_args)
|
||||
if require_gathered_buffer(mr.server_args):
|
||||
assert require_mlp_tp_gather_ or require_attn_tp_gather(mr.server_args)
|
||||
|
||||
input_ids = buffers.input_ids[:num_tokens]
|
||||
positions = buffers.positions[:num_tokens]
|
||||
out_cache_loc = buffers.out_cache_loc[:num_tokens]
|
||||
# Eager-reuse drops the logits buffer; only buffer sets that carry one slice it.
|
||||
next_token_logits_buffer = (
|
||||
buffers.next_token_logits_buffer[:num_tokens]
|
||||
if buffers.next_token_logits_buffer is not None
|
||||
else None
|
||||
)
|
||||
mrope_positions = buffers.mrope_positions[:, :num_tokens]
|
||||
req_pool_indices = buffers.req_pool_indices[:batch_size]
|
||||
seq_lens = buffers.seq_lens[:batch_size]
|
||||
seq_lens_cpu = buffers.seq_lens_cpu[:batch_size]
|
||||
encoder_lens = (
|
||||
buffers.encoder_lens[:batch_size]
|
||||
if buffers.encoder_lens is not None
|
||||
else None
|
||||
)
|
||||
|
||||
buffers.num_token_non_padded[...] = num_tokens
|
||||
|
||||
# For extend mode
|
||||
if capture_forward_mode == ForwardMode.EXTEND:
|
||||
extend_prefix_lens_cpu = [0] * batch_size
|
||||
extend_seq_lens_cpu = [seq_len_fill_value] * batch_size
|
||||
extend_num_tokens = num_tokens
|
||||
extend_seq_lens = torch.full(
|
||||
(batch_size,), seq_len_fill_value, dtype=torch.int32, device=mr.device
|
||||
)
|
||||
extend_prefix_lens = torch.zeros(
|
||||
(batch_size,), dtype=torch.int32, device=mr.device
|
||||
)
|
||||
extend_start_loc = torch.arange(
|
||||
0, num_tokens, num_tokens_per_bs, dtype=torch.int32, device=mr.device
|
||||
)
|
||||
else:
|
||||
extend_prefix_lens_cpu = None
|
||||
extend_seq_lens_cpu = None
|
||||
extend_num_tokens = None
|
||||
extend_seq_lens = None
|
||||
extend_prefix_lens = None
|
||||
extend_start_loc = None
|
||||
|
||||
if mr.server_args.pp_size > 1:
|
||||
# PP0 already cp-split hidden_states before send.
|
||||
pp_hidden_tokens = num_tokens
|
||||
if (
|
||||
capture_forward_mode == ForwardMode.EXTEND
|
||||
and mr.pp_rank != 0
|
||||
and mr.attn_cp_size > 1
|
||||
):
|
||||
pp_hidden_tokens = num_tokens // mr.attn_cp_size
|
||||
pp_proxy_tensors = PPProxyTensors(
|
||||
{k: v[:pp_hidden_tokens] for k, v in buffers.pp_proxy_tensors.items()}
|
||||
)
|
||||
|
||||
if require_mlp_tp_gather_:
|
||||
global_num_tokens_cpu = [num_tokens] * mr.server_args.dp_size
|
||||
elif require_attn_tp_gather(mr.server_args):
|
||||
global_num_tokens_cpu = [num_tokens]
|
||||
else:
|
||||
global_num_tokens_cpu = None
|
||||
|
||||
if global_num_tokens_cpu is not None:
|
||||
global_dp_buffer_len = sum(global_num_tokens_cpu)
|
||||
num_tokens_tensor = torch.tensor(
|
||||
global_num_tokens_cpu, dtype=torch.int32, device=mr.device
|
||||
)
|
||||
buffers.global_num_tokens_gpu.copy_(num_tokens_tensor)
|
||||
buffers.global_num_tokens_for_logprob_gpu.copy_(num_tokens_tensor)
|
||||
else:
|
||||
global_dp_buffer_len = None
|
||||
global_num_tokens_cpu = None
|
||||
|
||||
spec_info = create_dummy_verify_input(
|
||||
mr.spec_algorithm,
|
||||
mr.server_args,
|
||||
buffers.custom_mask,
|
||||
num_tokens_per_bs,
|
||||
mr.is_draft_worker,
|
||||
)
|
||||
if spec_info is not None and (
|
||||
mr.spec_algorithm.is_eagle() or mr.spec_algorithm.is_standalone()
|
||||
):
|
||||
# MTP models (e.g. deepseek_nextn) read spec_info.hidden_states
|
||||
# during forward; provide a dummy so warmup doesn't crash.
|
||||
spec_info.hidden_states = torch.zeros(
|
||||
(num_tokens, mr.model_config.hidden_size),
|
||||
dtype=mr.dtype,
|
||||
device=mr.device,
|
||||
)
|
||||
if capture_hidden_mode != CaptureHiddenMode.FULL:
|
||||
capture_hidden_mode = (
|
||||
spec_info.capture_hidden_mode if spec_info else CaptureHiddenMode.NULL
|
||||
)
|
||||
|
||||
if mr.server_args.enable_lora:
|
||||
lora_ids = [None] * batch_size
|
||||
else:
|
||||
lora_ids = None
|
||||
|
||||
forward_batch = ForwardBatch(
|
||||
forward_mode=capture_forward_mode,
|
||||
batch_size=batch_size,
|
||||
input_ids=input_ids,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
seq_lens_cpu=seq_lens_cpu,
|
||||
next_token_logits_buffer=next_token_logits_buffer,
|
||||
orig_seq_lens=seq_lens,
|
||||
out_cache_loc=out_cache_loc,
|
||||
seq_lens_sum=seq_lens.sum().item(),
|
||||
encoder_lens=encoder_lens,
|
||||
return_logprob=False,
|
||||
positions=positions,
|
||||
extend_num_tokens=extend_num_tokens,
|
||||
extend_seq_lens=extend_seq_lens,
|
||||
extend_prefix_lens=extend_prefix_lens,
|
||||
extend_start_loc=extend_start_loc,
|
||||
extend_prefix_lens_cpu=extend_prefix_lens_cpu,
|
||||
extend_seq_lens_cpu=extend_seq_lens_cpu,
|
||||
global_num_tokens_gpu=buffers.global_num_tokens_gpu,
|
||||
global_num_tokens_cpu=global_num_tokens_cpu,
|
||||
global_num_tokens_for_logprob_gpu=buffers.global_num_tokens_for_logprob_gpu,
|
||||
dp_padding_mode=DpPaddingMode.get_default_mode_in_cuda_graph(),
|
||||
global_dp_buffer_len=global_dp_buffer_len,
|
||||
mrope_positions=mrope_positions,
|
||||
spec_algorithm=mr.spec_algorithm,
|
||||
spec_info=spec_info,
|
||||
capture_hidden_mode=capture_hidden_mode,
|
||||
num_token_non_padded=buffers.num_token_non_padded,
|
||||
global_forward_mode=capture_forward_mode,
|
||||
lora_ids=lora_ids,
|
||||
)
|
||||
if buffers.ngram_embedding_info is not None:
|
||||
forward_batch.ngram_embedding_info = buffers.ngram_embedding_info.slice(
|
||||
batch_size
|
||||
)
|
||||
|
||||
if lora_ids is not None:
|
||||
mr.lora_manager.prepare_lora_batch(forward_batch)
|
||||
|
||||
mr.attn_backend.init_forward_metadata(forward_batch)
|
||||
|
||||
def run_once():
|
||||
forward_batch.dp_local_start_pos = forward_batch.dp_local_num_tokens = None
|
||||
set_dp_buffer_len(
|
||||
global_dp_buffer_len,
|
||||
num_tokens,
|
||||
forward_batch.dp_padding_mode.is_max_len(),
|
||||
global_num_tokens_cpu,
|
||||
)
|
||||
set_is_extend_in_batch(False)
|
||||
|
||||
kwargs = {}
|
||||
if (
|
||||
mr.server_args.pp_size > 1
|
||||
and "pp_proxy_tensors" in inspect.signature(mr.model.forward).parameters
|
||||
):
|
||||
kwargs["pp_proxy_tensors"] = PPProxyTensors(
|
||||
{k: v.clone() for k, v in pp_proxy_tensors.tensors.items()}
|
||||
)
|
||||
if not mr.is_generation:
|
||||
kwargs["get_embedding"] = True
|
||||
|
||||
logits_output_or_pp_proxy_tensors = mr.model.forward(
|
||||
input_ids,
|
||||
forward_batch.positions,
|
||||
forward_batch,
|
||||
**kwargs,
|
||||
)
|
||||
return logits_output_or_pp_proxy_tensors
|
||||
|
||||
torch.get_device_module(mr.device).synchronize()
|
||||
mr.tp_group.barrier()
|
||||
with forward_context(ForwardContext(attn_backend=mr.attn_backend)):
|
||||
with torch.inference_mode(), run_ctx or empty_context():
|
||||
run_once()
|
||||
|
||||
def _autotune_buffers(self) -> Tuple[Optional[Any], Optional[int]]:
|
||||
"""Return (buffers, bs) for the autotune dummy forward to reuse; the
|
||||
EagerRunner and DecodeCudaGraphRunner override this."""
|
||||
return None, None
|
||||
|
||||
@abstractmethod
|
||||
def can_run_graph(self, forward_batch: ForwardBatch) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
def load_batch(
|
||||
self,
|
||||
forward_batch: ForwardBatch,
|
||||
**kwargs,
|
||||
) -> Any: ...
|
||||
|
||||
@abstractmethod
|
||||
def execute(
|
||||
self,
|
||||
forward_batch: ForwardBatch,
|
||||
**kwargs,
|
||||
) -> Any: ...
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,414 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""No-cuda-graph phase runner; the eager dual of BaseCudaGraphRunner."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from dataclasses import replace
|
||||
from typing import TYPE_CHECKING, Any, Tuple, Union
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.dllm.config import DllmConfig
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.cp.utils import (
|
||||
cp_gather_after_forward,
|
||||
cp_split_before_forward,
|
||||
is_cp_v2_active,
|
||||
prepare_cp_forward,
|
||||
)
|
||||
from sglang.srt.layers.pooler import EmbeddingPoolerOutput
|
||||
from sglang.srt.model_executor.cuda_graph_buffer_registry import (
|
||||
build_eager_registry,
|
||||
)
|
||||
from sglang.srt.model_executor.forward_batch_deepseek_mha_mixin import (
|
||||
create_chunked_prefix_cache_kv_indices,
|
||||
)
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
|
||||
from sglang.srt.model_executor.forward_context import (
|
||||
ForwardContext,
|
||||
forward_context,
|
||||
get_req_to_token_pool,
|
||||
get_token_to_kv_pool,
|
||||
)
|
||||
from sglang.srt.model_executor.runner.base_runner import BaseRunner
|
||||
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph import (
|
||||
enable_tc_piecewise_cuda_graph,
|
||||
set_tc_piecewise_forward_context,
|
||||
)
|
||||
from sglang.srt.utils import is_hip
|
||||
from sglang.srt.utils.common import ceil_align, require_mlp_sync
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_is_hip = is_hip()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
|
||||
|
||||
class EagerRunner(BaseRunner):
|
||||
def __init__(self, model_runner: ModelRunner) -> None:
|
||||
super().__init__(model_runner)
|
||||
mr = model_runner
|
||||
sa = mr.server_args
|
||||
# Built first so the cg runners coalesce onto its buffers via the shared
|
||||
# input pool; size to the largest tokens/req across modes the worker hits.
|
||||
num_tokens_per_bs = 1
|
||||
if mr.spec_algorithm.is_speculative():
|
||||
# speculative_adaptive can grow draft tokens at runtime; size to the max.
|
||||
num_draft_tokens = sa.max_speculative_num_draft_tokens or 1
|
||||
if mr.is_draft_worker:
|
||||
num_tokens_per_bs = max(
|
||||
sa.speculative_eagle_topk or 1,
|
||||
num_draft_tokens,
|
||||
(
|
||||
2 * (sa.speculative_num_steps or 0)
|
||||
if sa.enable_multi_layer_eagle
|
||||
else 0
|
||||
),
|
||||
)
|
||||
else:
|
||||
num_tokens_per_bs = (
|
||||
mr.spec_algorithm.get_num_tokens_per_bs_for_target_verify(
|
||||
num_draft_tokens, mr.is_draft_worker
|
||||
)
|
||||
)
|
||||
else:
|
||||
dllm_config = DllmConfig.from_server_args(sa)
|
||||
if dllm_config is not None:
|
||||
# dLLM runs block_size tokens/request (DLLM_EXTEND).
|
||||
num_tokens_per_bs = dllm_config.block_size
|
||||
max_bs = mr.max_running_requests
|
||||
if (
|
||||
mr.is_draft_worker
|
||||
and mr.spec_algorithm.is_frozen_kv_mtp()
|
||||
and sa.speculative_eagle_topk > 1
|
||||
):
|
||||
# Frozen-KV MTP expands the draft batch by topk on the bs axis
|
||||
# (expand_for_topk_draft) before the eager fallback.
|
||||
max_bs *= sa.speculative_eagle_topk
|
||||
# Mirror prepare_mlp_sync_batch padding so the registry holds what load_batch copies.
|
||||
if require_mlp_sync(sa):
|
||||
from sglang.srt.layers.utils.cp_utils import get_cp_padding_align_size
|
||||
|
||||
max_bs = ceil_align(max_bs, self.attn_tp_size)
|
||||
max_bs = ceil_align(max_bs, get_cp_padding_align_size())
|
||||
prefill_ceiling = max(mr.max_total_num_tokens, sa.max_prefill_buffer_tokens())
|
||||
max_num_token = max(prefill_ceiling, max_bs * num_tokens_per_bs)
|
||||
if require_mlp_sync(sa):
|
||||
max_num_token = ceil_align(max_num_token, self.attn_tp_size)
|
||||
max_num_token = ceil_align(max_num_token, get_cp_padding_align_size())
|
||||
self._eager_max_bs = max_bs
|
||||
self._eager_num_tokens_per_bs = num_tokens_per_bs
|
||||
is_encoder_decoder = mr.model_config.is_encoder_decoder
|
||||
self._eager_registry = build_eager_registry(
|
||||
device=mr.device,
|
||||
max_bs=max_bs,
|
||||
max_num_token=max_num_token,
|
||||
cache_loc_dtype=torch.int64,
|
||||
enable_mamba_track=(
|
||||
sa.enable_mamba_extra_buffer() and mr.spec_algorithm.is_none()
|
||||
),
|
||||
is_encoder_decoder=is_encoder_decoder,
|
||||
encoder_len_fill_value=(
|
||||
getattr(mr.model_config.hf_config, "max_source_positions", 0)
|
||||
if is_encoder_decoder
|
||||
else 0
|
||||
),
|
||||
encoder_lens_dtype=(
|
||||
torch.int64 if torch.device(mr.device).type == "cpu" else torch.int32
|
||||
),
|
||||
dp_size=sa.dp_size,
|
||||
)
|
||||
# Eager has no capture step, so warm up here (run-once via mr._kernel_warmed_up).
|
||||
self.warmup()
|
||||
|
||||
def _autotune_buffers(self) -> Tuple[Any, int]:
|
||||
"""Decode-shaped dummy buffers (bs * num_tokens_per_bs) for the warmup
|
||||
flashinfer-autotune forward.
|
||||
|
||||
flashinfer's MoE autotuner times candidate tactics against the buffer it
|
||||
is given, so it must match the live decode shape for the cached tactic to
|
||||
be optimal at decode. The eager input registry spans the prefill token
|
||||
ceiling; the dummy run only needs the decode-sized slice.
|
||||
"""
|
||||
mr = self.model_runner
|
||||
num_tokens_per_bs = 1
|
||||
if mr.spec_algorithm.is_speculative():
|
||||
num_tokens_per_bs = (
|
||||
mr.spec_algorithm.get_num_tokens_per_bs_for_target_verify(
|
||||
mr.server_args.speculative_num_draft_tokens, mr.is_draft_worker
|
||||
)
|
||||
)
|
||||
return (
|
||||
self._alloc_dummy_decode_buffers(
|
||||
self._eager_max_bs, num_tokens_per_bs=num_tokens_per_bs
|
||||
),
|
||||
self._eager_max_bs,
|
||||
)
|
||||
|
||||
def can_run_graph(self, forward_batch: ForwardBatch) -> bool:
|
||||
# Eager never runs a cuda graph; callers dispatch on isinstance(...,
|
||||
# EagerRunner) and must not route an eager batch into a replay branch.
|
||||
return False
|
||||
|
||||
def load_batch(
|
||||
self, forward_batch: ForwardBatch, pp_proxy_tensors=None, **kwargs
|
||||
) -> ForwardBatch:
|
||||
"""Copy the live batch into the fixed-max eager static buffers (sliced to
|
||||
this batch's shape) — the eager counterpart of the cuda-graph runners'
|
||||
load_batch."""
|
||||
if envs.SGLANG_EAGER_INPUT_NO_COPY.get():
|
||||
return replace(forward_batch)
|
||||
raw_bs = forward_batch.batch_size
|
||||
if forward_batch.input_ids is not None:
|
||||
raw_num_tokens = forward_batch.input_ids.shape[0]
|
||||
elif forward_batch.input_embeds is not None:
|
||||
raw_num_tokens = forward_batch.input_embeds.shape[0]
|
||||
else:
|
||||
raw_num_tokens = 0
|
||||
registry = self._eager_registry
|
||||
registry.fill_from(
|
||||
forward_batch,
|
||||
raw_bs=raw_bs,
|
||||
padded_bs=raw_bs,
|
||||
raw_num_tokens=raw_num_tokens,
|
||||
padded_num_tokens=raw_num_tokens,
|
||||
pp_proxy_tensors=pp_proxy_tensors,
|
||||
)
|
||||
return registry.extract_buffer(
|
||||
padded_bs=raw_bs,
|
||||
padded_num_tokens=raw_num_tokens,
|
||||
forward_batch_template=forward_batch,
|
||||
)
|
||||
|
||||
def execute(
|
||||
self, forward_batch: ForwardBatch, pp_proxy_tensors=None, **kwargs
|
||||
) -> Any:
|
||||
mode = forward_batch.forward_mode
|
||||
if mode.is_decode():
|
||||
return self._execute_decode(forward_batch, pp_proxy_tensors)
|
||||
if mode.is_idle():
|
||||
return self._execute_idle(forward_batch, pp_proxy_tensors)
|
||||
if mode.is_extend(include_draft_extend_v2=True):
|
||||
return self._execute_extend(forward_batch, pp_proxy_tensors)
|
||||
raise ValueError(f"Invalid forward mode for eager runner: {mode}")
|
||||
|
||||
def _resolve_decode_pdmux(
|
||||
self,
|
||||
) -> Tuple[Any, contextlib.AbstractContextManager]:
|
||||
"""Resolve the (attn_backend, forward_context) the eager decode forward
|
||||
runs under. PDmux selects a per-stream backend and publishes it via an
|
||||
active ForwardContext; non-pdmux uses attn_backend + the ambient ctx."""
|
||||
model_runner = self.model_runner
|
||||
if self.enable_pdmux:
|
||||
return model_runner.decode_attn_backend, forward_context(
|
||||
ForwardContext(attn_backend=model_runner.decode_attn_backend)
|
||||
)
|
||||
return model_runner.attn_backend, contextlib.nullcontext()
|
||||
|
||||
def _execute_decode(
|
||||
self,
|
||||
forward_batch: ForwardBatch,
|
||||
pp_proxy_tensors=None,
|
||||
) -> Union[LogitsProcessorOutput, PPProxyTensors]:
|
||||
model_runner = self.model_runner
|
||||
enable_pdmux = self.enable_pdmux
|
||||
attn_backend, pdmux_ctx = self._resolve_decode_pdmux()
|
||||
if not enable_pdmux:
|
||||
forward_batch = self.load_batch(forward_batch, pp_proxy_tensors)
|
||||
if forward_batch.needs_forward_metadata_init():
|
||||
if hasattr(model_runner.model, "prepare_forward_batch"):
|
||||
# Prepare model-specific attention metadata before planning,
|
||||
# e.g. Moss-VL's prefill cross-attention custom mask.
|
||||
model_runner.model.prepare_forward_batch(forward_batch)
|
||||
attn_backend.init_forward_metadata(forward_batch)
|
||||
# FIXME: add pp_proxy_tensors arg to all models
|
||||
kwargs = model_runner._pp_kwargs(pp_proxy_tensors)
|
||||
|
||||
ctx = (
|
||||
model_runner.device_timer.wrap(metadata={"category": "decode"})
|
||||
if model_runner.device_timer
|
||||
else contextlib.nullcontext()
|
||||
)
|
||||
|
||||
with ctx, pdmux_ctx:
|
||||
return model_runner.model.forward(
|
||||
forward_batch.input_ids,
|
||||
forward_batch.positions,
|
||||
forward_batch,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _execute_extend(
|
||||
self,
|
||||
forward_batch: ForwardBatch,
|
||||
pp_proxy_tensors=None,
|
||||
) -> Union[LogitsProcessorOutput, PPProxyTensors, EmbeddingPoolerOutput]:
|
||||
model_runner = self.model_runner
|
||||
kwargs = model_runner._extend_forward_kwargs(forward_batch, pp_proxy_tensors)
|
||||
|
||||
if not self.enable_pdmux:
|
||||
forward_batch = self.load_batch(forward_batch, pp_proxy_tensors)
|
||||
|
||||
if forward_batch.needs_forward_metadata_init():
|
||||
if hasattr(model_runner.model, "prepare_context_parallel_metadata_for_dcp"):
|
||||
# prepare kv cache buffer for dcp to gather kv cache
|
||||
forward_batch.attn_dcp_metadata = (
|
||||
model_runner.model.prepare_context_parallel_metadata_for_dcp(
|
||||
forward_batch.seq_lens,
|
||||
forward_batch.extend_prefix_lens,
|
||||
forward_batch.extend_prefix_lens_cpu,
|
||||
forward_batch.extend_seq_lens,
|
||||
forward_batch.req_pool_indices,
|
||||
get_req_to_token_pool().req_to_token,
|
||||
forward_batch.seq_lens_sum,
|
||||
get_token_to_kv_pool().get_kv_buffer_shape()[0],
|
||||
model_runner.kv_cache_dtype,
|
||||
model_runner.device,
|
||||
create_chunked_prefix_cache_kv_indices,
|
||||
)
|
||||
)
|
||||
if hasattr(model_runner.model, "prepare_forward_batch"):
|
||||
# Prepare model-specific attention metadata before planning,
|
||||
# e.g. Moss-VL's prefill cross-attention custom mask.
|
||||
model_runner.model.prepare_forward_batch(forward_batch)
|
||||
model_runner.attn_backend.init_forward_metadata(forward_batch)
|
||||
|
||||
cp_v2_active = is_cp_v2_active(forward_batch)
|
||||
forward_positions = forward_batch.positions
|
||||
if cp_v2_active:
|
||||
prepare_cp_forward(forward_batch)
|
||||
complete_hidden_states = kwargs.get("input_embeds")
|
||||
if complete_hidden_states is None:
|
||||
embed_layer = model_runner.model.get_input_embeddings()
|
||||
complete_hidden_states = embed_layer(forward_batch.input_ids)
|
||||
sharded_hidden_states, sharded_positions = cp_split_before_forward(
|
||||
complete_hidden_states,
|
||||
forward_batch.positions,
|
||||
forward_batch,
|
||||
)
|
||||
kwargs["input_embeds"] = sharded_hidden_states
|
||||
forward_positions = sharded_positions
|
||||
else:
|
||||
forward_batch.attn_cp_metadata = None
|
||||
|
||||
category = (
|
||||
"target_verify"
|
||||
if forward_batch.forward_mode.is_target_verify()
|
||||
else "extend"
|
||||
)
|
||||
ctx = (
|
||||
model_runner.device_timer.wrap(metadata={"category": category})
|
||||
if model_runner.device_timer
|
||||
else contextlib.nullcontext()
|
||||
)
|
||||
with ctx:
|
||||
pcg_runner = model_runner.prefill_cuda_graph_runner
|
||||
if (
|
||||
_is_hip
|
||||
and pcg_runner is not None
|
||||
and not isinstance(pcg_runner, EagerRunner)
|
||||
and not cp_v2_active
|
||||
):
|
||||
# HIP PCG eager fallback: enter the PCG context so Dynamo guards
|
||||
# and PCG-specific MoE/attention paths stay consistent.
|
||||
with (
|
||||
enable_tc_piecewise_cuda_graph(),
|
||||
set_tc_piecewise_forward_context(
|
||||
forward_batch,
|
||||
model_runner.attention_layers,
|
||||
getattr(model_runner.model, "quant_config", None),
|
||||
model_runner.moe_layers,
|
||||
model_runner.moe_fusions,
|
||||
dsa_indexers=model_runner.dsa_indexers,
|
||||
),
|
||||
):
|
||||
ret = model_runner.model.forward(
|
||||
forward_batch.input_ids,
|
||||
forward_positions,
|
||||
forward_batch,
|
||||
**kwargs,
|
||||
)
|
||||
elif cp_v2_active:
|
||||
# CP-V2: drive .model directly to gather across CP ranks before logits.
|
||||
hidden_states = model_runner.model.model(
|
||||
forward_batch.input_ids,
|
||||
forward_positions,
|
||||
forward_batch,
|
||||
input_embeds=kwargs.get("input_embeds"),
|
||||
pp_proxy_tensors=kwargs.get("pp_proxy_tensors"),
|
||||
)
|
||||
aux_hidden_states = None
|
||||
capture_aux_hidden_states = getattr(
|
||||
model_runner.model, "capture_aux_hidden_states", False
|
||||
)
|
||||
if capture_aux_hidden_states:
|
||||
hidden_states, aux_hidden_states = hidden_states
|
||||
if model_runner.model.pp_group.is_last_rank:
|
||||
hidden_states = cp_gather_after_forward(
|
||||
hidden_states,
|
||||
forward_batch,
|
||||
torch.cuda.current_stream(),
|
||||
)
|
||||
ret = model_runner.model.logits_processor(
|
||||
forward_batch.input_ids,
|
||||
hidden_states,
|
||||
model_runner.model.lm_head,
|
||||
forward_batch,
|
||||
aux_hidden_states,
|
||||
)
|
||||
elif capture_aux_hidden_states:
|
||||
ret = hidden_states, aux_hidden_states
|
||||
else:
|
||||
ret = hidden_states
|
||||
else:
|
||||
ret = model_runner.model.forward(
|
||||
forward_batch.input_ids,
|
||||
forward_positions,
|
||||
forward_batch,
|
||||
**kwargs,
|
||||
)
|
||||
return ret
|
||||
|
||||
def _execute_idle(
|
||||
self, forward_batch: ForwardBatch, pp_proxy_tensors=None
|
||||
) -> Union[LogitsProcessorOutput, PPProxyTensors]:
|
||||
model_runner = self.model_runner
|
||||
# Padded idle (DP-attn MLP sync) needs metadata reinit; unpadded must
|
||||
# drop stale forward_metadata to avoid an SWA use-after-free on req_pool.
|
||||
if forward_batch.batch_size > 0:
|
||||
if not self.enable_pdmux:
|
||||
forward_batch = self.load_batch(forward_batch, pp_proxy_tensors)
|
||||
model_runner.attn_backend.init_forward_metadata(forward_batch)
|
||||
else:
|
||||
model_runner.attn_backend.forward_metadata = None
|
||||
|
||||
kwargs = model_runner._pp_kwargs(pp_proxy_tensors)
|
||||
ctx = (
|
||||
model_runner.device_timer.wrap(metadata={"category": "idle"})
|
||||
if model_runner.device_timer
|
||||
else contextlib.nullcontext()
|
||||
)
|
||||
with ctx:
|
||||
return model_runner.model.forward(
|
||||
forward_batch.input_ids,
|
||||
forward_batch.positions,
|
||||
forward_batch,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,224 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
from sglang.srt.model_executor.runner.base_runner import BaseRunner
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def should_run_flashinfer_autotune(
|
||||
model_runner: ModelRunner, *, for_speculative_draft: bool = False
|
||||
) -> bool:
|
||||
"""Check if flashinfer autotune should be run."""
|
||||
mr = model_runner
|
||||
if mr.device != "cuda":
|
||||
return False
|
||||
if mr.server_args.disable_flashinfer_autotune:
|
||||
return False
|
||||
|
||||
# CuteDSL v1 (cutedsl runner + deepep a2a) bypasses MoeRunner and must not
|
||||
# be autotuned -- its _dummy_run would dispatch more tokens per rank than
|
||||
# SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK, tripping a DeepEP assert.
|
||||
# Read server_args directly to avoid depending on initialize_moe_config()
|
||||
# having already populated the MoE backend globals.
|
||||
if (
|
||||
mr.server_args.moe_runner_backend == "flashinfer_cutedsl"
|
||||
and mr.server_args.moe_a2a_backend == "deepep"
|
||||
):
|
||||
return False
|
||||
|
||||
backend_str = mr.server_args.moe_runner_backend
|
||||
|
||||
# TODO smor- support other cases for flashinfer autotune, such as, mamba backend
|
||||
|
||||
moe_needs_autotune = backend_str in [
|
||||
"flashinfer_trtllm",
|
||||
"flashinfer_trtllm_routed",
|
||||
"flashinfer_mxfp4",
|
||||
"flashinfer_cutedsl",
|
||||
"flashinfer_cutlass",
|
||||
]
|
||||
|
||||
from sglang.srt.layers.quantization.fp4_utils import (
|
||||
get_fp4_gemm_runner_backend,
|
||||
)
|
||||
|
||||
model_quantization = mr.model_config.quantization
|
||||
model_uses_fp4 = model_quantization in (
|
||||
"modelopt_fp4",
|
||||
"modelopt_mixed",
|
||||
)
|
||||
fp4_gemm_needs_autotune = model_uses_fp4 and (
|
||||
get_fp4_gemm_runner_backend().is_flashinfer_cutlass()
|
||||
or get_fp4_gemm_runner_backend().is_flashinfer_cutedsl()
|
||||
)
|
||||
|
||||
from sglang.srt.layers.quantization.fp8_utils import (
|
||||
get_fp8_gemm_runner_backend,
|
||||
)
|
||||
from sglang.srt.utils import is_sm100_supported
|
||||
|
||||
model_uses_modelopt_fp8 = model_quantization in (
|
||||
"modelopt",
|
||||
"modelopt_fp8",
|
||||
"modelopt_mixed",
|
||||
)
|
||||
# Online MXFP8 (microscaling) linears dispatch to flashinfer's
|
||||
# ``mm_mxfp8``, which the flashinfer fp8 autotune dummy run does not
|
||||
# exercise correctly -- it triggers an illegal memory access inside the
|
||||
# mxfp8 cutlass cubin. The mxfp8 gemm is fixed-config and needs no
|
||||
# tuning, so skip autotune for these models.
|
||||
model_uses_mxfp8 = "mxfp8" in (model_quantization or "")
|
||||
fp8_gemm_needs_autotune = not model_uses_mxfp8 and (
|
||||
get_fp8_gemm_runner_backend().is_flashinfer_cutlass()
|
||||
or (model_uses_modelopt_fp8 and is_sm100_supported())
|
||||
)
|
||||
|
||||
if not (moe_needs_autotune or fp4_gemm_needs_autotune or fp8_gemm_needs_autotune):
|
||||
return False
|
||||
|
||||
if torch.cuda.get_device_capability()[0] < 9:
|
||||
return False
|
||||
|
||||
if mr.spec_algorithm.is_speculative():
|
||||
return mr.is_draft_worker if for_speculative_draft else not mr.is_draft_worker
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def flashinfer_autotune_cache_path(model_runner: ModelRunner) -> Path:
|
||||
import flashinfer
|
||||
|
||||
mr = model_runner
|
||||
major, minor = torch.cuda.get_device_capability(mr.device)
|
||||
arch = f"sm{major}{minor}"
|
||||
flashinfer_version = getattr(flashinfer, "__version__", "unknown")
|
||||
|
||||
server_args = mr.server_args
|
||||
model_key_parts = [
|
||||
str(server_args.model_path),
|
||||
str(mr.dtype),
|
||||
str(server_args.quantization),
|
||||
str(server_args.moe_runner_backend),
|
||||
str(mr.tp_size),
|
||||
str(mr.pp_size),
|
||||
str(mr.dp_size),
|
||||
str(mr.moe_ep_size),
|
||||
str(mr.model_config.hf_config.__class__.__name__),
|
||||
]
|
||||
if mr.is_draft_worker:
|
||||
model_key_parts.append(f"draft_quant={mr.model_config.quantization}")
|
||||
model_key = "|".join(model_key_parts)
|
||||
cache_key = hashlib.sha256(model_key.encode()).hexdigest()[:16]
|
||||
cache_dir = (
|
||||
Path(envs.SGLANG_CACHE_DIR.get())
|
||||
/ "flashinfer"
|
||||
/ "autotune"
|
||||
/ flashinfer_version
|
||||
/ arch
|
||||
/ cache_key
|
||||
)
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
return cache_dir / f"rank_tp{mr.tp_rank}_pp{mr.pp_rank}_dp{mr.dp_rank or 0}.json"
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def flashinfer_autotune_context(model_runner: ModelRunner, *, skip_logits: bool):
|
||||
from flashinfer.autotuner import autotune
|
||||
|
||||
mr = model_runner
|
||||
cache_path = flashinfer_autotune_cache_path(mr)
|
||||
if envs.SGLANG_FLASHINFER_AUTOTUNE_CACHE.get():
|
||||
autotune_cache = cache_path
|
||||
logger.info("Running FlashInfer autotune with cache: %s", autotune_cache)
|
||||
else:
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
runs_dir = cache_path.parent / "runs"
|
||||
runs_dir.mkdir(parents=True, exist_ok=True)
|
||||
autotune_cache = runs_dir / f"{cache_path.stem}.{timestamp}{cache_path.suffix}"
|
||||
logger.info(
|
||||
"Running FlashInfer autotune (cache reuse DISABLED via "
|
||||
"SGLANG_FLASHINFER_AUTOTUNE_CACHE=0); writing fresh result to: %s",
|
||||
autotune_cache,
|
||||
)
|
||||
|
||||
# Run warmup on the non-default stream to avoid NCCL 2.29+ cudaMemcpyBatchAsync
|
||||
# calls on default stream (unsupported by CUDA) when --enable-symm-mem is used.
|
||||
mr.forward_stream.wait_stream(torch.cuda.current_stream())
|
||||
with torch.get_device_module(mr.device).stream(mr.forward_stream):
|
||||
maybe_skip_logits = contextlib.nullcontext()
|
||||
if skip_logits:
|
||||
from sglang.srt.layers.logits_processor import autotune_dummy_run_mode
|
||||
|
||||
maybe_skip_logits = autotune_dummy_run_mode()
|
||||
with torch.inference_mode(), autotune(
|
||||
True, cache=str(autotune_cache)
|
||||
), maybe_skip_logits:
|
||||
yield
|
||||
torch.cuda.current_stream().wait_stream(mr.forward_stream)
|
||||
logger.info("FlashInfer autotune completed.")
|
||||
|
||||
|
||||
def run_flashinfer_autotune_forward(
|
||||
model_runner: ModelRunner, forward_fn: Callable[[], None], *, skip_logits: bool
|
||||
) -> None:
|
||||
"""Run flashinfer autotune forward."""
|
||||
with flashinfer_autotune_context(model_runner, skip_logits=skip_logits):
|
||||
forward_fn()
|
||||
|
||||
|
||||
def maybe_flashinfer_autotune_speculative_draft(
|
||||
runner: BaseRunner,
|
||||
forward_fn: Callable[[], None],
|
||||
*,
|
||||
post_warmup_hook: Optional[Callable[[], None]] = None,
|
||||
skip_logits: bool = False,
|
||||
) -> None:
|
||||
"""Run speculative draft flashinfer autotune."""
|
||||
mr = runner.model_runner
|
||||
phase_key = f"{runner.__class__.__module__}.{runner.__class__.__qualname__}"
|
||||
tuned_phases = getattr(mr, "_flashinfer_spec_draft_autotuned_phases", None)
|
||||
if tuned_phases is None:
|
||||
tuned_phases = set()
|
||||
mr._flashinfer_spec_draft_autotuned_phases = tuned_phases
|
||||
if phase_key in tuned_phases:
|
||||
return
|
||||
if (
|
||||
not mr.spec_algorithm.is_speculative()
|
||||
or not mr.is_draft_worker
|
||||
or not should_run_flashinfer_autotune(mr, for_speculative_draft=True)
|
||||
):
|
||||
return
|
||||
|
||||
def run_and_reset():
|
||||
forward_fn()
|
||||
if post_warmup_hook is not None:
|
||||
post_warmup_hook()
|
||||
|
||||
run_flashinfer_autotune_forward(mr, run_and_reset, skip_logits=skip_logits)
|
||||
tuned_phases.add(phase_key)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""ShapeKey — typed identifier for one captured CUDA-graph shape."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ShapeKey:
|
||||
"""Identifies one captured CUDA-graph shape across all runners.
|
||||
|
||||
size: the per-phase capture size — what the runner iterates over.
|
||||
- prefill: num_tokens
|
||||
- decode: bs
|
||||
stream_idx: pdmux stream index, or None for single-stream runners.
|
||||
variant_label: LoRA-variant label ("lora" / "nolora"), or None
|
||||
for runners that don't record per-variant graphs.
|
||||
"""
|
||||
|
||||
size: int
|
||||
stream_idx: Optional[int] = None
|
||||
variant_label: Optional[str] = None
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Capture-mechanism backends for CUDA graphs.
|
||||
|
||||
A backend owns *how* a captured artifact is produced and replayed for
|
||||
one shape; it is phase-agnostic. Runners (cuda_graph_runner/) own
|
||||
*what* data flows in and out.
|
||||
|
||||
Public API:
|
||||
- BaseCudaGraphBackend — abstract interface.
|
||||
- FullCudaGraphBackend — single torch.cuda.CUDAGraph per shape.
|
||||
- BreakableCudaGraphBackend — segmented capture with eager break
|
||||
markers; no torch.compile.
|
||||
- TcPiecewiseCudaGraphBackend — torch.compile-driven piecewise
|
||||
capture; FX-splits the model at attention layers.
|
||||
"""
|
||||
|
||||
from sglang.srt.model_executor.runner_backend.base_cuda_graph_backend import ( # noqa: F401
|
||||
BaseCudaGraphBackend,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend.breakable_cuda_graph_backend import ( # noqa: F401
|
||||
BreakableCudaGraphBackend,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend.full_cuda_graph_backend import ( # noqa: F401
|
||||
FullCudaGraphBackend,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend.tc_piecewise_cuda_graph_backend import ( # noqa: F401
|
||||
TcPiecewiseCudaGraphBackend,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend.utils import ( # noqa: F401
|
||||
resolve_decode_backend,
|
||||
resolve_prefill_backend,
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""Backend interface for CUDA graph capture/replay."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional
|
||||
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
from sglang.srt.model_executor.runner.shape_key import ShapeKey
|
||||
|
||||
|
||||
class BaseCudaGraphBackend(ABC):
|
||||
"""Pure ABC: no state, no defaults. Each implementation owns its
|
||||
per-backend state and binds the handles it needs from the
|
||||
cuda_graph_runner passed to its __init__.
|
||||
|
||||
Methods:
|
||||
- capture_session(stream) — context wrapping the runner's outer
|
||||
capture loop; backends bind stream / pool and open per-backend
|
||||
capture flags here.
|
||||
- capture_one(shape_key, forward_fn, dummies, post_warmup_hook)
|
||||
— record the replayable artifact for shape_key; one call per
|
||||
shape inside capture_session.
|
||||
- can_run(forward_batch, shape_key) — can this backend replay
|
||||
for the given batch at the given shape.
|
||||
- replay_session() — context wrapping replay-time model code;
|
||||
backends open the "we are replaying" flag here when they have
|
||||
one.
|
||||
- replay(shape_key, static_forward_batch, **kwargs) — invoke
|
||||
the captured artifact.
|
||||
- cleanup() — release pool and drop captured artifacts.
|
||||
|
||||
Notes:
|
||||
- The outer capture loop is runner-specific; it lives on the
|
||||
runner, not here.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def capture_session(self, stream: torch.cuda.Stream) -> Iterator[None]: ...
|
||||
|
||||
@abstractmethod
|
||||
def capture_one(
|
||||
self,
|
||||
shape_key: ShapeKey,
|
||||
forward_fn,
|
||||
dummies: Optional[Any] = None,
|
||||
post_warmup_hook: Optional[Callable[[], None]] = None,
|
||||
) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def can_run(self, forward_batch: ForwardBatch, shape_key: ShapeKey) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
def replay_session(self) -> Iterator[None]: ...
|
||||
|
||||
@abstractmethod
|
||||
def replay(
|
||||
self,
|
||||
shape_key: ShapeKey,
|
||||
static_forward_batch: ForwardBatch,
|
||||
**kwargs,
|
||||
) -> Any: ...
|
||||
|
||||
@abstractmethod
|
||||
def cleanup(self) -> None: ...
|
||||
@@ -0,0 +1,252 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""BreakableCudaGraphBackend — segment-captured graphs with eager break
|
||||
markers (eager_on_graph decorators on attention / mamba layers).
|
||||
No torch.compile.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
||||
set_graph_pool_id,
|
||||
)
|
||||
from sglang.srt.model_executor.forward_batch_info import PPProxyTensors
|
||||
from sglang.srt.model_executor.runner_backend.base_cuda_graph_backend import (
|
||||
BaseCudaGraphBackend,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend.cuda_graph_dedup_mixin import (
|
||||
DedupedCudaGraphMixin,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph import (
|
||||
BreakableCUDAGraph,
|
||||
BreakableCUDAGraphCapture,
|
||||
eager_on_graph,
|
||||
enable_breakable_cuda_graph,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_utils.pool import (
|
||||
get_or_create_global_graph_memory_pool,
|
||||
)
|
||||
from sglang.srt.utils import get_bool_env_var
|
||||
from sglang.srt.utils.torch_memory_saver_adapter import TorchMemorySaverAdapter
|
||||
|
||||
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,
|
||||
)
|
||||
from sglang.srt.model_executor.runner.shape_key import ShapeKey
|
||||
|
||||
|
||||
class BreakableCudaGraphBackend(DedupedCudaGraphMixin, BaseCudaGraphBackend):
|
||||
"""Segmented capture: graphs break at attention / mamba boundaries;
|
||||
attention metadata is recomputed at replay outside captured segments.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cuda_graph_runner: BaseCudaGraphRunner,
|
||||
*,
|
||||
enable_memory_saver: bool = False,
|
||||
debug_eager: bool = False,
|
||||
) -> None:
|
||||
self._model_runner = cuda_graph_runner.model_runner
|
||||
self._graphs: Dict[Any, BreakableCUDAGraph] = {}
|
||||
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.cuda.Stream] = None
|
||||
self._debug_eager = debug_eager
|
||||
self._shared_output_buffer: Optional[Any] = None
|
||||
self._memory_saver_adapter: Optional[Any] = TorchMemorySaverAdapter.create(
|
||||
enable=enable_memory_saver
|
||||
and get_bool_env_var("SGLANG_MEMORY_SAVER_CUDA_GRAPH")
|
||||
)
|
||||
if (
|
||||
self._memory_saver_adapter is not None
|
||||
and self._memory_saver_adapter.enabled
|
||||
):
|
||||
raise NotImplementedError(
|
||||
"Breakable CUDA graph is not compatible with memory saver mode"
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def capture_session(self, stream: torch.cuda.Stream):
|
||||
if self._pool is None:
|
||||
self._pool = get_or_create_global_graph_memory_pool(self._device_module)
|
||||
set_graph_pool_id(self._pool)
|
||||
self._capture_stream = stream
|
||||
self._shared_output_buffer = None
|
||||
self.begin_cuda_graph_capture()
|
||||
try:
|
||||
with self.replay_session():
|
||||
yield
|
||||
finally:
|
||||
try:
|
||||
self.end_cuda_graph_capture()
|
||||
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:
|
||||
warmup_out = None
|
||||
for _ in range(2):
|
||||
self._device_module.synchronize()
|
||||
self._tp_group.barrier()
|
||||
warmup_out = forward_fn()
|
||||
if post_warmup_hook is not None:
|
||||
post_warmup_hook()
|
||||
|
||||
graph = BreakableCUDAGraph()
|
||||
captured_fn = (
|
||||
eager_on_graph(True)(forward_fn) if self._debug_eager else forward_fn
|
||||
)
|
||||
size = shape_key.size
|
||||
if self._shared_output_buffer is None:
|
||||
self._shared_output_buffer = self._alloc_full_buffer(warmup_out, size)
|
||||
with BreakableCUDAGraphCapture(
|
||||
cuda_graph=graph,
|
||||
pool=self._pool,
|
||||
stream=self._capture_stream,
|
||||
):
|
||||
out = captured_fn()
|
||||
out_rows = self._output_rows(out, size)
|
||||
self._copy_output_to_buffer(out, self._shared_output_buffer, out_rows)
|
||||
|
||||
stored = self._slice_output(self._shared_output_buffer, out_rows)
|
||||
self._graphs[shape_key] = graph
|
||||
self._outputs[shape_key] = stored
|
||||
|
||||
def _output_rows(self, output: Any, cap: int) -> int:
|
||||
"""Leading-dim row count actually produced by the body, clamped to ``cap``.
|
||||
|
||||
A body that shards or prunes its output along dim 0 returns fewer than
|
||||
``cap`` rows; everything else returns exactly ``cap``.
|
||||
"""
|
||||
if torch.is_tensor(output):
|
||||
return min(cap, output.shape[0])
|
||||
if isinstance(output, PPProxyTensors):
|
||||
rows = [t.shape[0] for t in output.tensors.values()]
|
||||
return min([cap, *rows])
|
||||
if isinstance(output, (list, tuple)) and output:
|
||||
return min(self._output_rows(o, cap) for o in output if o is not None)
|
||||
return cap
|
||||
|
||||
def _alloc_full_buffer(self, output: Any, size: int) -> Any:
|
||||
"""A same-structure buffer as ``output`` but with ``size`` leading rows."""
|
||||
if output is None:
|
||||
return None
|
||||
if torch.is_tensor(output):
|
||||
return output.new_empty((size, *output.shape[1:]))
|
||||
if isinstance(output, PPProxyTensors):
|
||||
return PPProxyTensors(
|
||||
{
|
||||
key: t.new_empty((size, *t.shape[1:]))
|
||||
for key, t in output.tensors.items()
|
||||
}
|
||||
)
|
||||
if isinstance(output, tuple):
|
||||
return tuple(self._alloc_full_buffer(o, size) for o in output)
|
||||
if isinstance(output, list):
|
||||
return [self._alloc_full_buffer(o, size) for o in output]
|
||||
raise TypeError(f"Unsupported BCG output type: {type(output)}")
|
||||
|
||||
def _slice_output(self, output: Any, num_tokens: int) -> Any:
|
||||
if output is None:
|
||||
return None
|
||||
if torch.is_tensor(output):
|
||||
return output[:num_tokens]
|
||||
if isinstance(output, PPProxyTensors):
|
||||
return output[:num_tokens]
|
||||
if isinstance(output, tuple):
|
||||
return tuple(self._slice_output(item, num_tokens) for item in output)
|
||||
if isinstance(output, list):
|
||||
return [self._slice_output(item, num_tokens) for item in output]
|
||||
raise TypeError(f"Unsupported BCG output type: {type(output)}")
|
||||
|
||||
def _copy_output_to_buffer(
|
||||
self, output: Any, output_buffer: Any, num_tokens: int
|
||||
) -> None:
|
||||
if output is None or output_buffer is None:
|
||||
if output is None and output_buffer is None:
|
||||
return
|
||||
raise ValueError(
|
||||
"BCG output structure changed between capture sizes: "
|
||||
f"{type(output)} vs {type(output_buffer)}"
|
||||
)
|
||||
if torch.is_tensor(output) and torch.is_tensor(output_buffer):
|
||||
output_buffer[:num_tokens].copy_(output[:num_tokens])
|
||||
return
|
||||
if isinstance(output, PPProxyTensors) and isinstance(
|
||||
output_buffer, PPProxyTensors
|
||||
):
|
||||
if output.tensors.keys() != output_buffer.tensors.keys():
|
||||
raise ValueError(
|
||||
"BCG output proxy structure changed between capture sizes: "
|
||||
f"{output.tensors.keys()} != {output_buffer.tensors.keys()}"
|
||||
)
|
||||
for key, tensor in output.tensors.items():
|
||||
self._copy_output_to_buffer(
|
||||
tensor, output_buffer.tensors[key], num_tokens
|
||||
)
|
||||
return
|
||||
if isinstance(output, (list, tuple)) and isinstance(
|
||||
output_buffer, type(output)
|
||||
):
|
||||
if len(output) != len(output_buffer):
|
||||
raise ValueError(
|
||||
"BCG output sequence structure changed between capture sizes: "
|
||||
f"{len(output)} != {len(output_buffer)}"
|
||||
)
|
||||
for item, buffer in zip(output, output_buffer):
|
||||
self._copy_output_to_buffer(item, buffer, num_tokens)
|
||||
return
|
||||
raise TypeError(
|
||||
"Unsupported BCG output buffer pair: "
|
||||
f"{type(output)} vs {type(output_buffer)}"
|
||||
)
|
||||
|
||||
def can_run(self, forward_batch: ForwardBatch, shape_key: ShapeKey) -> bool:
|
||||
return shape_key in self._graphs
|
||||
|
||||
@contextmanager
|
||||
def replay_session(self):
|
||||
with enable_breakable_cuda_graph():
|
||||
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.close()
|
||||
self._graphs.clear()
|
||||
self._outputs.clear()
|
||||
self._pool = None
|
||||
self._shared_output_buffer = None
|
||||
@@ -0,0 +1,375 @@
|
||||
"""Shared CUDA graph executable-dedup plumbing for CUDA graph backends."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import torch
|
||||
|
||||
try:
|
||||
from cuda.bindings import driver as cuda_drv
|
||||
from cuda.bindings import runtime as cuda_rt
|
||||
except ImportError:
|
||||
cuda_drv = None
|
||||
cuda_rt = None
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.cuda_utils import (
|
||||
checkCudaErrors,
|
||||
)
|
||||
from sglang.srt.utils import get_bool_env_var
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def dedup_update(graph_exec: int, raw_graph: int) -> tuple[bool, str]:
|
||||
assert cuda_rt is not None
|
||||
err, info = cuda_rt.cudaGraphExecUpdate(graph_exec, raw_graph)
|
||||
if info is None:
|
||||
return False, f"err={int(err)}"
|
||||
result = info.result
|
||||
ok = (
|
||||
err == cuda_rt.cudaError_t.cudaSuccess
|
||||
and result == cuda_rt.cudaGraphExecUpdateResult.cudaGraphExecUpdateSuccess
|
||||
)
|
||||
return ok, "" if ok else f"err={int(err)} result={result}"
|
||||
|
||||
|
||||
def maybe_cuda_result(result):
|
||||
return None if int(result[0]) != 0 else checkCudaErrors(result)
|
||||
|
||||
|
||||
def kernel_name(params) -> str:
|
||||
assert cuda_drv is not None
|
||||
for handle, getter in (
|
||||
(getattr(params, "kern", None), cuda_drv.cuKernelGetName),
|
||||
(getattr(params, "func", None), cuda_drv.cuFuncGetName),
|
||||
):
|
||||
if handle is None or int(handle) == 0:
|
||||
continue
|
||||
name = maybe_cuda_result(getter(handle))
|
||||
if name is not None:
|
||||
return name.decode("utf-8", "replace")
|
||||
return f"func:{int(getattr(params, 'func', 0))}"
|
||||
|
||||
|
||||
def kernel_attrs(node) -> tuple[tuple[str, object], ...]:
|
||||
assert cuda_drv is not None
|
||||
attrs = []
|
||||
for name, attr_name, get_value in (
|
||||
(
|
||||
"cooperative",
|
||||
"CU_LAUNCH_ATTRIBUTE_COOPERATIVE",
|
||||
lambda v: int(v.cooperative),
|
||||
),
|
||||
(
|
||||
"clusterDim",
|
||||
"CU_LAUNCH_ATTRIBUTE_CLUSTER_DIMENSION",
|
||||
lambda v: (
|
||||
int(v.clusterDim.x),
|
||||
int(v.clusterDim.y),
|
||||
int(v.clusterDim.z),
|
||||
),
|
||||
),
|
||||
(
|
||||
"clusterSchedulingPolicyPreference",
|
||||
"CU_LAUNCH_ATTRIBUTE_CLUSTER_SCHEDULING_POLICY_PREFERENCE",
|
||||
lambda v: int(v.clusterSchedulingPolicyPreference),
|
||||
),
|
||||
(
|
||||
"preferredClusterDim",
|
||||
"CU_LAUNCH_ATTRIBUTE_PREFERRED_CLUSTER_DIMENSION",
|
||||
lambda v: (
|
||||
int(v.preferredClusterDim.x),
|
||||
int(v.preferredClusterDim.y),
|
||||
int(v.preferredClusterDim.z),
|
||||
),
|
||||
),
|
||||
(
|
||||
"sharedMemCarveout",
|
||||
"CU_LAUNCH_ATTRIBUTE_PREFERRED_SHARED_MEMORY_CARVEOUT",
|
||||
lambda v: int(v.sharedMemCarveout),
|
||||
),
|
||||
):
|
||||
attr = getattr(cuda_drv.CUkernelNodeAttrID, attr_name, None)
|
||||
if attr is None:
|
||||
continue
|
||||
value = maybe_cuda_result(cuda_drv.cuGraphKernelNodeGetAttribute(node, attr))
|
||||
if value is not None:
|
||||
attrs.append((name, get_value(value)))
|
||||
return tuple(attrs)
|
||||
|
||||
|
||||
def kernel_node_payload(node):
|
||||
assert cuda_drv is not None
|
||||
params = checkCudaErrors(cuda_drv.cuGraphKernelNodeGetParams(node))
|
||||
return (
|
||||
kernel_name(params),
|
||||
(int(params.gridDimX), int(params.gridDimY), int(params.gridDimZ)),
|
||||
(int(params.blockDimX), int(params.blockDimY), int(params.blockDimZ)),
|
||||
int(params.sharedMemBytes),
|
||||
kernel_attrs(node),
|
||||
)
|
||||
|
||||
|
||||
def graph_node_payload(node):
|
||||
assert cuda_drv is not None
|
||||
node_type = checkCudaErrors(cuda_drv.cuGraphNodeGetType(node))
|
||||
match node_type:
|
||||
case cuda_drv.CUgraphNodeType.CU_GRAPH_NODE_TYPE_KERNEL:
|
||||
payload = kernel_node_payload(node)
|
||||
case cuda_drv.CUgraphNodeType.CU_GRAPH_NODE_TYPE_MEMCPY:
|
||||
params = checkCudaErrors(cuda_drv.cuGraphMemcpyNodeGetParams(node))
|
||||
payload = (int(params.srcMemoryType), int(params.dstMemoryType))
|
||||
case cuda_drv.CUgraphNodeType.CU_GRAPH_NODE_TYPE_MEMSET:
|
||||
params = checkCudaErrors(cuda_drv.cuGraphMemsetNodeGetParams(node))
|
||||
payload = (int(params.elementSize),)
|
||||
case cuda_drv.CUgraphNodeType.CU_GRAPH_NODE_TYPE_GRAPH:
|
||||
child_graph = checkCudaErrors(cuda_drv.cuGraphChildGraphNodeGetGraph(node))
|
||||
payload = graph_signature(child_graph)
|
||||
case cuda_drv.CUgraphNodeType.CU_GRAPH_NODE_TYPE_EMPTY:
|
||||
payload = ()
|
||||
case _:
|
||||
payload = ()
|
||||
return (node_type.name, payload)
|
||||
|
||||
|
||||
def graph_signature(raw_graph: int):
|
||||
assert cuda_drv is not None
|
||||
_, num_nodes = checkCudaErrors(cuda_drv.cuGraphGetNodes(raw_graph, 0))
|
||||
nodes, _ = checkCudaErrors(cuda_drv.cuGraphGetNodes(raw_graph, num_nodes))
|
||||
node_indices = {int(node): i for i, node in enumerate(nodes)}
|
||||
|
||||
_, _, _, num_edges = checkCudaErrors(cuda_drv.cuGraphGetEdges(raw_graph, 0))
|
||||
from_nodes, to_nodes, _, _ = checkCudaErrors(
|
||||
cuda_drv.cuGraphGetEdges(raw_graph, num_edges)
|
||||
)
|
||||
edges = [
|
||||
(node_indices[int(src)], node_indices[int(dst)])
|
||||
for src, dst in zip(from_nodes, to_nodes)
|
||||
]
|
||||
|
||||
children = [[] for _ in nodes]
|
||||
indegree = [0] * len(nodes)
|
||||
for src, dst in edges:
|
||||
children[src].append(dst)
|
||||
indegree[dst] += 1
|
||||
|
||||
ready = [i for i, degree in enumerate(indegree) if degree == 0]
|
||||
heapq.heapify(ready)
|
||||
order = []
|
||||
while ready:
|
||||
node_idx = heapq.heappop(ready)
|
||||
order.append(node_idx)
|
||||
for child_idx in sorted(children[node_idx]):
|
||||
indegree[child_idx] -= 1
|
||||
if indegree[child_idx] == 0:
|
||||
heapq.heappush(ready, child_idx)
|
||||
assert len(order) == len(nodes), "CUDA graph contains a dependency cycle"
|
||||
|
||||
topo_indices = {node_idx: i for i, node_idx in enumerate(order)}
|
||||
topo_edges = tuple(
|
||||
sorted((topo_indices[src], topo_indices[dst]) for src, dst in edges)
|
||||
)
|
||||
return (
|
||||
tuple(graph_node_payload(nodes[node_idx]) for node_idx in order),
|
||||
topo_edges,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GraphExecGroup:
|
||||
graph_exec: int
|
||||
current_raw_graph: int
|
||||
compat_exec: int | None
|
||||
graphs: list[DedupedCudaGraph] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(eq=False, slots=True)
|
||||
class DedupedCudaGraph:
|
||||
raw_graph: int
|
||||
original_graph: object | None
|
||||
registry: DedupedCudaGraphRegistry
|
||||
group: GraphExecGroup | None = None
|
||||
|
||||
def replay(self, stream: int | None = None) -> None:
|
||||
if stream is None:
|
||||
stream = torch.cuda.current_stream().cuda_stream
|
||||
self.registry.replay(self, stream)
|
||||
|
||||
|
||||
class DedupedCudaGraphRegistry:
|
||||
def __init__(self):
|
||||
self.groups: dict[tuple, GraphExecGroup] = {}
|
||||
self.sealed = False
|
||||
|
||||
def instantiate(self, raw_graph: int) -> int:
|
||||
assert cuda_rt is not None
|
||||
graph_exec = checkCudaErrors(
|
||||
cuda_rt.cudaGraphInstantiateWithFlags(raw_graph, 0)
|
||||
)
|
||||
return graph_exec
|
||||
|
||||
def destroy_exec(self, graph_exec: int) -> None:
|
||||
assert cuda_rt is not None
|
||||
checkCudaErrors(cuda_rt.cudaGraphExecDestroy(graph_exec))
|
||||
|
||||
def register(self, captured_graph) -> DedupedCudaGraph:
|
||||
assert not self.sealed
|
||||
raw_graph = captured_graph.raw_cuda_graph()
|
||||
signature = graph_signature(raw_graph)
|
||||
graph = DedupedCudaGraph(raw_graph, captured_graph, self)
|
||||
|
||||
group = self.groups.get(signature)
|
||||
if group is not None:
|
||||
assert group.compat_exec is not None
|
||||
ok, detail = dedup_update(group.compat_exec, graph.raw_graph)
|
||||
assert ok, f"CUDA graph dedup register update failed ({detail})"
|
||||
graph.group = group
|
||||
group.graphs.append(graph)
|
||||
return graph
|
||||
|
||||
group = GraphExecGroup(
|
||||
graph_exec=self.instantiate(graph.raw_graph),
|
||||
current_raw_graph=graph.raw_graph,
|
||||
compat_exec=self.instantiate(graph.raw_graph),
|
||||
graphs=[graph],
|
||||
)
|
||||
graph.group = group
|
||||
self.groups[signature] = group
|
||||
return graph
|
||||
|
||||
def seal(self) -> None:
|
||||
if self.sealed:
|
||||
return
|
||||
self.sealed = True
|
||||
for group in self.groups.values():
|
||||
if group.compat_exec is not None:
|
||||
self.destroy_exec(group.compat_exec)
|
||||
group.compat_exec = None
|
||||
|
||||
def stats(self) -> tuple[int, int]:
|
||||
return sum(len(group.graphs) for group in self.groups.values()), len(
|
||||
self.groups
|
||||
)
|
||||
|
||||
def replay(self, graph: DedupedCudaGraph, stream: int) -> None:
|
||||
assert cuda_rt is not None
|
||||
group = graph.group
|
||||
assert (
|
||||
group is not None
|
||||
), "captured CUDA graph does not belong to this dedup state"
|
||||
|
||||
raw_graph = graph.raw_graph
|
||||
graph_exec = group.graph_exec
|
||||
if group.current_raw_graph != raw_graph:
|
||||
ok, detail = dedup_update(graph_exec, raw_graph)
|
||||
assert ok, (
|
||||
"CUDA graph dedup replay update failed "
|
||||
f"({detail}); captured graph is not compatible with its dedup group"
|
||||
)
|
||||
group.current_raw_graph = raw_graph
|
||||
|
||||
checkCudaErrors(cuda_rt.cudaGraphLaunch(graph_exec, stream))
|
||||
|
||||
def close(self) -> None:
|
||||
self.sealed = True
|
||||
|
||||
for group in self.groups.values():
|
||||
if group.compat_exec is not None:
|
||||
self.destroy_exec(group.compat_exec)
|
||||
group.compat_exec = None
|
||||
self.destroy_exec(group.graph_exec)
|
||||
for graph in group.graphs:
|
||||
if graph.original_graph is not None:
|
||||
graph.original_graph.reset()
|
||||
graph.original_graph = None
|
||||
graph.group = None
|
||||
group.graphs.clear()
|
||||
|
||||
self.groups.clear()
|
||||
|
||||
|
||||
class DedupedCudaGraphMixin:
|
||||
deduped_cuda_graph: DedupedCudaGraphRegistry | None = None
|
||||
|
||||
def _dedup_registries(self) -> list[DedupedCudaGraphRegistry]:
|
||||
registries = getattr(self, "_deduped_cuda_graph_registries", None)
|
||||
if registries is None:
|
||||
registries = []
|
||||
self._deduped_cuda_graph_registries = registries
|
||||
return registries
|
||||
|
||||
def _memory_saver_cuda_graph_enabled(self) -> bool:
|
||||
adapter = getattr(self, "_memory_saver_adapter", None)
|
||||
if adapter is not None and getattr(adapter, "enabled", False):
|
||||
return True
|
||||
|
||||
model_runner = getattr(self, "model_runner", None)
|
||||
if model_runner is None:
|
||||
model_runner = getattr(self, "_model_runner", None)
|
||||
server_args = getattr(model_runner, "server_args", None)
|
||||
return bool(
|
||||
server_args is not None
|
||||
and getattr(server_args, "enable_memory_saver", False)
|
||||
and get_bool_env_var("SGLANG_MEMORY_SAVER_CUDA_GRAPH")
|
||||
)
|
||||
|
||||
def build_deduped_cuda_graph(self):
|
||||
if not envs.SGLANG_ENABLE_CUDA_GRAPH_DEDUP.get():
|
||||
return None
|
||||
if cuda_drv is None or cuda_rt is None:
|
||||
return None
|
||||
try:
|
||||
graph = torch.cuda.CUDAGraph(keep_graph=True)
|
||||
if not hasattr(graph, "raw_cuda_graph"):
|
||||
return None
|
||||
return DedupedCudaGraphRegistry()
|
||||
except TypeError:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"[CudaGraph][dedup] %s init failed (%s); using plain executables.",
|
||||
type(self).__name__,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
|
||||
def begin_cuda_graph_capture(self) -> None:
|
||||
if self.deduped_cuda_graph is not None:
|
||||
self.end_cuda_graph_capture()
|
||||
|
||||
if self._memory_saver_cuda_graph_enabled():
|
||||
self.deduped_cuda_graph = None
|
||||
return
|
||||
|
||||
self.deduped_cuda_graph = self.build_deduped_cuda_graph()
|
||||
if self.deduped_cuda_graph is not None:
|
||||
self._dedup_registries().append(self.deduped_cuda_graph)
|
||||
|
||||
def end_cuda_graph_capture(self) -> None:
|
||||
dedup = self.deduped_cuda_graph
|
||||
self.deduped_cuda_graph = None
|
||||
if dedup is not None:
|
||||
captured, execs = dedup.stats()
|
||||
dedup.seal()
|
||||
logger.info("captured %d CUDA graphs, deduped to %d execs", captured, execs)
|
||||
|
||||
def close(self) -> None:
|
||||
registries = self._dedup_registries()
|
||||
seen: set[int] = set()
|
||||
for registry in [self.deduped_cuda_graph, *registries]:
|
||||
if registry is None or id(registry) in seen:
|
||||
continue
|
||||
seen.add(id(registry))
|
||||
registry.close()
|
||||
registries.clear()
|
||||
self.deduped_cuda_graph = None
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,135 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""FullCudaGraphBackend — captures the entire model forward as one
|
||||
torch.cuda.CUDAGraph per shape.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import AbstractContextManager, contextmanager
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.constants import GPU_MEMORY_TYPE_CUDA_GRAPH
|
||||
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
||||
set_graph_pool_id,
|
||||
)
|
||||
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,
|
||||
)
|
||||
from sglang.srt.utils import get_bool_env_var
|
||||
from sglang.srt.utils.torch_memory_saver_adapter import TorchMemorySaverAdapter
|
||||
|
||||
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,
|
||||
)
|
||||
from sglang.srt.model_executor.runner.shape_key import ShapeKey
|
||||
|
||||
|
||||
class FullCudaGraphBackend(BaseCudaGraphBackend):
|
||||
"""One torch.cuda.CUDAGraph per shape; attention metadata is
|
||||
captured inside the graph. Memory-saver-aware.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cuda_graph_runner: BaseCudaGraphRunner,
|
||||
*,
|
||||
enable_memory_saver: bool = False,
|
||||
) -> None:
|
||||
self._graphs: Dict[Any, torch.cuda.CUDAGraph] = {}
|
||||
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.cuda.Stream] = None
|
||||
self._memory_saver_adapter: Optional[Any] = TorchMemorySaverAdapter.create(
|
||||
enable=enable_memory_saver
|
||||
and get_bool_env_var("SGLANG_MEMORY_SAVER_CUDA_GRAPH")
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def capture_session(self, stream: torch.cuda.Stream):
|
||||
if self._pool is None:
|
||||
self._pool = get_or_create_global_graph_memory_pool(self._device_module)
|
||||
set_graph_pool_id(self._pool)
|
||||
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:
|
||||
# Two warmups so kernels are loaded and one-time setup is paid before capture.
|
||||
# post_warmup_hook lets the attention backend reset state that warmup mutated.
|
||||
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.cuda.CUDAGraph()
|
||||
|
||||
graph_ctx: Callable[..., AbstractContextManager]
|
||||
if (
|
||||
self._memory_saver_adapter is not None
|
||||
and self._memory_saver_adapter.enabled
|
||||
):
|
||||
graph_ctx = partial(
|
||||
self._memory_saver_adapter.cuda_graph,
|
||||
tag=GPU_MEMORY_TYPE_CUDA_GRAPH,
|
||||
)
|
||||
else:
|
||||
graph_ctx = self._device_module.graph
|
||||
|
||||
with graph_ctx(cuda_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,259 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""TcPiecewiseCudaGraphBackend — torch.compile-driven piecewise CUDA graph.
|
||||
|
||||
FX-splits the model forward at attention layers; per-shape compiled
|
||||
callables internally capture sub-graphs via
|
||||
compilation/cuda_piecewise_backend. torch.compile owns the per-shape
|
||||
cache so this backend has no _graphs table — only a single
|
||||
_compiled_fn reused for every shape.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional
|
||||
|
||||
import torch
|
||||
import tqdm
|
||||
|
||||
from sglang.srt.compilation.compilation_config import CompilationConfig
|
||||
from sglang.srt.compilation.compile import install_torch_compiled
|
||||
from sglang.srt.compilation.compile_phase import (
|
||||
enable_torch_compile_warmup,
|
||||
set_pcg_capture_stream,
|
||||
)
|
||||
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
||||
set_graph_pool_id,
|
||||
)
|
||||
from sglang.srt.layers.moe.utils import get_moe_a2a_backend
|
||||
from sglang.srt.layers.utils import MultiPlatformOp
|
||||
from sglang.srt.model_executor.runner_backend.base_cuda_graph_backend import (
|
||||
BaseCudaGraphBackend,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph import (
|
||||
enable_tc_piecewise_cuda_graph,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_utils.pool import (
|
||||
get_or_create_global_graph_memory_pool,
|
||||
)
|
||||
from sglang.srt.runtime_context import get_parallel
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
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,
|
||||
)
|
||||
from sglang.srt.model_executor.runner.shape_key import ShapeKey
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
|
||||
_VALID_COMPILERS = ("eager", "inductor")
|
||||
|
||||
|
||||
def _toggle_multi_platform_ops(
|
||||
model: torch.nn.Module, *, reverse: bool, num_tokens: int
|
||||
) -> None:
|
||||
"""Recursively flip MultiPlatformOp submodules into / out of
|
||||
torch.compile mode."""
|
||||
for sub in model._modules.values():
|
||||
if isinstance(sub, MultiPlatformOp):
|
||||
if reverse:
|
||||
sub.leave_torch_compile()
|
||||
else:
|
||||
sub.enter_torch_compile(num_tokens=num_tokens)
|
||||
if isinstance(sub, torch.nn.Module):
|
||||
_toggle_multi_platform_ops(sub, reverse=reverse, num_tokens=num_tokens)
|
||||
|
||||
|
||||
class TcPiecewiseCudaGraphBackend(BaseCudaGraphBackend):
|
||||
"""torch.compile-driven piecewise capture; attention metadata
|
||||
recomputed at replay outside the compiled callable's sub-graphs.
|
||||
"""
|
||||
|
||||
def __init__(self, cuda_graph_runner: BaseCudaGraphRunner) -> None:
|
||||
model_runner = cuda_graph_runner.model_runner
|
||||
self._pool = None
|
||||
self._device_module = cuda_graph_runner.device_module
|
||||
self._tp_group = model_runner.tp_group
|
||||
self._capture_stream: Optional[torch.cuda.Stream] = None
|
||||
self._compile_config: CompilationConfig = self.build_compilation_config(
|
||||
model_runner.server_args
|
||||
)
|
||||
self._language_model: torch.nn.Module = getattr(
|
||||
model_runner.model, "language_model", model_runner.model
|
||||
)
|
||||
self._run_compile_pass(cuda_graph_runner)
|
||||
# model_runner.model.forward is the wrapper that builds LogitsProcessorOutput.
|
||||
# The compiled trampoline is dispatched internally by it.
|
||||
self._compiled_fn: Callable = model_runner.model.forward
|
||||
|
||||
@staticmethod
|
||||
def build_compilation_config(server_args: ServerArgs) -> CompilationConfig:
|
||||
"""Construct a CompilationConfig from ServerArgs and
|
||||
register the MoE A2A split-op when DeepEP / Mooncake is in use."""
|
||||
prefill = server_args.cuda_graph_config.prefill
|
||||
num_tokens = prefill.bs
|
||||
compiler = prefill.tc_compiler
|
||||
assert num_tokens is not None, "cuda_graph_config[prefill].bs is not set"
|
||||
assert compiler in _VALID_COMPILERS, (
|
||||
f"By now, only {_VALID_COMPILERS} are supported for the "
|
||||
"tc_piecewise prefill compiler."
|
||||
)
|
||||
|
||||
config = CompilationConfig(
|
||||
num_tokens,
|
||||
compiler,
|
||||
server_args.enable_torch_compile_debug_mode,
|
||||
)
|
||||
|
||||
if get_moe_a2a_backend().is_deepep() or get_moe_a2a_backend().is_mooncake():
|
||||
config.add_split_op("sglang.moe_forward_piecewise_cuda_graph_impl")
|
||||
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def install_compile(
|
||||
language_model: Any,
|
||||
*,
|
||||
compile_config: CompilationConfig,
|
||||
graph_pool: Any,
|
||||
fullgraph: bool = True,
|
||||
dynamic_arg_dims: Optional[Any] = None,
|
||||
) -> None:
|
||||
"""Wrap language_model.model.forward with torch.compile."""
|
||||
install_torch_compiled(
|
||||
language_model,
|
||||
fullgraph=fullgraph,
|
||||
dynamic_arg_dims=dynamic_arg_dims,
|
||||
compile_config=compile_config,
|
||||
graph_pool=graph_pool,
|
||||
)
|
||||
|
||||
def _run_compile_pass(self, cuda_graph_runner: BaseCudaGraphRunner) -> None:
|
||||
"""JIT-activate kernels at the smallest shape, install
|
||||
torch.compile, then run one forward per shape inside
|
||||
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(
|
||||
inner_model, reverse=False, num_tokens=16
|
||||
)
|
||||
|
||||
cuda_graph_runner._run_dummy_forward(
|
||||
num_tokens=cuda_graph_runner.capture_num_tokens[0]
|
||||
)
|
||||
|
||||
if self._pool is None:
|
||||
self._pool = get_or_create_global_graph_memory_pool(
|
||||
self._device_module
|
||||
)
|
||||
set_graph_pool_id(self._pool)
|
||||
|
||||
self.install_compile(
|
||||
inner_model,
|
||||
compile_config=self._compile_config,
|
||||
graph_pool=self._pool,
|
||||
)
|
||||
|
||||
with enable_torch_compile_warmup():
|
||||
if is_hip():
|
||||
# AMD: single Dynamo trace is sufficient; the capture
|
||||
# phase does per-shape JIT kernel warmup before each
|
||||
# CUDA graph recording. The N-iteration loop is
|
||||
# redundant and extremely slow on ROCm (~30 min).
|
||||
cuda_graph_runner._run_dummy_forward(
|
||||
num_tokens=cuda_graph_runner.capture_num_tokens[-1]
|
||||
)
|
||||
else:
|
||||
compile_range = (
|
||||
tqdm.tqdm(
|
||||
list(reversed(cuda_graph_runner.capture_num_tokens))
|
||||
)
|
||||
if get_parallel().tp_rank == 0
|
||||
else reversed(cuda_graph_runner.capture_num_tokens)
|
||||
)
|
||||
for num_tokens in compile_range:
|
||||
if get_parallel().tp_rank == 0:
|
||||
compile_range.set_description(
|
||||
f"Compiling num tokens ({num_tokens=})"
|
||||
)
|
||||
cuda_graph_runner._run_dummy_forward(num_tokens=num_tokens)
|
||||
finally:
|
||||
_toggle_multi_platform_ops(inner_model, reverse=True, num_tokens=16)
|
||||
|
||||
@contextmanager
|
||||
def capture_session(self, stream: torch.cuda.Stream):
|
||||
self._capture_stream = stream
|
||||
try:
|
||||
with self.replay_session():
|
||||
with set_pcg_capture_stream(stream):
|
||||
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:
|
||||
# Call 1 warms FX state; call 2 captures the cuda graph inside capture_session.
|
||||
# See cuda_piecewise_backend.py for the FX backend that drives the capture.
|
||||
for _ in range(2):
|
||||
self._device_module.synchronize()
|
||||
self._tp_group.barrier()
|
||||
forward_fn()
|
||||
if post_warmup_hook is not None:
|
||||
post_warmup_hook()
|
||||
|
||||
def can_run(self, forward_batch: ForwardBatch, shape_key: ShapeKey) -> bool:
|
||||
# torch.compile manages its per-shape cache internally.
|
||||
# _run_compile_pass warms every shape in capture_num_tokens at __init__.
|
||||
return True
|
||||
|
||||
@contextmanager
|
||||
def replay_session(self):
|
||||
with enable_tc_piecewise_cuda_graph():
|
||||
yield
|
||||
|
||||
def replay(
|
||||
self,
|
||||
shape_key: ShapeKey,
|
||||
static_forward_batch: ForwardBatch,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
return self._compiled_fn(
|
||||
static_forward_batch.input_ids,
|
||||
static_forward_batch.positions,
|
||||
static_forward_batch,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self._compiled_fn = None
|
||||
self._compile_config = None
|
||||
self._language_model = None
|
||||
self._pool = None
|
||||
@@ -0,0 +1,124 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""runner_backend utilities — phase → BaseCudaGraphBackend resolution.
|
||||
|
||||
Centralizes per-phase backend resolution so platform overrides (NPU,
|
||||
out-of-tree) and future backend additions can plug in without
|
||||
modifying the runner files. Phase / backend identifiers used here
|
||||
live in :mod:`.cuda_graph_config`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sglang.srt.model_executor.cuda_graph_config import Backend
|
||||
from sglang.srt.model_executor.runner_backend.base_cuda_graph_backend import (
|
||||
BaseCudaGraphBackend,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend.breakable_cuda_graph_backend import (
|
||||
BreakableCudaGraphBackend,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend.full_cuda_graph_backend import (
|
||||
FullCudaGraphBackend,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend.tc_piecewise_cuda_graph_backend import (
|
||||
TcPiecewiseCudaGraphBackend,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.runner.base_cuda_graph_runner import (
|
||||
BaseCudaGraphRunner,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Track first occurrence of each fallback warning to avoid log spam.
|
||||
_TC_PIECEWISE_DECODE_FALLBACK_LOGGED = False
|
||||
|
||||
|
||||
def resolve_decode_backend(
|
||||
cuda_graph_runner: BaseCudaGraphRunner,
|
||||
) -> BaseCudaGraphBackend:
|
||||
"""Pick a backend instance from cuda_graph_config['decode']['backend'].
|
||||
|
||||
NPU device returns NPUCudaGraphBackend regardless of mode (only
|
||||
the Full-style backend is wired for NPU today).
|
||||
"""
|
||||
model_runner = cuda_graph_runner.model_runner
|
||||
cfg = model_runner.server_args.cuda_graph_config
|
||||
backend_name = cfg.decode.backend if cfg is not None else Backend.FULL
|
||||
|
||||
enable_memory_saver = model_runner.server_args.enable_memory_saver
|
||||
|
||||
if model_runner.device == "npu":
|
||||
from sglang.srt.hardware_backend.npu.graph_runner.npu_cudagraph_backend import (
|
||||
NPUCudaGraphBackend,
|
||||
)
|
||||
|
||||
return NPUCudaGraphBackend(
|
||||
cuda_graph_runner, enable_memory_saver=enable_memory_saver
|
||||
)
|
||||
elif 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,
|
||||
enable_memory_saver=enable_memory_saver,
|
||||
debug_eager=model_runner.server_args.debug_cuda_graph,
|
||||
)
|
||||
if backend_name == Backend.TC_PIECEWISE:
|
||||
global _TC_PIECEWISE_DECODE_FALLBACK_LOGGED
|
||||
if not _TC_PIECEWISE_DECODE_FALLBACK_LOGGED:
|
||||
logger.warning(
|
||||
"cuda_graph_config decode='tc_piecewise' is not yet implemented; "
|
||||
"falling back to 'full'."
|
||||
)
|
||||
_TC_PIECEWISE_DECODE_FALLBACK_LOGGED = True
|
||||
return FullCudaGraphBackend(
|
||||
cuda_graph_runner, enable_memory_saver=enable_memory_saver
|
||||
)
|
||||
|
||||
|
||||
def resolve_prefill_backend(
|
||||
cuda_graph_runner: BaseCudaGraphRunner,
|
||||
) -> BaseCudaGraphBackend:
|
||||
"""Pick a backend instance from cuda_graph_config['prefill']['backend']."""
|
||||
model_runner = cuda_graph_runner.model_runner
|
||||
cfg = model_runner.server_args.cuda_graph_config
|
||||
backend_name = cfg.prefill.backend if cfg is not None else Backend.TC_PIECEWISE
|
||||
|
||||
if backend_name == Backend.BREAKABLE:
|
||||
return BreakableCudaGraphBackend(
|
||||
cuda_graph_runner,
|
||||
enable_memory_saver=model_runner.server_args.enable_memory_saver,
|
||||
debug_eager=model_runner.server_args.debug_cuda_graph,
|
||||
)
|
||||
if backend_name == Backend.FULL:
|
||||
return FullCudaGraphBackend(
|
||||
cuda_graph_runner,
|
||||
enable_memory_saver=model_runner.server_args.enable_memory_saver,
|
||||
)
|
||||
# Default: tc_piecewise.
|
||||
return TcPiecewiseCudaGraphBackend(cuda_graph_runner)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Low-level primitives used by the CUDA graph backends.
|
||||
|
||||
Subpackages:
|
||||
- breakable_cuda_graph: BreakableCUDAGraph + capture context,
|
||||
eager_on_graph decorator, is_in_breakable_cuda_graph flag.
|
||||
- piecewise_cuda_graph: shared piecewise context manager
|
||||
(set_tc_piecewise_forward_context, is_in_tc_piecewise_cuda_graph).
|
||||
|
||||
Backends in cuda_graph_backend/ import from here. Runners do not.
|
||||
"""
|
||||
|
||||
# Generic failure-message hint for decode-style CUDA graph capture paths
|
||||
# (Full backend used by decode + EAGLE draft runners).
|
||||
CUDA_GRAPH_CAPTURE_FAILED_MSG = (
|
||||
"Possible solutions:\n"
|
||||
"1. set --mem-fraction-static to a smaller value (e.g., 0.8 or 0.7)\n"
|
||||
"2. set --cuda-graph-max-bs-decode to a smaller value (e.g., 16)\n"
|
||||
"3. disable decode CUDA graph by --cuda-graph-backend-decode=disabled. "
|
||||
"(Not recommended. Huge performance loss)\n"
|
||||
"Open an issue on GitHub https://github.com/sgl-project/sglang/issues/new/choose \n"
|
||||
)
|
||||
|
||||
PREFILL_CUDA_GRAPH_CAPTURE_FAILED_MSG = (
|
||||
"Fail when using backend: {backend} for prefill runner.\n"
|
||||
"Possible suggestions:\n"
|
||||
"{suggestions}"
|
||||
"Open an issue on GitHub https://github.com/sgl-project/sglang/issues/new/choose \n"
|
||||
)
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
"""Breakable primitives — segmented CUDA graph capture with eager break points.
|
||||
|
||||
Public API (also reachable via the deeper module paths):
|
||||
- BreakableCUDAGraph, BreakableCUDAGraphCapture — capture/replay
|
||||
- eager_on_graph — decorator that marks a callable as a graph break
|
||||
- break_graph — helper that inserts a bare graph break
|
||||
- enable_breakable_cuda_graph — context that flips the Breakable runtime flag
|
||||
- is_in_breakable_cuda_graph — runtime flag getter
|
||||
|
||||
"""
|
||||
|
||||
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.breakable_cuda_graph import ( # noqa: F401
|
||||
BreakableCUDAGraph,
|
||||
BreakableCUDAGraphCapture,
|
||||
break_graph,
|
||||
eager_on_graph,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.context import ( # noqa: F401
|
||||
enable_breakable_cuda_graph,
|
||||
is_in_breakable_cuda_graph,
|
||||
)
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""Breakable CUDA Graph: capture a region as a sequence of
|
||||
torch.cuda.CUDAGraph segments separated by eager break points.
|
||||
|
||||
Each segment is a real torch.cuda.CUDAGraph. Its destructor calls
|
||||
releasePool on the shared mempool, so the pool's use_count tracks how
|
||||
many segments are alive; the pool stays pinned as long as any segment graph
|
||||
is alive. This lets weak_ref_tensor views of intermediate pool-allocated
|
||||
tensors remain valid across replays — we don't need Python-managed bridge
|
||||
buffers to keep break-point tensors at stable addresses.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from contextvars import ContextVar
|
||||
from typing import Any, Callable
|
||||
|
||||
import torch
|
||||
|
||||
try:
|
||||
from cuda.bindings import runtime as rt
|
||||
except ImportError:
|
||||
rt = None
|
||||
|
||||
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.cuda_utils import (
|
||||
checkCudaErrors,
|
||||
)
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"eager_on_graph",
|
||||
"BreakableCUDAGraph",
|
||||
"BreakableCUDAGraphCapture",
|
||||
"break_graph",
|
||||
]
|
||||
|
||||
|
||||
def _check_cuda_bindings():
|
||||
if rt is None:
|
||||
raise ImportError(
|
||||
"Breakable CUDA graph on NVIDIA requires the 'cuda-python' package. "
|
||||
"Install it with: pip install cuda-python"
|
||||
)
|
||||
|
||||
|
||||
# Active BreakableCUDAGraphCapture context for the currently-capturing thread.
|
||||
# eager_on_graph's wrapper uses this to split the current torch.cuda.CUDAGraph
|
||||
# at break points.
|
||||
_current_capture_var: ContextVar["BreakableCUDAGraphCapture | None"] = ContextVar(
|
||||
"current_capture", default=None
|
||||
)
|
||||
_current_stream_var: ContextVar[torch.cuda.Stream | None] = ContextVar(
|
||||
"current_stream", default=None
|
||||
)
|
||||
_forked_streams_var: ContextVar[set[torch.cuda.Stream] | None] = ContextVar(
|
||||
"forked_streams", default=None
|
||||
)
|
||||
|
||||
|
||||
def get_current_stream(device: torch.device | None = None) -> torch.cuda.Stream:
|
||||
stream = _current_stream_var.get()
|
||||
if stream is None:
|
||||
return torch.cuda.current_stream(device)
|
||||
return stream
|
||||
|
||||
|
||||
def _capture_status(stream_ptr: int) -> "rt.cudaStreamCaptureStatus":
|
||||
_check_cuda_bindings()
|
||||
status, *_ = checkCudaErrors(rt.cudaStreamGetCaptureInfo(stream_ptr))
|
||||
return status
|
||||
|
||||
|
||||
def _is_stream_capturing(stream: torch.cuda.Stream) -> bool:
|
||||
# On ROCm/HIP, cuda-python is unavailable, so use the portable torch API
|
||||
# (which maps to the HIP runtime). On NVIDIA, keep querying the CUDA runtime
|
||||
# directly via cuda-python: torch.cuda.is_current_stream_capturing() has
|
||||
# proven unreliable there, so we preserve the original behavior.
|
||||
if is_hip():
|
||||
with torch.cuda.stream(stream):
|
||||
return torch.cuda.is_current_stream_capturing()
|
||||
return (
|
||||
_capture_status(stream.cuda_stream)
|
||||
== rt.cudaStreamCaptureStatus.cudaStreamCaptureStatusActive
|
||||
)
|
||||
|
||||
|
||||
# Hook torch.cuda.Stream.wait_stream to track side-stream forks/joins that happen
|
||||
# during breakable capture. We need this because capture_end() on a torch
|
||||
# CUDAGraph fails if there are still side streams participating in the capture
|
||||
# — so before ending each segment we auto-join any forked-but-not-rejoined streams.
|
||||
_original_wait_stream: Callable | None = None
|
||||
_hook_lock = threading.Lock()
|
||||
_hook_refcount = 0
|
||||
|
||||
|
||||
def _hooked_wait_stream(self: torch.cuda.Stream, other: torch.cuda.Stream):
|
||||
assert _original_wait_stream is not None
|
||||
forked = _forked_streams_var.get()
|
||||
if forked is None:
|
||||
_original_wait_stream(self, other)
|
||||
return
|
||||
capturing = _current_stream_var.get()
|
||||
if capturing is None:
|
||||
_original_wait_stream(self, other)
|
||||
return
|
||||
|
||||
cap_ptr = capturing.cuda_stream
|
||||
is_self_cap = self is capturing or self.cuda_stream == cap_ptr
|
||||
is_other_cap = other is capturing or other.cuda_stream == cap_ptr
|
||||
|
||||
if is_self_cap and not is_other_cap:
|
||||
if not _is_stream_capturing(other):
|
||||
return
|
||||
_original_wait_stream(self, other)
|
||||
forked.discard(other)
|
||||
elif is_other_cap and not is_self_cap:
|
||||
_original_wait_stream(self, other)
|
||||
forked.add(self)
|
||||
else:
|
||||
_original_wait_stream(self, other)
|
||||
|
||||
|
||||
def _install_wait_stream_hook():
|
||||
global _original_wait_stream, _hook_refcount
|
||||
with _hook_lock:
|
||||
if _hook_refcount == 0:
|
||||
_original_wait_stream = torch.cuda.Stream.wait_stream
|
||||
torch.cuda.Stream.wait_stream = _hooked_wait_stream # type: ignore[assignment]
|
||||
_hook_refcount += 1
|
||||
|
||||
|
||||
def _uninstall_wait_stream_hook():
|
||||
global _original_wait_stream, _hook_refcount
|
||||
with _hook_lock:
|
||||
_hook_refcount -= 1
|
||||
if _hook_refcount == 0:
|
||||
assert _original_wait_stream is not None, "wait_stream hook not installed"
|
||||
torch.cuda.Stream.wait_stream = _original_wait_stream # type: ignore[assignment]
|
||||
_original_wait_stream = None
|
||||
|
||||
|
||||
def _weak_ref_if_tensor(x):
|
||||
"""Return a weak-ref tensor view (shared storage, no refcount) for tensors;
|
||||
recurse into tuples/lists; pass-through for non-tensors. Weak-ref'ing
|
||||
captured args lets the shared mempool reclaim per-layer intermediates
|
||||
between segments — storage stays alive for each segment CUDAGraph's
|
||||
lifetime via its pool use_count.
|
||||
|
||||
weak_ref_tensors is imported lazily because it hard-raises on
|
||||
platforms without a CUDA/HIP/NPU backend; we only reach this code during
|
||||
an active Breakable capture, which runs only on those backends."""
|
||||
if torch.is_tensor(x):
|
||||
from sglang.srt.compilation.weak_ref_tensor import weak_ref_tensors
|
||||
|
||||
return weak_ref_tensors(x)
|
||||
if isinstance(x, tuple):
|
||||
return tuple(_weak_ref_if_tensor(e) for e in x)
|
||||
if isinstance(x, list):
|
||||
return [_weak_ref_if_tensor(e) for e in x]
|
||||
return x
|
||||
|
||||
|
||||
def _copy_output(dst: Any, src: Any) -> Any:
|
||||
"""Copy src output into dst in-place where possible.
|
||||
|
||||
Handles plain tensors, tuples/lists of tensors, dataclass/object with
|
||||
tensor attributes, and dicts of tensors. Returns dst if in-place copy
|
||||
succeeded, otherwise returns src.
|
||||
"""
|
||||
if torch.is_tensor(dst) and torch.is_tensor(src):
|
||||
dst.copy_(src)
|
||||
return dst
|
||||
|
||||
if (
|
||||
isinstance(dst, (tuple, list))
|
||||
and isinstance(src, (tuple, list))
|
||||
and len(dst) == len(src)
|
||||
):
|
||||
copied = [_copy_output(d, s) for d, s in zip(dst, src)]
|
||||
return tuple(copied) if isinstance(dst, tuple) else copied
|
||||
|
||||
if hasattr(dst, "__dict__") and hasattr(src, "__dict__"):
|
||||
for key, src_val in src.__dict__.items():
|
||||
dst_val = getattr(dst, key, None)
|
||||
if torch.is_tensor(dst_val) and torch.is_tensor(src_val):
|
||||
dst_val.copy_(src_val)
|
||||
else:
|
||||
setattr(dst, key, src_val)
|
||||
return dst
|
||||
|
||||
if isinstance(dst, dict) and isinstance(src, dict):
|
||||
for key, src_val in src.items():
|
||||
dst_val = dst.get(key)
|
||||
if torch.is_tensor(dst_val) and torch.is_tensor(src_val):
|
||||
dst_val.copy_(src_val)
|
||||
else:
|
||||
dst[key] = src_val
|
||||
return dst
|
||||
|
||||
return src
|
||||
|
||||
|
||||
def eager_on_graph(enable: bool):
|
||||
def decorator(inner: Callable):
|
||||
if not enable:
|
||||
return inner
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
capture = _current_capture_var.get()
|
||||
if capture is None:
|
||||
return inner(*args, **kwargs)
|
||||
|
||||
logger.debug("Break graph due to function: %s", inner.__name__)
|
||||
|
||||
# End the segment that captured up to this break point.
|
||||
capture._end_current_segment()
|
||||
|
||||
# Run the eager function once so it allocates its outputs and
|
||||
# writes real data into them.
|
||||
output = inner(*args, **kwargs)
|
||||
|
||||
# Weak-ref captured inputs produced by graph segments. Their storage
|
||||
# is pinned by the segment CUDAGraphs' mempool use-count, so Python
|
||||
# refs do not need to keep every intermediate alive.
|
||||
captured_inner = inner
|
||||
captured_args = tuple(_weak_ref_if_tensor(a) for a in args)
|
||||
captured_kwargs = {k: _weak_ref_if_tensor(v) for k, v in kwargs.items()}
|
||||
# The eager break output is different: it is allocated between graph
|
||||
# captures and is the static input address consumed by the next
|
||||
# captured segment. Keep a strong reference so replay can safely
|
||||
# copy fresh eager output into that bridge buffer.
|
||||
captured_output = output
|
||||
|
||||
def replay_fn():
|
||||
new_out = captured_inner(*captured_args, **captured_kwargs)
|
||||
return _copy_output(captured_output, new_out)
|
||||
|
||||
capture.cuda_graph._break_fns.append(replay_fn)
|
||||
|
||||
# Start a fresh CUDAGraph segment for the remainder of the forward.
|
||||
capture._begin_new_segment()
|
||||
return output
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class BreakableCUDAGraph:
|
||||
"""Container holding one torch.cuda.CUDAGraph per segment plus an
|
||||
eager break function between consecutive segments."""
|
||||
|
||||
def __init__(self, deduped_cuda_graph=None) -> None:
|
||||
self._segments: list[Any] = []
|
||||
self._break_fns: list[Callable[[], Any]] = []
|
||||
self._deduped_cuda_graph = deduped_cuda_graph
|
||||
|
||||
def replay(self) -> None:
|
||||
stream = torch.cuda.current_stream()
|
||||
token = _current_stream_var.set(stream)
|
||||
try:
|
||||
for i, seg in enumerate(self._segments):
|
||||
seg.replay()
|
||||
if i < len(self._break_fns):
|
||||
self._break_fns[i]()
|
||||
finally:
|
||||
_current_stream_var.reset(token)
|
||||
|
||||
def _append_segment(
|
||||
self, graph: torch.cuda.CUDAGraph, needs_instantiate: bool
|
||||
) -> None:
|
||||
if self._deduped_cuda_graph is not None:
|
||||
self._segments.append(self._deduped_cuda_graph.register(graph))
|
||||
return
|
||||
if needs_instantiate:
|
||||
graph.instantiate()
|
||||
self._segments.append(graph)
|
||||
|
||||
|
||||
class BreakableCUDAGraphCapture:
|
||||
"""Context manager that captures the enclosed code as one or more
|
||||
torch.cuda.CUDAGraph segments separated by eager break points.
|
||||
|
||||
Each segment shares the supplied pool (MempoolId_t tuple) so
|
||||
pool-allocated intermediates can be reused across segments. While any
|
||||
segment is alive, its beginAllocateToPool call keeps the mempool's
|
||||
use_count > 0, which makes weak_ref_tensor of segment-allocated
|
||||
tensors safe across subsequent replays.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cuda_graph: BreakableCUDAGraph,
|
||||
pool=None,
|
||||
stream: torch.cuda.Stream | None = None,
|
||||
capture_error_mode: str = "global",
|
||||
):
|
||||
assert isinstance(
|
||||
cuda_graph, BreakableCUDAGraph
|
||||
), "cuda_graph must be a BreakableCUDAGraph"
|
||||
self.cuda_graph = cuda_graph
|
||||
self._pool = pool if pool is not None else (0, 0)
|
||||
self._stream = stream
|
||||
self._capture_error_mode = capture_error_mode
|
||||
self._stream_ctx = None
|
||||
self._capture_token = None
|
||||
self._stream_token = None
|
||||
self._forked_token = None
|
||||
self._current_graph: torch.cuda.CUDAGraph | None = None
|
||||
self._current_graph_needs_instantiate = False
|
||||
|
||||
def __enter__(self):
|
||||
_install_wait_stream_hook()
|
||||
if self._stream is not None:
|
||||
self._stream_ctx = torch.cuda.stream(self._stream)
|
||||
self._stream_ctx.__enter__()
|
||||
self._capture_token = _current_capture_var.set(self)
|
||||
self._stream_token = _current_stream_var.set(
|
||||
self._stream or torch.cuda.current_stream()
|
||||
)
|
||||
self._forked_token = _forked_streams_var.set(set())
|
||||
self._begin_new_segment()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object):
|
||||
try:
|
||||
self._end_current_segment()
|
||||
finally:
|
||||
_forked_streams_var.reset(self._forked_token)
|
||||
_current_stream_var.reset(self._stream_token)
|
||||
_current_capture_var.reset(self._capture_token)
|
||||
if self._stream_ctx is not None:
|
||||
self._stream_ctx.__exit__(*args)
|
||||
self._stream_ctx = None
|
||||
_uninstall_wait_stream_hook()
|
||||
return False
|
||||
|
||||
def _begin_new_segment(self) -> None:
|
||||
# keep_graph retains the raw graph for dedup; skip it on the plain path.
|
||||
if self.cuda_graph._deduped_cuda_graph is not None:
|
||||
try:
|
||||
graph = torch.cuda.CUDAGraph(keep_graph=True)
|
||||
self._current_graph_needs_instantiate = True
|
||||
except TypeError:
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
self._current_graph_needs_instantiate = False
|
||||
else:
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
self._current_graph_needs_instantiate = False
|
||||
graph.capture_begin(
|
||||
pool=self._pool, capture_error_mode=self._capture_error_mode
|
||||
)
|
||||
self._current_graph = graph
|
||||
|
||||
def _end_current_segment(self) -> None:
|
||||
# Auto-join any side streams forked during this segment but not joined.
|
||||
main_stream = get_current_stream()
|
||||
forked = _forked_streams_var.get()
|
||||
if forked:
|
||||
assert _original_wait_stream is not None
|
||||
for side in list(forked):
|
||||
if _is_stream_capturing(side):
|
||||
_original_wait_stream(main_stream, side)
|
||||
forked.clear()
|
||||
graph = self._current_graph
|
||||
assert graph is not None
|
||||
graph.capture_end()
|
||||
self.cuda_graph._append_segment(graph, self._current_graph_needs_instantiate)
|
||||
self._current_graph = None
|
||||
self._current_graph_needs_instantiate = False
|
||||
|
||||
|
||||
@eager_on_graph(True)
|
||||
def break_graph() -> None:
|
||||
"""Insert a graph break. The @eager_on_graph decorator does the actual
|
||||
segment split; this function body intentionally does nothing."""
|
||||
pass
|
||||
@@ -0,0 +1,60 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""Runtime state for the breakable CUDA graph runner."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
|
||||
from sglang.srt.model_executor.cuda_graph_config import Backend
|
||||
from sglang.srt.model_executor.runner_backend_utils import (
|
||||
PREFILL_CUDA_GRAPH_CAPTURE_FAILED_MSG,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_in_breakable_cuda_graph = False
|
||||
|
||||
|
||||
def is_in_breakable_cuda_graph() -> bool:
|
||||
return _in_breakable_cuda_graph
|
||||
|
||||
|
||||
@contextmanager
|
||||
def enable_breakable_cuda_graph():
|
||||
"""Mark the enclosed scope as inside a BCG capture/replay. Any exception
|
||||
raised inside is logged with the BCG-specific failure hint, then re-raised
|
||||
for the caller to handle."""
|
||||
global _in_breakable_cuda_graph
|
||||
_in_breakable_cuda_graph = True
|
||||
try:
|
||||
yield
|
||||
except Exception as exc:
|
||||
msg = PREFILL_CUDA_GRAPH_CAPTURE_FAILED_MSG.format(
|
||||
backend=Backend.BREAKABLE, suggestions=BCG_FAILURE_HINT
|
||||
)
|
||||
logger.error(f"{type(exc).__name__}: {exc}\n{msg}")
|
||||
raise
|
||||
finally:
|
||||
_in_breakable_cuda_graph = False
|
||||
|
||||
|
||||
BCG_FAILURE_HINT = (
|
||||
"1. change to tc_piecewise by --cuda-graph-backend-prefill=tc_piecewise\n"
|
||||
"2. disable the prefill CUDA graph by --cuda-graph-backend-prefill=disabled\n"
|
||||
"3. if it is an OOM problem, set --mem-fraction-static to a smaller value "
|
||||
"(e.g., 0.8 or 0.7) or set --cuda-graph-max-bs-prefill to a smaller value "
|
||||
"(e.g., 2048)\n"
|
||||
)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""CUDA runtime binding utilities."""
|
||||
|
||||
try:
|
||||
from cuda.bindings import runtime as rt
|
||||
except ImportError:
|
||||
rt = None
|
||||
|
||||
|
||||
def _cudaGetErrorString(error):
|
||||
if rt is None:
|
||||
return "<cuda.bindings not available>"
|
||||
err, msg = rt.cudaGetErrorString(error)
|
||||
if err != rt.cudaError_t.cudaSuccess:
|
||||
return "<unknown>"
|
||||
if isinstance(msg, bytes):
|
||||
return msg.decode("utf-8", "replace")
|
||||
return str(msg)
|
||||
|
||||
|
||||
def checkCudaErrors(result):
|
||||
if rt is None:
|
||||
raise RuntimeError(
|
||||
"cuda.bindings is not available. "
|
||||
"Install it with: pip install cuda-python"
|
||||
)
|
||||
if result[0] != rt.cudaError_t.cudaSuccess:
|
||||
raise RuntimeError(
|
||||
f"CUDA error {int(result[0])}({_cudaGetErrorString(result[0])})"
|
||||
)
|
||||
if len(result) == 1:
|
||||
return None
|
||||
elif len(result) == 2:
|
||||
return result[1]
|
||||
else:
|
||||
return result[1:]
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
"""Piecewise CUDA graph utilities — shared between Breakable and tc_piecewise backends.
|
||||
|
||||
Public API:
|
||||
- is_in_tc_piecewise_cuda_graph() — true while inside any piecewise capture.
|
||||
- enable_tc_piecewise_cuda_graph() — context manager that toggles the flag.
|
||||
- TcPiecewiseForwardContext + set_tc_piecewise_forward_context + get_tc_piecewise_forward_context.
|
||||
- TCPCG_FAILURE_HINT — backend-switch suggestion plugged into
|
||||
PREFILL_CUDA_GRAPH_CAPTURE_FAILED_MSG by the prefill runner.
|
||||
|
||||
The torch.compile-warmup flag (is_in_torch_compile_warmup) lives in
|
||||
sglang.srt.compilation.compile_phase — it is torch.compile-internal,
|
||||
not piecewise-shared.
|
||||
"""
|
||||
|
||||
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph.context_manager import ( # noqa: F401
|
||||
TCPCG_FAILURE_HINT,
|
||||
TcPiecewiseForwardContext,
|
||||
enable_tc_piecewise_cuda_graph,
|
||||
get_tc_piecewise_forward_context,
|
||||
is_in_tc_piecewise_cuda_graph,
|
||||
set_tc_piecewise_forward_context,
|
||||
)
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
"""CUDA graph capture context manager + forward-context propagation.
|
||||
|
||||
Owns two pieces of cross-cutting state used by *every* piecewise-style
|
||||
backend (currently breakable + tc_piecewise):
|
||||
|
||||
* _in_tc_piecewise_cuda_graph — a process-global flag set true while we
|
||||
are inside the capture or replay window of a piecewise CUDA graph.
|
||||
Read by model code that needs to take the static-buffer / fixed-shape
|
||||
branch. See refactor/plan.md §6.5 for the full semantics.
|
||||
* TcPiecewiseForwardContext — a dataclass propagated across attention/MoE
|
||||
layers during capture and replay so that submodules can reach the
|
||||
current ForwardBatch and per-layer metadata without threading
|
||||
arguments through every call site. Named TcPiecewise… (matches
|
||||
Backend.TC_PIECEWISE + enable_tc_piecewise_cuda_graph) to
|
||||
disambiguate from the per-forward-call
|
||||
sglang.srt.model_executor.forward_context.ForwardContext.
|
||||
|
||||
This module deliberately does **not** own torch.compile-specific state
|
||||
(warmup flag, capture stream); those live in compilation/compile_phase.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, List, Optional
|
||||
|
||||
from sglang.srt.model_executor.cuda_graph_config import Backend
|
||||
from sglang.srt.model_executor.runner_backend_utils import (
|
||||
PREFILL_CUDA_GRAPH_CAPTURE_FAILED_MSG,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_in_tc_piecewise_cuda_graph = False
|
||||
|
||||
|
||||
def is_in_tc_piecewise_cuda_graph() -> bool:
|
||||
"""True while inside tc_piecewise CUDA graph capture/replay."""
|
||||
return _in_tc_piecewise_cuda_graph
|
||||
|
||||
|
||||
@contextmanager
|
||||
def enable_tc_piecewise_cuda_graph():
|
||||
"""Mark the enclosed scope as inside a tc_piecewise CUDA graph
|
||||
capture/replay. Any exception raised inside is logged with the
|
||||
PCG-specific failure hint, then re-raised for the caller to handle.
|
||||
"""
|
||||
global _in_tc_piecewise_cuda_graph
|
||||
_in_tc_piecewise_cuda_graph = True
|
||||
try:
|
||||
yield
|
||||
except Exception as exc:
|
||||
msg = PREFILL_CUDA_GRAPH_CAPTURE_FAILED_MSG.format(
|
||||
backend=Backend.TC_PIECEWISE, suggestions=TCPCG_FAILURE_HINT
|
||||
)
|
||||
logger.error(f"{type(exc).__name__}: {exc}\n{msg}")
|
||||
raise
|
||||
finally:
|
||||
_in_tc_piecewise_cuda_graph = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class TcPiecewiseForwardContext:
|
||||
forward_batch: Optional[ForwardBatch] = None
|
||||
attention_layers: Optional[List[Any]] = field(default=None)
|
||||
quant_config: Any = None
|
||||
moe_layers: Optional[List[Any]] = field(default=None)
|
||||
moe_fusions: Optional[List[Any]] = field(default=None)
|
||||
dsa_indexers: Optional[List[Any]] = field(default=None)
|
||||
num_tokens: Optional[int] = None
|
||||
raw_num_tokens: Optional[int] = None
|
||||
|
||||
|
||||
_tc_piecewise_forward_context: Optional[TcPiecewiseForwardContext] = None
|
||||
|
||||
|
||||
def get_tc_piecewise_forward_context() -> Optional[TcPiecewiseForwardContext]:
|
||||
return _tc_piecewise_forward_context
|
||||
|
||||
|
||||
@contextmanager
|
||||
def set_tc_piecewise_forward_context(
|
||||
forward_batch: ForwardBatch,
|
||||
attention_layers: List[Any],
|
||||
quant_config: Any,
|
||||
moe_layers: List[Any],
|
||||
moe_fusions: List[Any],
|
||||
dsa_indexers: Optional[List[Any]] = None,
|
||||
num_tokens: Optional[int] = None,
|
||||
raw_num_tokens: Optional[int] = None,
|
||||
):
|
||||
global _tc_piecewise_forward_context
|
||||
_tc_piecewise_forward_context = TcPiecewiseForwardContext(
|
||||
forward_batch=forward_batch,
|
||||
attention_layers=attention_layers,
|
||||
quant_config=quant_config,
|
||||
moe_layers=moe_layers,
|
||||
moe_fusions=moe_fusions,
|
||||
dsa_indexers=dsa_indexers,
|
||||
num_tokens=num_tokens,
|
||||
raw_num_tokens=raw_num_tokens,
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_tc_piecewise_forward_context = None
|
||||
|
||||
|
||||
TCPCG_FAILURE_HINT = (
|
||||
"1. change to breakable by --cuda-graph-backend-prefill=breakable\n"
|
||||
"2. disable the prefill CUDA graph by --cuda-graph-backend-prefill=disabled\n"
|
||||
"3. if it is an OOM problem, set --mem-fraction-static to a smaller value "
|
||||
"(e.g., 0.8 or 0.7) or set --cuda-graph-max-bs-prefill to a smaller value "
|
||||
"(e.g., 2048)\n"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Low-level utilities used by the CUDA graph runners.
|
||||
|
||||
Mirror of cuda_graph_backend_utils/ for runner-side state — buffer
|
||||
dataclasses, process-global capture flags, the speculative-shared
|
||||
graph memory pool, and the DeepEP capture/replay adapter. Runners in
|
||||
cuda_graph_runner/ import from here; nothing here should import
|
||||
back into cuda_graph_runner/.
|
||||
"""
|
||||
|
||||
from sglang.srt.model_executor.runner_utils.buffers import ( # noqa: F401
|
||||
DecodeInputBuffers,
|
||||
PrefillInputBuffers,
|
||||
_grouped_foreach_copy_,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_utils.capture_mode import ( # noqa: F401
|
||||
_set_capture_lora_variant,
|
||||
compile_in_capture_mode,
|
||||
get_capture_lora_variant,
|
||||
get_is_capture_mode,
|
||||
model_capture_mode,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_utils.deepep_adapter import ( # noqa: F401
|
||||
DeepEPCudaGraphRunnerAdapter,
|
||||
)
|
||||
from sglang.srt.model_executor.runner_utils.pool import ( # noqa: F401
|
||||
get_global_graph_memory_pool,
|
||||
set_global_graph_memory_pool,
|
||||
)
|
||||
@@ -0,0 +1,435 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""Static-buffer dataclasses used by the CUDA graph runners.
|
||||
|
||||
DecodeInputBuffers backs the decode-phase capture/replay path.
|
||||
PrefillInputBuffers backs the prefill-phase capture/replay path.
|
||||
|
||||
Both subclass ForwardInputBuffers so that buffer-pool sharing works
|
||||
the same way as for non-cuda-graph forward paths.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.model_executor.forward_batch_info import (
|
||||
ForwardBatch,
|
||||
NgramEmbeddingInfo,
|
||||
PPProxyTensors,
|
||||
compute_local_num_token_non_padded,
|
||||
)
|
||||
from sglang.srt.model_executor.input_buffers import ForwardInputBuffers
|
||||
|
||||
_has_foreach_copy = hasattr(torch, "_foreach_copy_")
|
||||
|
||||
|
||||
def _grouped_foreach_copy_(dsts: List[torch.Tensor], srcs: List[torch.Tensor]) -> None:
|
||||
"""Call torch._foreach_copy_ grouped by (dst_dtype, src_dtype) pairs."""
|
||||
|
||||
def foreach_copy(dsts: List[torch.Tensor], srcs: List[torch.Tensor]) -> None:
|
||||
if _has_foreach_copy:
|
||||
torch._foreach_copy_(dsts, srcs)
|
||||
else:
|
||||
for dst, src in zip(dsts, srcs):
|
||||
dst.copy_(src)
|
||||
|
||||
groups: Dict[Tuple[torch.dtype, torch.dtype], Tuple[List, List]] = {}
|
||||
for dst, src in zip(dsts, srcs):
|
||||
key = (dst.dtype, src.dtype)
|
||||
if key not in groups:
|
||||
groups[key] = ([], [])
|
||||
groups[key][0].append(dst)
|
||||
groups[key][1].append(src)
|
||||
for group_dsts, group_srcs in groups.values():
|
||||
foreach_copy(group_dsts, group_srcs)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DecodeInputBuffers(ForwardInputBuffers):
|
||||
input_ids: torch.Tensor
|
||||
input_embeds: torch.Tensor
|
||||
req_pool_indices: torch.Tensor
|
||||
seq_lens: torch.Tensor
|
||||
seq_lens_cpu: torch.Tensor
|
||||
out_cache_loc: torch.Tensor
|
||||
positions: torch.Tensor
|
||||
mrope_positions: torch.Tensor
|
||||
num_token_non_padded: torch.Tensor
|
||||
custom_mask: torch.Tensor
|
||||
next_token_logits_buffer: torch.Tensor
|
||||
mamba_track_indices: Optional[torch.Tensor]
|
||||
mamba_track_mask: Optional[torch.Tensor]
|
||||
global_num_tokens_gpu: torch.Tensor
|
||||
global_num_tokens_for_logprob_gpu: torch.Tensor
|
||||
encoder_lens: Optional[torch.Tensor]
|
||||
pp_proxy_tensors: Optional[Dict[str, torch.Tensor]]
|
||||
ngram_embedding_info: Optional[NgramEmbeddingInfo]
|
||||
rids_int: Optional[torch.Tensor]
|
||||
bootstrap_room_ids_int: Optional[torch.Tensor]
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
device: torch.device,
|
||||
max_bs: int,
|
||||
max_num_token: int,
|
||||
hidden_size: int,
|
||||
next_token_logits_buffer: torch.Tensor,
|
||||
dtype: torch.dtype,
|
||||
dp_size: int,
|
||||
pp_size: int,
|
||||
is_encoder_decoder: bool,
|
||||
require_mlp_tp_gather: bool,
|
||||
seq_len_fill_value: int,
|
||||
encoder_len_fill_value: int,
|
||||
num_tokens_per_bs: int,
|
||||
cache_loc_dtype: torch.dtype,
|
||||
enable_mamba_track: bool,
|
||||
ne_token_table: Optional[torch.Tensor] = None,
|
||||
hc_hidden_size: Optional[int] = None,
|
||||
pp_proxy_topk_size: Optional[int] = None,
|
||||
) -> DecodeInputBuffers:
|
||||
with torch.device(device):
|
||||
input_ids = torch.zeros((max_num_token,), dtype=torch.int64)
|
||||
input_embeds = torch.zeros((max_num_token, hidden_size), dtype=dtype)
|
||||
req_pool_indices = torch.zeros((max_bs,), dtype=torch.int64)
|
||||
seq_lens = torch.full((max_bs,), seq_len_fill_value, dtype=torch.int64)
|
||||
out_cache_loc = torch.zeros((max_num_token,), dtype=cache_loc_dtype)
|
||||
positions = torch.zeros((max_num_token,), dtype=torch.int64)
|
||||
mrope_positions = torch.zeros((3, max_num_token), dtype=torch.int64)
|
||||
num_token_non_padded = torch.zeros((1,), dtype=torch.int32)
|
||||
custom_mask = torch.ones(
|
||||
(max_bs * seq_len_fill_value + max_num_token) * num_tokens_per_bs,
|
||||
dtype=torch.bool,
|
||||
)
|
||||
mamba_track_indices = (
|
||||
torch.zeros((max_bs,), dtype=torch.int64)
|
||||
if enable_mamba_track
|
||||
else None
|
||||
)
|
||||
mamba_track_mask = (
|
||||
torch.zeros((max_bs,), dtype=torch.bool) if enable_mamba_track else None
|
||||
)
|
||||
|
||||
if pp_size > 1:
|
||||
is_mhc = hc_hidden_size is not None
|
||||
hs = hc_hidden_size if is_mhc else hidden_size
|
||||
pp_proxy_tensors = {
|
||||
"hidden_states": torch.zeros((max_bs, hs), dtype=dtype),
|
||||
}
|
||||
if not is_mhc:
|
||||
pp_proxy_tensors["residual"] = torch.zeros(
|
||||
(max_bs, hidden_size), dtype=dtype
|
||||
)
|
||||
if pp_proxy_topk_size is not None:
|
||||
pp_proxy_tensors["topk_indices"] = torch.zeros(
|
||||
(max_num_token, pp_proxy_topk_size), dtype=torch.int32
|
||||
)
|
||||
else:
|
||||
pp_proxy_tensors = None
|
||||
|
||||
if is_encoder_decoder:
|
||||
encoder_lens = torch.full(
|
||||
(max_bs,), encoder_len_fill_value, dtype=torch.int32
|
||||
)
|
||||
else:
|
||||
encoder_lens = None
|
||||
|
||||
if require_mlp_tp_gather:
|
||||
global_num_tokens_gpu = torch.zeros((dp_size,), dtype=torch.int32)
|
||||
global_num_tokens_for_logprob_gpu = torch.zeros(
|
||||
(dp_size,), dtype=torch.int32
|
||||
)
|
||||
else:
|
||||
global_num_tokens_gpu = torch.zeros((1,), dtype=torch.int32)
|
||||
global_num_tokens_for_logprob_gpu = torch.zeros((1,), dtype=torch.int32)
|
||||
|
||||
ngram_embedding_info = (
|
||||
NgramEmbeddingInfo(
|
||||
token_table=ne_token_table,
|
||||
column_starts=torch.zeros([max_bs], dtype=torch.int32),
|
||||
req_lens=torch.ones([max_bs], dtype=torch.int32),
|
||||
out_column_starts=torch.zeros([max_bs], dtype=torch.int32),
|
||||
out_req_lens=torch.ones([max_bs], dtype=torch.int32),
|
||||
skip_token_table_update=torch.zeros([max_bs], dtype=torch.bool),
|
||||
)
|
||||
if ne_token_table is not None
|
||||
else None
|
||||
)
|
||||
|
||||
if envs.SGLANG_KV_CANARY_ENABLE_TOKEN_ORACLE.get():
|
||||
rids_int = torch.zeros((max_bs,), dtype=torch.int64)
|
||||
bootstrap_room_ids_int = torch.full((max_bs,), -1, dtype=torch.int64)
|
||||
else:
|
||||
rids_int = None
|
||||
bootstrap_room_ids_int = None
|
||||
|
||||
seq_lens_cpu = torch.full(
|
||||
(max_bs,),
|
||||
seq_len_fill_value,
|
||||
dtype=torch.int64,
|
||||
device="cpu",
|
||||
)
|
||||
|
||||
return cls(
|
||||
input_ids=input_ids,
|
||||
input_embeds=input_embeds,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
seq_lens_cpu=seq_lens_cpu,
|
||||
out_cache_loc=out_cache_loc,
|
||||
positions=positions,
|
||||
mrope_positions=mrope_positions,
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
custom_mask=custom_mask,
|
||||
next_token_logits_buffer=next_token_logits_buffer,
|
||||
mamba_track_indices=mamba_track_indices,
|
||||
mamba_track_mask=mamba_track_mask,
|
||||
encoder_lens=encoder_lens,
|
||||
global_num_tokens_gpu=global_num_tokens_gpu,
|
||||
global_num_tokens_for_logprob_gpu=global_num_tokens_for_logprob_gpu,
|
||||
pp_proxy_tensors=pp_proxy_tensors,
|
||||
ngram_embedding_info=ngram_embedding_info,
|
||||
rids_int=rids_int,
|
||||
bootstrap_room_ids_int=bootstrap_room_ids_int,
|
||||
)
|
||||
|
||||
def populate_from_forward_batch(
|
||||
self,
|
||||
*,
|
||||
forward_batch: ForwardBatch,
|
||||
raw_bs: int,
|
||||
raw_num_token: int,
|
||||
bs: int,
|
||||
seq_len_fill_value: int,
|
||||
require_gathered_buffer: bool,
|
||||
num_tokens_per_bs: int,
|
||||
dsa_enable_prefill_cp: bool,
|
||||
enable_num_token_non_padded_flag: bool,
|
||||
pp_proxy_tensors: Optional[PPProxyTensors] = None,
|
||||
):
|
||||
if bs != raw_bs:
|
||||
self.seq_lens.fill_(seq_len_fill_value)
|
||||
self.out_cache_loc.zero_()
|
||||
if self.mamba_track_indices is not None:
|
||||
self.mamba_track_indices.zero_()
|
||||
if self.mamba_track_mask is not None:
|
||||
self.mamba_track_mask.fill_(False)
|
||||
|
||||
# Build batched copy lists for all GPU tensors.
|
||||
dsts = [
|
||||
self.input_ids[:raw_num_token],
|
||||
self.req_pool_indices[:raw_bs],
|
||||
self.seq_lens[:raw_bs],
|
||||
self.out_cache_loc[:raw_num_token],
|
||||
self.positions[:raw_num_token],
|
||||
]
|
||||
srcs = [
|
||||
forward_batch.input_ids,
|
||||
forward_batch.req_pool_indices,
|
||||
forward_batch.seq_lens,
|
||||
forward_batch.out_cache_loc,
|
||||
forward_batch.positions,
|
||||
]
|
||||
|
||||
if self.ngram_embedding_info is not None:
|
||||
ngram_embedding_info = forward_batch.ngram_embedding_info
|
||||
self.ngram_embedding_info.column_starts[:raw_bs].copy_(
|
||||
ngram_embedding_info.column_starts
|
||||
)
|
||||
self.ngram_embedding_info.req_lens[:raw_bs].copy_(
|
||||
ngram_embedding_info.req_lens
|
||||
)
|
||||
|
||||
if (
|
||||
self.mamba_track_indices is not None
|
||||
and forward_batch.mamba_track_indices is not None
|
||||
):
|
||||
dsts.append(self.mamba_track_indices[:raw_bs])
|
||||
srcs.append(forward_batch.mamba_track_indices)
|
||||
if (
|
||||
self.mamba_track_mask is not None
|
||||
and forward_batch.mamba_track_mask is not None
|
||||
):
|
||||
dsts.append(self.mamba_track_mask[:raw_bs])
|
||||
srcs.append(forward_batch.mamba_track_mask)
|
||||
|
||||
if self.encoder_lens is not None and forward_batch.encoder_lens is not None:
|
||||
dsts.append(self.encoder_lens[:raw_bs])
|
||||
srcs.append(forward_batch.encoder_lens)
|
||||
|
||||
if forward_batch.mrope_positions is not None:
|
||||
dsts.append(self.mrope_positions[:, :raw_num_token])
|
||||
srcs.append(forward_batch.mrope_positions)
|
||||
|
||||
if self.rids_int is not None and forward_batch.rids_int is not None:
|
||||
dsts.append(self.rids_int[:raw_bs])
|
||||
srcs.append(forward_batch.rids_int)
|
||||
if (
|
||||
self.bootstrap_room_ids_int is not None
|
||||
and forward_batch.bootstrap_room_ids_int is not None
|
||||
):
|
||||
dsts.append(self.bootstrap_room_ids_int[:raw_bs])
|
||||
srcs.append(forward_batch.bootstrap_room_ids_int)
|
||||
|
||||
if require_gathered_buffer:
|
||||
self.global_num_tokens_gpu.fill_(bs * num_tokens_per_bs)
|
||||
self.global_num_tokens_for_logprob_gpu.fill_(bs * num_tokens_per_bs)
|
||||
|
||||
if enable_num_token_non_padded_flag:
|
||||
if require_gathered_buffer and not dsa_enable_prefill_cp:
|
||||
num_tokens_per_dp = bs * num_tokens_per_bs
|
||||
local = compute_local_num_token_non_padded(
|
||||
global_num_token_non_padded=forward_batch.num_token_non_padded,
|
||||
num_tokens_per_dp=num_tokens_per_dp,
|
||||
)
|
||||
dsts.append(self.num_token_non_padded)
|
||||
srcs.append(local)
|
||||
else:
|
||||
dsts.append(self.num_token_non_padded)
|
||||
srcs.append(forward_batch.num_token_non_padded)
|
||||
|
||||
# Pipeline-parallel proxy tensors.
|
||||
if pp_proxy_tensors is not None and self.pp_proxy_tensors is not None:
|
||||
for key, buf in self.pp_proxy_tensors.items():
|
||||
src = pp_proxy_tensors.tensors[key]
|
||||
dim = src.shape[0]
|
||||
dsts.append(buf[:dim])
|
||||
srcs.append(src)
|
||||
|
||||
# Batch all GPU copies, grouped by dtype pair.
|
||||
_grouped_foreach_copy_(dsts, srcs)
|
||||
|
||||
if forward_batch.seq_lens_cpu is not None:
|
||||
if bs != raw_bs:
|
||||
self.seq_lens_cpu.fill_(seq_len_fill_value)
|
||||
self.seq_lens_cpu[:raw_bs].copy_(forward_batch.seq_lens_cpu)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrefillInputBuffers(ForwardInputBuffers):
|
||||
input_ids: torch.Tensor
|
||||
out_cache_loc: torch.Tensor
|
||||
num_token_non_padded: torch.Tensor
|
||||
mamba_track_indices: Optional[torch.Tensor]
|
||||
mamba_track_mask: Optional[torch.Tensor]
|
||||
mamba_track_seqlens: Optional[torch.Tensor]
|
||||
positions: torch.Tensor
|
||||
input_embeds: Optional[torch.Tensor]
|
||||
mrope_positions: Optional[torch.Tensor]
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
device: torch.device,
|
||||
max_bs: int,
|
||||
max_num_tokens: int,
|
||||
cache_loc_dtype: torch.dtype,
|
||||
is_multimodal: bool,
|
||||
hidden_size: int,
|
||||
dtype: torch.dtype,
|
||||
enable_mamba_track: bool,
|
||||
) -> PrefillInputBuffers:
|
||||
with torch.device(device):
|
||||
input_ids = torch.zeros((max_num_tokens,), dtype=torch.int64)
|
||||
out_cache_loc = torch.zeros((max_num_tokens,), dtype=cache_loc_dtype)
|
||||
num_token_non_padded = torch.zeros((1,), dtype=torch.int32)
|
||||
mamba_track_indices = (
|
||||
torch.zeros((max_bs,), dtype=torch.int64)
|
||||
if enable_mamba_track
|
||||
else None
|
||||
)
|
||||
mamba_track_mask = (
|
||||
torch.zeros((max_bs,), dtype=torch.bool) if enable_mamba_track else None
|
||||
)
|
||||
mamba_track_seqlens = (
|
||||
torch.zeros((max_bs,), dtype=torch.int32)
|
||||
if enable_mamba_track
|
||||
else None
|
||||
)
|
||||
positions = torch.zeros((max_num_tokens,), dtype=torch.int64)
|
||||
|
||||
if is_multimodal:
|
||||
input_embeds = torch.zeros((max_num_tokens, hidden_size), dtype=dtype)
|
||||
mrope_positions = torch.zeros((3, max_num_tokens), dtype=torch.int64)
|
||||
else:
|
||||
input_embeds = None
|
||||
mrope_positions = None
|
||||
|
||||
return cls(
|
||||
input_ids=input_ids,
|
||||
out_cache_loc=out_cache_loc,
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
mamba_track_indices=mamba_track_indices,
|
||||
mamba_track_mask=mamba_track_mask,
|
||||
mamba_track_seqlens=mamba_track_seqlens,
|
||||
positions=positions,
|
||||
input_embeds=input_embeds,
|
||||
mrope_positions=mrope_positions,
|
||||
)
|
||||
|
||||
def populate_from_forward_batch(
|
||||
self,
|
||||
*,
|
||||
forward_batch: ForwardBatch,
|
||||
raw_num_tokens: int,
|
||||
static_num_tokens: int,
|
||||
is_multimodal: bool,
|
||||
) -> None:
|
||||
"""Copy serving-batch values into static buffers and zero out
|
||||
the padding region between raw_num_tokens and
|
||||
static_num_tokens.
|
||||
"""
|
||||
if static_num_tokens != raw_num_tokens:
|
||||
self.out_cache_loc.zero_()
|
||||
self.input_ids[raw_num_tokens:static_num_tokens].zero_()
|
||||
self.positions[raw_num_tokens:static_num_tokens].zero_()
|
||||
if is_multimodal:
|
||||
self.input_embeds[raw_num_tokens:static_num_tokens].zero_()
|
||||
if forward_batch.mrope_positions is not None:
|
||||
self.mrope_positions[:, raw_num_tokens:static_num_tokens].zero_()
|
||||
|
||||
bs = forward_batch.batch_size
|
||||
|
||||
self.input_ids[:raw_num_tokens].copy_(forward_batch.input_ids)
|
||||
self.positions[:raw_num_tokens].copy_(forward_batch.positions)
|
||||
self.out_cache_loc[:raw_num_tokens].copy_(forward_batch.out_cache_loc)
|
||||
|
||||
if (
|
||||
self.mamba_track_indices is not None
|
||||
and forward_batch.mamba_track_indices is not None
|
||||
):
|
||||
self.mamba_track_indices[:bs].copy_(forward_batch.mamba_track_indices)
|
||||
if (
|
||||
self.mamba_track_mask is not None
|
||||
and forward_batch.mamba_track_mask is not None
|
||||
):
|
||||
self.mamba_track_mask[:bs].copy_(forward_batch.mamba_track_mask)
|
||||
if (
|
||||
self.mamba_track_seqlens is not None
|
||||
and forward_batch.mamba_track_seqlens is not None
|
||||
):
|
||||
self.mamba_track_seqlens[:bs].copy_(forward_batch.mamba_track_seqlens)
|
||||
|
||||
if forward_batch.mrope_positions is not None:
|
||||
self.mrope_positions[:, :raw_num_tokens].copy_(
|
||||
forward_batch.mrope_positions
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""Process-global capture-mode flags shared by the decode runner and the
|
||||
speculative-draft runners. Read by model code that needs to take a
|
||||
capture-time branch (e.g. lora dual-graph capture decides per-batch
|
||||
which variant to use).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.context import (
|
||||
is_in_breakable_cuda_graph,
|
||||
)
|
||||
|
||||
# Detect whether the current forward pass is in capture mode.
|
||||
is_capture_mode = False
|
||||
|
||||
# When capturing dual MoE backends, tracks which variant is being captured.
|
||||
# None = not dual, "lora" = capturing lora variant, "nolora" = capturing nolora variant.
|
||||
_capture_lora_variant: Optional[str] = None
|
||||
|
||||
|
||||
def get_is_capture_mode() -> bool:
|
||||
return is_capture_mode or is_in_breakable_cuda_graph()
|
||||
|
||||
|
||||
def compile_in_capture_mode(func):
|
||||
"""Decorator: wrap func with torch.compile only when defined
|
||||
inside model capture mode; passthrough otherwise.
|
||||
|
||||
Used by model code (e.g. DeepSeek-V4) to opt nested helpers into
|
||||
torch.compile during cuda-graph capture without paying the
|
||||
compilation cost in the eager forward path.
|
||||
"""
|
||||
if is_capture_mode:
|
||||
return torch.compile(func)
|
||||
return func
|
||||
|
||||
|
||||
def get_capture_lora_variant() -> Optional[str]:
|
||||
"""Return the lora variant being captured, or None if not in dual capture."""
|
||||
return _capture_lora_variant
|
||||
|
||||
|
||||
def _set_capture_lora_variant(variant: Optional[str]) -> None:
|
||||
global _capture_lora_variant
|
||||
_capture_lora_variant = variant
|
||||
|
||||
|
||||
@contextmanager
|
||||
def model_capture_mode():
|
||||
global is_capture_mode
|
||||
from sglang.srt.runtime_context import get_flags
|
||||
|
||||
# Disable dispose_tensor() during capture: freeing mid-capture records data_ptr()==0 into the graph.
|
||||
is_capture_mode = True
|
||||
get_flags().capture.disable_dispose_tensor = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
is_capture_mode = False
|
||||
get_flags().capture.disable_dispose_tensor = False
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""DeepEP capture/replay adapter — records the dispatch mode used during
|
||||
capture and re-applies it during replay so DeepEP all-to-all has
|
||||
consistent expert routing across the captured graph.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sglang.srt.layers.moe.token_dispatcher.deepep import DeepEPBuffer
|
||||
from sglang.srt.layers.moe.utils import get_deepep_mode, get_moe_a2a_backend
|
||||
|
||||
|
||||
class DeepEPCudaGraphRunnerAdapter:
|
||||
def __init__(self) -> None:
|
||||
# Record DeepEP mode used during capture to ensure replay consistency.
|
||||
self._captured_deepep_mode = None
|
||||
|
||||
def capture(self, is_extend_in_batch: bool) -> None:
|
||||
if not get_moe_a2a_backend().is_deepep():
|
||||
return
|
||||
self._captured_deepep_mode = get_deepep_mode().resolve(
|
||||
is_extend_in_batch=is_extend_in_batch
|
||||
)
|
||||
DeepEPBuffer.set_dispatch_mode(self._captured_deepep_mode)
|
||||
|
||||
def replay(self) -> None:
|
||||
if not get_moe_a2a_backend().is_deepep():
|
||||
return
|
||||
assert self._captured_deepep_mode is not None
|
||||
DeepEPBuffer.set_dispatch_mode(self._captured_deepep_mode)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright 2023-2026 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.
|
||||
# ==============================================================================
|
||||
"""Process-wide CUDA graph memory pool shared across the prefill and
|
||||
decode graph backends. The two phases never replay concurrently, so
|
||||
sharing one pool reserves only the larger phase's capture footprint.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from sglang.srt.runtime_context import get_resources
|
||||
|
||||
|
||||
def get_global_graph_memory_pool() -> Optional[Any]:
|
||||
return get_resources().graph_memory_pool
|
||||
|
||||
|
||||
def set_global_graph_memory_pool(val: Any) -> None:
|
||||
get_resources().graph_memory_pool = val
|
||||
|
||||
|
||||
def get_or_create_global_graph_memory_pool(device_module: Any) -> Any:
|
||||
"""Return the shared graph memory pool, creating it on first use so
|
||||
later backends reuse the same handle."""
|
||||
resources = get_resources()
|
||||
if resources.graph_memory_pool is None:
|
||||
resources.graph_memory_pool = device_module.graph_pool_handle()
|
||||
return resources.graph_memory_pool
|
||||
Reference in New Issue
Block a user