Files
wehub-resource-sync 94057c3d3e
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
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

473 lines
18 KiB
Python

# 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 (BCG) runner for diffusion DiT transformers.
A runner wraps a callable ``nn.Module`` and turns it into an *eager runner* that
transparently proxies every attribute to the wrapped module and, when called,
replays a previously captured graph for the input signature — or runs the
module eagerly when no graph was captured for that signature. Capture is an
explicit, idempotent ``capture()`` call (driven at warmup) so that serving never
triggers a fresh capture.
This file is intentionally local to ``multimodal_gen``: diffusion reuses the
low-level SRT BCG primitives, but the capture/replay runner owns diffusion DiT
signature handling, static tensor buffers, prompt-bucket warmup, and fallback
behavior.
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
from typing import Any
import torch
import torch.nn as nn
from sglang.multimodal_gen.runtime.breakable_cuda_graph.replay_token import (
replay_token_scope,
)
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.breakable_cuda_graph import (
BreakableCUDAGraph,
BreakableCUDAGraphCapture,
)
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.context import (
enable_breakable_cuda_graph,
)
# Log under the multimodal_gen namespace so the diffusion server's logging
# config surfaces the "[Diffusion BCG] captured ..." lines.
logger = logging.getLogger(__name__)
def _env_int(name: str, default: int) -> int:
raw = os.environ.get(name)
if raw is None:
return default
try:
return int(raw)
except ValueError:
logger.warning("[BCG] ignoring invalid integer %s=%r", name, raw)
return default
def _env_float(name: str, default: float) -> float:
raw = os.environ.get(name)
if raw is None:
return default
try:
return float(raw)
except ValueError:
logger.warning("[BCG] ignoring invalid float %s=%r", name, raw)
return default
def _map_tensors(obj, fn):
"""Rebuild ``obj`` applying ``fn`` to every tensor leaf, recursing into
list/tuple/dict containers; everything else passes through unchanged."""
if torch.is_tensor(obj):
return fn(obj)
if isinstance(obj, tuple):
return tuple(_map_tensors(o, fn) for o in obj)
if isinstance(obj, list):
return [_map_tensors(o, fn) for o in obj]
if isinstance(obj, dict):
return {k: _map_tensors(v, fn) for k, v in obj.items()}
return obj
def _flatten_tensors(obj, out: list):
"""Depth-first collect every tensor leaf into ``out`` (deterministic order:
dicts traversed in sorted-key order to match across calls)."""
if torch.is_tensor(obj):
out.append(obj)
elif isinstance(obj, (list, tuple)):
for o in obj:
_flatten_tensors(o, out)
elif isinstance(obj, dict):
for k in sorted(obj):
_flatten_tensors(obj[k], out)
def _flatten_kwargs(kwargs: dict[str, Any]) -> list[torch.Tensor]:
out: list[torch.Tensor] = []
for name in sorted(kwargs):
_flatten_tensors(kwargs[name], out)
return out
def _signature_leaf(obj: Any) -> Any:
if torch.is_tensor(obj):
return ("tensor", tuple(obj.shape), str(obj.dtype))
if isinstance(obj, tuple):
return ("tuple", tuple(_signature_leaf(o) for o in obj))
if isinstance(obj, list):
return ("list", tuple(_signature_leaf(o) for o in obj))
if isinstance(obj, dict):
return (
"dict",
tuple((k, _signature_leaf(obj[k])) for k in sorted(obj)),
)
if obj is None or isinstance(obj, (bool, int, float, str)):
return ("const", obj)
return ("object", type(obj).__module__, type(obj).__qualname__, id(obj))
def _signature_kwargs(kwargs: dict[str, Any]) -> tuple:
return tuple((name, _signature_leaf(kwargs[name])) for name in sorted(kwargs))
def _signature_summary_leaf(sig: Any, *, depth: int = 0) -> Any:
if not isinstance(sig, tuple) or not sig:
return sig
tag = sig[0]
if tag == "tensor":
return sig
if tag == "const":
value = sig[1]
if isinstance(value, str) and len(value) > 64:
value = value[:61] + "..."
return (tag, value)
if tag == "object":
return sig[:3]
if depth >= 2:
return (tag, "...")
if tag in ("tuple", "list"):
items = sig[1]
preview = tuple(
_signature_summary_leaf(item, depth=depth + 1) for item in items[:4]
)
if len(items) > 4:
preview += (("...", len(items) - 4),)
return (tag, len(items), preview)
if tag == "dict":
items = sig[1]
preview = tuple(
(key, _signature_summary_leaf(value, depth=depth + 1))
for key, value in items[:4]
)
if len(items) > 4:
preview += (("...", len(items) - 4),)
return (tag, len(items), preview)
return sig
def _signature_summary(key: tuple) -> tuple:
return tuple((name, _signature_summary_leaf(value)) for name, value in key[:16]) + (
(("...", len(key) - 16),) if len(key) > 16 else ()
)
def _clone_output(out: Any) -> Any:
if torch.is_tensor(out):
return out.clone()
if isinstance(out, tuple):
return tuple(_clone_output(o) for o in out)
if isinstance(out, list):
return [_clone_output(o) for o in out]
return out
@dataclass
class _CaptureEntry:
graph: BreakableCUDAGraph
# full captured kwargs with persistent static buffers at every tensor leaf
static_kwargs: dict[str, Any]
# the same static buffers, flattened in _flatten_kwargs order (replay copies
# live tensors into these positionally)
static_leaves: list[torch.Tensor]
output: Any
num_segments: int
class _CaptureRejected(RuntimeError):
pass
class BaseBreakableCudaGraphRunner:
"""Eager runner around ``transformer`` with an explicit capture/replay API.
The capture/replay contract:
* :meth:`capture` captures a BCG graph for the given input signature, once
(idempotent). It is intended to be driven at warmup so that every
signature served later is already captured.
* :meth:`replay` copies live inputs into the captured static buffers and
replays the graph, returning a clone of the captured output.
* :meth:`__call__` is the *eager runner*: it replays when a graph exists for
the signature and otherwise runs ``transformer`` eagerly. It never
captures, so serving never pays a capture cost.
Any attribute not defined on the runner is proxied to ``transformer`` so the
runner can stand in for the wrapped module ("other functions directly
pass").
"""
def __init__(
self,
transformer: nn.Module,
device: torch.device,
pool=None,
) -> None:
self.transformer = transformer
self.device = device
self.device_module = torch.get_device_module(device)
# One shared mempool across all captured graphs/segments so per-block
# intermediates can be reclaimed and weak-ref'd safely.
self._pool = (
pool if pool is not None else self.device_module.graph_pool_handle()
)
self._capture_stream = self.device_module.Stream(device=device)
self.entries: dict[tuple, _CaptureEntry] = {}
# Signatures we have given up capturing (capture raised); run eager.
self._blocked: set[tuple] = set()
self._disabled_reason: str | None = None
self.max_entries = max(0, _env_int("SGLANG_DIFFUSION_BCG_MAX_ENTRIES", 32))
self.max_segments = max(0, _env_int("SGLANG_DIFFUSION_BCG_MAX_SEGMENTS", 128))
def __getattr__(self, name: str) -> Any:
# Only reached for attributes the runner itself does not define; proxy
# them to the wrapped transformer so callers can treat the runner as a
# transparent stand-in. Use __dict__ to avoid recursing through
# __getattr__ before ``transformer`` is assigned in __init__.
try:
transformer = self.__dict__["transformer"]
except KeyError as e: # pragma: no cover - during/ before __init__
raise AttributeError(name) from e
return getattr(transformer, name)
# ------------------------------------------------------------------ #
# Public capture / replay API
# ------------------------------------------------------------------ #
@torch.no_grad()
def capture(self, **kwargs) -> bool:
"""Capture a graph for ``kwargs``'s signature if not already captured.
Idempotent: returns ``True`` when a graph is available for the
signature afterwards (already captured or newly captured), ``False``
when capture is disabled/blocked or failed (the caller then runs eager).
"""
if self._disabled_reason is not None:
return False
key = self._signature(kwargs)
if key in self._blocked:
return False
if key in self.entries:
return True
try:
entry = self._capture(kwargs, key)
except Exception as e: # noqa: BLE001 — never break generation on capture
logger.warning(
"[Diffusion BCG] capture failed for signature %s (%s); "
"this signature will run eager.",
_signature_summary(key),
e,
)
self._blocked.add(key)
return False
self.entries[key] = entry
self._evict_entries_if_needed()
return True
def _should_capture_on_call(self, key: tuple) -> bool:
"""Whether ``__call__`` may lazily capture an unseen signature.
Base runners only ever capture through the explicit :meth:`capture`
API, so this returns ``False``: serving never records a fresh graph.
Subclasses gate lazy capture on a warmup window (see the diffusion
runner) so warmup can capture by simply driving the forward as usual.
"""
return False
@torch.no_grad()
def __call__(self, **kwargs) -> Any:
"""Eager runner: replay a captured graph, else run ``transformer``.
While serving this never captures, so no new graph is recorded once
warmup is over. During the warmup window subclasses opt into lazy
capture via :meth:`_should_capture_on_call`.
"""
if self._disabled_reason is not None:
return self.transformer(**kwargs)
key = self._signature(kwargs)
entry = self.entries.get(key)
if entry is None:
if not self._should_capture_on_call(key):
return self.transformer(**kwargs)
if not self.capture(**kwargs):
return self.transformer(**kwargs)
entry = self.entries[key]
return self.replay(entry, kwargs)
def replay(self, entry: _CaptureEntry, kwargs: dict[str, Any]) -> Any:
live_leaves = _flatten_kwargs(kwargs)
if len(live_leaves) != len(entry.static_leaves):
# Structure changed under a matching shape key — should not happen;
# fall back to eager rather than copy mismatched buffers.
return self.transformer(**kwargs)
for buf, live in zip(entry.static_leaves, live_leaves):
buf.copy_(live, non_blocking=True)
with replay_token_scope():
entry.graph.replay()
# Clone so the caller can hold the result across the next replay / the
# other CFG branch (which shares this static output buffer when shapes
# match). The clone is one cheap DtoD copy relative to the full DiT.
return _clone_output(entry.output)
# ------------------------------------------------------------------ #
# Internals
# ------------------------------------------------------------------ #
def _signature(self, kwargs: dict[str, Any]) -> tuple:
"""Capture key for tensor leaves and non-tensor control values.
Tensor leaves are keyed by shape+dtype so their values can change per
replay. Non-tensor leaves are baked into the captured Python control
flow, so simple constants must be part of the key as well. Mutable
objects are keyed by identity to avoid replaying a graph whose eager
break points still reference a previous request's state object.
"""
return _signature_kwargs(kwargs)
def _empty_cache(self) -> None:
empty_cache = getattr(self.device_module, "empty_cache", None)
if callable(empty_cache):
empty_cache()
@staticmethod
def _drop_entry(entry: _CaptureEntry) -> None:
entry.graph._break_fns.clear()
entry.graph._segments.clear()
entry.static_kwargs.clear()
entry.static_leaves.clear()
entry.output = None
def reset(self, *, disabled_reason: str | None = None) -> None:
for entry in self.entries.values():
self._drop_entry(entry)
self.entries.clear()
self._blocked.clear()
self._pool = None
self._empty_cache()
if disabled_reason is not None:
self._disabled_reason = disabled_reason
def _capture_limit_reason(self, entry: _CaptureEntry) -> str | None:
if self.max_segments and entry.num_segments > self.max_segments:
return (
f"captured {entry.num_segments} segments, above "
f"SGLANG_DIFFUSION_BCG_MAX_SEGMENTS={self.max_segments}"
)
return None
def _evict_entries_if_needed(self) -> None:
if not self.max_entries:
return
while len(self.entries) > self.max_entries:
evicted_key = next(iter(self.entries))
entry = self.entries.pop(evicted_key)
self._drop_entry(entry)
logger.info(
"[Diffusion BCG] evicted oldest capture for signature %s "
"(SGLANG_DIFFUSION_BCG_MAX_ENTRIES=%d)",
_signature_summary(evicted_key),
self.max_entries,
)
self._empty_cache()
def _capture(self, kwargs: dict[str, Any], key: tuple) -> _CaptureEntry:
if self._pool is None:
self._pool = self.device_module.graph_pool_handle()
# Persistent static buffers at every tensor leaf; bake non-tensors.
def _to_static(t: torch.Tensor) -> torch.Tensor:
# Static buffers live on the capture device. A CPU input (e.g. a
# scalar timestep/sigma or an index tensor built on the host)
# would otherwise force a CPU->CUDA copy inside the captured
# region, which is illegal; place its buffer on the device so the
# only host->device copy happens here, before capture, and replay
# is device-to-device.
if t.device.type == "cpu":
buf = torch.empty(t.shape, dtype=t.dtype, device=self.device)
else:
buf = torch.empty_like(t)
buf.copy_(t)
return buf
static_kwargs = {
name: _map_tensors(v, _to_static) for name, v in kwargs.items()
}
static_leaves = _flatten_kwargs(static_kwargs)
# Warm up on the capture stream so cuBLAS/cuDNN/Triton workspaces and
# any lazy JIT are materialized before capture (mirrors the LLM runner
# and torch.cuda.make_graphed_callables).
self.device_module.synchronize()
with self.device_module.stream(self._capture_stream):
for _ in range(2):
self.transformer(**static_kwargs)
self._capture_stream.synchronize()
self.device_module.synchronize()
graph = BreakableCUDAGraph()
with enable_breakable_cuda_graph():
with BreakableCUDAGraphCapture(
cuda_graph=graph, pool=self._pool, stream=self._capture_stream
):
output = self.transformer(**static_kwargs)
self.device_module.synchronize()
logger.info(
"[Diffusion BCG] captured %d segment(s), %d tensor input(s) for "
"signature %s",
len(graph._segments),
len(static_leaves),
_signature_summary(key),
)
entry = _CaptureEntry(
graph=graph,
static_kwargs=static_kwargs,
static_leaves=static_leaves,
output=output,
num_segments=len(graph._segments),
)
limit_reason = self._capture_limit_reason(entry)
if limit_reason is not None:
self._drop_entry(entry)
self.reset(disabled_reason=limit_reason)
raise _CaptureRejected(
f"{limit_reason}; disabling this BCG runner and using eager"
)
return entry
class DiffusionBreakableCudaGraphRunner(BaseBreakableCudaGraphRunner):
"""Capture/replay a diffusion DiT ``transformer`` with BCG.
Unknown attributes proxy to the wrapped transformer, so the runner can
stand in for the module while only intercepting ``forward`` calls.
"""
def _should_capture_on_call(self, key) -> bool:
try:
from sglang.multimodal_gen.runtime.managers.forward_context import (
get_forward_context,
)
forward_batch = get_forward_context().forward_batch
except Exception:
return False
return bool(getattr(forward_batch, "is_warmup", False))