fix: fix vlm cuda graph shape stability (#30868)

This commit is contained in:
Mick
2026-07-19 22:35:51 +08:00
committed by GitHub
parent a03ca46a28
commit d4801be447
11 changed files with 327 additions and 27 deletions
+45 -6
View File
@@ -86,6 +86,22 @@ def _mark_dynamic_on_value(val, dims):
# else: ignore (None or non-tensor)
_MROPE_TOKEN_AXIS_ARGUMENTS = frozenset(
{"positions", "position_ids", "mrope_positions"}
)
def _runtime_dynamic_dim_for_argument(name: str) -> int:
"""Return the runtime-sized tensor dimension for a compiled argument.
Most compiler-facing tensors are batch/token-major and vary on dim 0.
mRoPE positions instead have shape ``[rope_axis, token]``; their runtime
token length is therefore the final dimension. ``maybe_mark_dynamic``
accepts ``-1`` for that final dimension irrespective of tensor rank.
"""
return -1 if name in _MROPE_TOKEN_AXIS_ARGUMENTS else 0
def _infer_dynamic_arg_dims_from_annotations(forward_fn):
sig = inspect.signature(forward_fn)
dyn = {}
@@ -96,7 +112,7 @@ def _infer_dynamic_arg_dims_from_annotations(forward_fn):
ann is torch.Tensor
or getattr(getattr(ann, "__args__", [None])[0], "__name__", "") == "Tensor"
):
dyn[name] = 0
dyn[name] = _runtime_dynamic_dim_for_argument(name)
elif getattr(ann, "__name__", "") in ("IntermediateTensors",) or any(
getattr(a, "__name__", "") == "IntermediateTensors"
for a in getattr(ann, "__args__", [])
@@ -104,12 +120,33 @@ def _infer_dynamic_arg_dims_from_annotations(forward_fn):
dyn[name] = 0
elif ann == "torch.Tensor" or ann == "Optional[torch.Tensor]":
# For future import annotations (e.g. from __future__ import annotations), the annotation is a string
dyn[name] = 0
dyn[name] = _runtime_dynamic_dim_for_argument(name)
if not dyn:
raise ValueError("No dynamic dims inferred; pass dynamic_arg_dims explicitly.")
return dyn
def _mark_dynamic_forward_batch(forward_batch) -> None:
"""Mark runtime-sized ForwardBatch tensors before the first PCG trace.
``ForwardBatch`` is deliberately not annotated as a Tensor, so the
annotation-based argument inference above cannot see its tensor fields.
PCG nevertheless reads sequence- and token-sized metadata from it in
attention layers. Leaving those dimensions static gives Dynamo a guard on
the dummy capture shape and silently creates a new backend for a real VLM
request. All direct tensor fields are batch-major except mRoPE positions,
whose token axis is the final dimension.
"""
if forward_batch is None:
return
for name, value in vars(forward_batch).items():
if not isinstance(value, torch.Tensor) or value.ndim == 0:
continue
dims = _runtime_dynamic_dim_for_argument(name)
_mark_dynamic_on_value(value, dims)
def install_torch_compiled(
module: torch.nn.Module,
*,
@@ -129,9 +166,8 @@ def install_torch_compiled(
if backend_factory is None:
from sglang.srt.compilation.backend import SGLangBackend
backend_factory = lambda gm, ex: SGLangBackend(compile_config, graph_pool)(
gm, ex
)
def backend_factory(gm, ex):
return SGLangBackend(compile_config, graph_pool)(gm, ex)
compiled_codes: list[type(original_code)] = []
state = {"compiled": False, "compiled_callable": None}
@@ -172,13 +208,16 @@ def install_torch_compiled(
val = ba.arguments[name]
if val is not None:
_mark_dynamic_on_value(val, dims)
_mark_dynamic_forward_batch(ba.arguments.get("forward_batch"))
# Avoid cross-instance cache reuse
torch._dynamo.eval_frame.remove_from_cache(unbound_fwd.__code__)
bound = types.MethodType(unbound_fwd, self)
compiled_callable = torch.compile(
bound, fullgraph=fullgraph, backend=backend_factory
bound,
fullgraph=fullgraph,
backend=backend_factory,
)
# Trigger Dynamo so bytecode hook can capture
@@ -18,11 +18,9 @@ from sglang.srt.compilation.compile_phase import (
is_in_torch_compile_warmup,
)
from sglang.srt.compilation.weak_ref_tensor import weak_ref_tensors
from sglang.srt.utils import is_hip
from sglang.srt.utils.common import print_warning_once
logger = logging.getLogger(__name__)
_is_hip = is_hip()
@dataclasses.dataclass
@@ -155,22 +153,21 @@ class CUDAPiecewiseBackend:
# During normal capture (PiecewiseCudaGraphRunner.capture()),
# set_pcg_capture_stream() guarantees a valid stream. However,
# Dynamo may silently recompile on HIP/MLA serving batches whose
# token count exceeds the captured range. The replacement backend
# has no capture stream; fall back there instead of crashing while
# preserving the original assertion on other platforms.
# Dynamo may silently recompile serving batches when a dynamic
# multimodal input introduces a previously unseen guard. The
# replacement backend has no capture stream, so it cannot safely
# create a CUDA graph. Execute that subgraph normally instead of
# crashing the scheduler; subsequent matching shapes still use
# their captured graphs.
stream = get_pcg_capture_stream()
if _is_hip and stream is None:
if stream is None:
print_warning_once(
"PCG capture stream is not set; likely a Dynamo runtime "
"recompilation. Falling back to eager execution for this "
"PCG capture stream is not set. This can be a Dynamo runtime "
"recompilation or an optional VLM branch pre-warmed outside "
"CUDA graph capture; falling back to eager execution for this "
"subgraph."
)
return entry.runnable(*args)
assert (
stream is not None
), "PCG capture stream is not set, please check if runtime recompilation happened"
if self.compile_config.get_enable_debug_mode():
input_addresses = [
x.data_ptr() for x in args if isinstance(x, torch.Tensor)
@@ -65,7 +65,7 @@ class XPUPiecewiseBackend(CUDAPiecewiseBackend):
# at 8192 tokens when the capture grid tops out at 512). The
# recompiled backend instance has no capture stream; fall back to
# eager for that sub-graph instead of crashing the scheduler.
# Mirrors the HIP fallback in CUDAPiecewiseBackend.__call__.
# Mirrors the CUDA fallback in CUDAPiecewiseBackend.__call__.
stream = get_pcg_capture_stream()
if stream is None:
print_warning_once(
+21 -5
View File
@@ -85,6 +85,7 @@ from sglang.srt.layers.linear import (
)
from sglang.srt.layers.quantization import QuantizationConfig
from sglang.srt.layers.rotary_embedding import apply_rotary_pos_emb
from sglang.srt.layers.rotary_embedding.utils import apply_rotary_pos_emb_native_eager
from sglang.srt.runtime_context import get_server_args
from sglang.srt.utils import add_prefix
@@ -203,11 +204,15 @@ def resolve_seqlens(
return resolved_seqlens
def resolve_max_seqlen(source, cu_seqlens: torch.Tensor) -> int:
"""Return max segment length, caching it on a stable carrier so the
device->host sync (.item()) happens once per forward instead of once per layer.
def resolve_max_seqlen(
source: torch.Tensor | SingletonCache | None, cu_seqlens: torch.Tensor
) -> int:
"""Return the maximum segment length, caching only on ``SingletonCache``.
Raw tensors have no mutable instance dictionary, so caching on them would
raise ``AttributeError``. They use the same calculation without a cache.
"""
if isinstance(source, SingletonCache) or isinstance(source, torch.Tensor):
if isinstance(source, SingletonCache):
cached = getattr(source, "_max_seqlen", None)
if cached is None:
seq_lens = cu_seqlens[1:] - cu_seqlens[:-1]
@@ -1331,7 +1336,18 @@ class VisionAttention(nn.Module):
cos = torch.cat([cos, cos], dim=-1)
sin = torch.cat([sin, sin], dim=-1)
q, k = apply_rotary_pos_emb(q, k, cos, sin)
# `apply_rotary_pos_emb` is torch.compile-decorated. Its first
# specialization may otherwise be compiled while a ViT CUDA graph
# is being captured, which makes Inductor attempt an illegal
# CPU-to-CUDA copy. The eager version is captured as part of the
# graph, so its pointwise work is still replayed without launch
# overhead.
rotary_fn = (
apply_rotary_pos_emb_native_eager
if envs.SGLANG_VIT_ENABLE_CUDA_GRAPH.get()
else apply_rotary_pos_emb
)
q, k = rotary_fn(q, k, cos, sin)
q = q.view(original_q_shape)
k = k.view(original_k_shape)
@@ -70,8 +70,7 @@ def rotate_half(x):
return torch.cat((-x2, x1), dim=-1)
@torch.compile(dynamic=True, backend=get_compiler_backend())
def apply_rotary_pos_emb_native(
def apply_rotary_pos_emb_native_eager(
q: torch.Tensor,
k: torch.Tensor,
cos: torch.Tensor,
@@ -94,6 +93,17 @@ def apply_rotary_pos_emb_native(
return q_embed, k_embed
@torch.compile(dynamic=True, backend=get_compiler_backend())
def apply_rotary_pos_emb_native(
q: torch.Tensor,
k: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
unsqueeze_dim=1,
) -> Tuple[torch.Tensor, torch.Tensor]:
return apply_rotary_pos_emb_native_eager(q, k, cos, sin, unsqueeze_dim)
def apply_rotary_pos_emb_npu(
q: torch.Tensor,
k: torch.Tensor,
@@ -544,6 +544,71 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
attn_backend.init_forward_metadata(fb)
self._run_forward(fb, num_tokens)
def run_dummy_multimodal_deepstack_forward(
self, language_model: torch.nn.Module, num_tokens: int
) -> bool:
"""Warm the tensor-valued deepstack branch before serving requests.
The regular PCG dummy is text-only. Qwen3-VL only provides
``input_deepstack_embeds`` after visual encoding, so leaving this
branch cold makes the first image request synchronously recompile the
language model. The model/signature checks keep this a no-op for
non-deepstack architectures.
"""
if (
"input_deepstack_embeds"
not in inspect.signature(language_model.forward).parameters
):
return False
num_deepstack = getattr(self.model_runner.model, "num_deepstack_embeddings", 0)
if num_deepstack <= 0:
return False
hidden_size = (
getattr(getattr(language_model, "config", None), "hidden_size", None)
or self.model_runner.model_config.hidden_size
)
fb, attn_backend = self.capture_prepare(num_tokens)
attn_backend.init_forward_metadata(fb)
deepstack_embeds = torch.zeros(
(num_tokens, hidden_size * num_deepstack),
dtype=self.model_runner.dtype,
device=self.device,
)
torch._dynamo.maybe_mark_dynamic(deepstack_embeds, 0)
fb.dp_local_start_pos = fb.dp_local_num_tokens = None
set_dp_buffer_len(
fb.global_dp_buffer_len,
num_tokens,
fb.dp_padding_mode.is_max_len(),
fb.global_num_tokens_cpu,
)
set_is_extend_in_batch(False)
with (
forward_context(
ForwardContext(attn_backend=self.model_runner.attn_backend)
),
set_tc_piecewise_forward_context(
fb,
self.attention_layers,
self.quant_config,
self.moe_layers,
self.moe_fusions,
dsa_indexers=self.dsa_indexers,
),
):
language_model.forward(
fb.input_ids,
self._get_layer_model_positions(fb),
fb,
input_embeds=fb.input_embeds,
input_deepstack_embeds=deepstack_embeds,
)
return True
def _has_inactive_dp_rank(self, forward_batch: ForwardBatch) -> bool:
# DSV4 DP attention / DeepEP collectives need every DP rank to enter
# the same replay path. Sparse-DP batches (one or more ranks with
@@ -200,6 +200,14 @@ class TcPiecewiseCudaGraphBackend(BaseCudaGraphBackend):
f"Compiling num tokens ({num_tokens=})"
)
cuda_graph_runner._run_dummy_forward(num_tokens=num_tokens)
# Qwen3-VL deepstack embeddings are produced only after
# visual encoding. First trace the tensor branch above, then
# execute it once outside the compile-warmup marker so its
# regular kernel/JIT warmup also happens during startup.
cuda_graph_runner.run_dummy_multimodal_deepstack_forward(
inner_model, cuda_graph_runner.capture_num_tokens[-1]
)
finally:
_toggle_multi_platform_ops(inner_model, reverse=True, num_tokens=16)