fix: fix vlm cuda graph shape stability (#30868)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user