[diffusion] feat: support request-scoped skip-softmax attention (#37959)

This commit is contained in:
Mick
2026-09-05 13:50:17 +08:00
committed by GitHub
parent 4b44a1cde2
commit 0ea8378085
16 changed files with 689 additions and 31 deletions
@@ -59,6 +59,57 @@ QUALITY_LEVELS: tuple[str, ...] = ("lossless", "extra-high", "high")
KERNEL_FUSION_QUALITY_LEVELS = frozenset({"extra-high", "high"})
@dataclass(frozen=True)
class SkipSoftmaxParams:
"""Validated request-scoped BLASST/Skip-Softmax controls."""
threshold_scale_factor: float
start_step: int = 0
def resolve_skip_softmax_params(
params: dict[str, Any] | None,
) -> SkipSoftmaxParams | None:
if params is None:
return None
if not isinstance(params, dict):
raise ValueError(f"skip_softmax_params must be a dict, got {params!r}")
valid_keys = {"threshold_scale_factor", "start_step"}
unknown = sorted(set(params) - valid_keys)
if unknown:
raise ValueError(
f"Unknown skip_softmax_params keys: {unknown}. "
f"Valid keys: {sorted(valid_keys)}."
)
if "threshold_scale_factor" not in params:
raise ValueError("skip_softmax_params requires 'threshold_scale_factor'.")
threshold = params["threshold_scale_factor"]
if (
isinstance(threshold, bool)
or not isinstance(threshold, (int, float))
or not math.isfinite(float(threshold))
or float(threshold) <= 0
):
raise ValueError(
"skip_softmax_params.threshold_scale_factor must be a finite "
f"positive number, got {threshold!r}"
)
start_step = params.get("start_step", 0)
if (
isinstance(start_step, bool)
or not isinstance(start_step, int)
or start_step < 0
):
raise ValueError(
"skip_softmax_params.start_step must be a non-negative int, "
f"got {start_step!r}"
)
return SkipSoftmaxParams(float(threshold), start_step)
def quality_allows_kernel_fusions(quality: str) -> bool:
"""Return whether a quality level includes request-gated kernel fusions."""
return quality in KERNEL_FUSION_QUALITY_LEVELS
@@ -232,6 +283,11 @@ class SamplingParams:
# request; see DenoisingStage._maybe_override_attention_backend.
attention_backend_override: str | None = None
# Request-scoped BLASST/Skip-Softmax sparse attention. This is an explicit
# lossy opt-in; compatible self-attention layers are dispatched through
# FlashInfer while cross-attention remains on its normal backend.
skip_softmax_params: dict[str, Any] | None = None
# Spectrum parameters
enable_spectrum: bool = False
spectrum_params: Any = None # SpectrumParams
@@ -485,6 +541,8 @@ class SamplingParams:
f"quality must be one of {list(QUALITY_LEVELS)}, got {self.quality!r}"
)
resolve_skip_softmax_params(self.skip_softmax_params)
# These are always required to be sane regardless of pipeline.
if (
not isinstance(self.num_outputs_per_prompt, int)
@@ -309,6 +309,7 @@ async def generations(
attention_backend_override=_get_extra_field(
request, "attention_backend_override"
),
skip_softmax_params=_get_extra_field(request, "skip_softmax_params"),
quality=_runtime_sampling_quality(request.quality),
output_compression=request.output_compression,
output_quality=request.output_quality,
@@ -133,6 +133,7 @@ _MULTIPART_EXTRA_FORM_FIELDS = (
"cfg_gate_step",
"enable_cache_dit",
"quality",
"skip_softmax_params",
)
@@ -291,6 +292,7 @@ def _build_video_sampling_params(request_id: str, request: VideoGenerationsReque
"attention_backend_override": _extra_value(
request, "attention_backend_override"
),
"skip_softmax_params": _extra_value(request, "skip_softmax_params"),
"enable_frame_interpolation": request.enable_frame_interpolation,
"frame_interpolation_exp": request.frame_interpolation_exp,
"frame_interpolation_scale": request.frame_interpolation_scale,
@@ -23,6 +23,20 @@ class AttentionRequirements:
packed_varlen: bool = False
def trailing_padding_used_len(
total_tokens: int,
max_seqlen: int,
bounds: tuple[int, ...],
) -> int | None:
"""Return the live prefix length for a packed, padded single sequence."""
if len(bounds) != 3:
return None
start, used, total = bounds
if start != 0 or used >= total or total != total_tokens or used != max_seqlen:
return None
return used
class AttentionBackend(ABC):
"""Abstract class for attention backends."""
@@ -1,6 +1,7 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from numbers import Integral
from typing import Any, List, Optional, Tuple
import torch
@@ -11,8 +12,16 @@ from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend i
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
trailing_padding_used_len,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.skip_softmax import (
get_fixed_sequence_metadata,
get_host_sequence_lengths,
get_request_skip_softmax_params,
run_skip_softmax,
)
from sglang.multimodal_gen.runtime.layers.utils import register_custom_op
from sglang.multimodal_gen.runtime.managers.forward_context import get_forward_context
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
)
@@ -365,6 +374,7 @@ class FlashAttentionImpl(AttentionImpl):
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
is_cross_attention: bool = False,
**extra_impl_args,
) -> None:
self.num_heads = num_heads
@@ -372,8 +382,26 @@ class FlashAttentionImpl(AttentionImpl):
self.head_size = head_size
self.causal = causal
self.softmax_scale = softmax_scale
self.is_cross_attention = is_cross_attention
self.packed_trailing_padding = extra_impl_args.get(
"packed_trailing_padding", False
)
self.attention_metadata = FlashAttentionMetadata()
def _request_skip_softmax_threshold(self) -> tuple[bool, float | None]:
params = get_request_skip_softmax_params()
if params is None or self.is_cross_attention:
return False, None
current_step = get_forward_context().current_timestep
if not isinstance(current_step, Integral):
raise RuntimeError(
"Skip Softmax requires the denoising loop to publish an integer "
f"step index, got {current_step!r}."
)
if current_step < params.start_step:
return True, None
return True, params.threshold_scale_factor
def forward(
self,
query: torch.Tensor,
@@ -383,6 +411,38 @@ class FlashAttentionImpl(AttentionImpl):
*,
return_softmax_lse: bool = False,
):
use_trtllm, skip_threshold = self._request_skip_softmax_threshold()
if use_trtllm:
if return_softmax_lse:
raise NotImplementedError(
"Skip Softmax does not support the LSE output required by "
"Ring Attention."
)
batch_size, query_length = query.shape[:2]
kv_length = key.shape[1]
metadata = get_fixed_sequence_metadata(
query.device,
batch_size,
query_length,
kv_length,
)
output = run_skip_softmax(
query.reshape(-1, query.shape[-2], query.shape[-1]),
key.reshape(-1, key.shape[-2], key.shape[-1]),
value.reshape(-1, value.shape[-2], value.shape[-1]),
seq_lens=metadata.seq_lens,
cu_seqlens_q=metadata.cu_seqlens_q,
cu_seqlens_kv=metadata.cu_seqlens_kv,
max_seqlen_q=query_length,
max_seqlen_kv=kv_length,
softmax_scale=self.softmax_scale,
causal=self.causal,
threshold_scale_factor=skip_threshold,
q_seq_lens_cpu=metadata.q_seq_lens_cpu,
kv_seq_lens_cpu=metadata.kv_seq_lens_cpu,
)
return output.view(batch_size, query_length, *output.shape[1:])
if attn_metadata is not None:
if attn_metadata.max_seqlen_q is None:
attn_metadata.max_seqlen_q = query.shape[1]
@@ -457,7 +517,62 @@ class FlashAttentionImpl(AttentionImpl):
max_seqlen: int,
cu_seqlens_host: tuple[int, ...] | None = None,
) -> torch.Tensor:
del cu_seqlens_host
use_trtllm, skip_threshold = self._request_skip_softmax_threshold()
if use_trtllm:
bounds = cu_seqlens_host
used = (
trailing_padding_used_len(
total_tokens=query.shape[0],
max_seqlen=max_seqlen,
bounds=bounds,
)
if self.packed_trailing_padding and bounds is not None
else None
)
if used is not None:
live_cu_seqlens = cu_seqlens[:2]
live_output = run_skip_softmax(
query[:used],
key[:used],
value[:used],
seq_lens=live_cu_seqlens[1:] - live_cu_seqlens[:-1],
cu_seqlens_q=live_cu_seqlens,
cu_seqlens_kv=live_cu_seqlens,
max_seqlen_q=used,
max_seqlen_kv=used,
softmax_scale=self.softmax_scale,
causal=self.causal,
threshold_scale_factor=skip_threshold,
q_seq_lens_cpu=get_host_sequence_lengths(bounds[:2]),
kv_seq_lens_cpu=get_host_sequence_lengths(bounds[:2]),
)
output = live_output.new_zeros(
query.shape[0], query.shape[1], value.shape[2]
)
output[:used] = live_output
return output
seq_lens = cu_seqlens[1:] - cu_seqlens[:-1]
seq_lens_cpu = (
get_host_sequence_lengths(cu_seqlens_host)
if cu_seqlens_host is not None
else None
)
return run_skip_softmax(
query,
key,
value,
seq_lens=seq_lens,
cu_seqlens_q=cu_seqlens,
cu_seqlens_kv=cu_seqlens,
max_seqlen_q=max_seqlen,
max_seqlen_kv=max_seqlen,
softmax_scale=self.softmax_scale,
causal=self.causal,
threshold_scale_factor=skip_threshold,
q_seq_lens_cpu=seq_lens_cpu,
kv_seq_lens_cpu=seq_lens_cpu,
)
output = flash_attn_varlen_func(
query,
key,
@@ -10,6 +10,7 @@ from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend i
AttentionBackend,
AttentionImpl,
AttentionMetadata,
trailing_padding_used_len,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
@@ -17,21 +18,6 @@ from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
def _trailing_padding_used_len(
*,
total_tokens: int,
max_seqlen: int,
bounds: tuple[int, ...],
) -> int | None:
"""Return live token count for H3-style [0, used, total] trailing padding."""
if len(bounds) != 3:
return None
start, used, total = bounds
if start != 0 or used >= total or total != total_tokens or used != max_seqlen:
return None
return used
class SageAttentionBackend(AttentionBackend):
@classmethod
def supports_ring_rotation(cls) -> bool:
@@ -66,6 +52,9 @@ class SageAttentionImpl(AttentionImpl):
self.causal = causal
self.softmax_scale = softmax_scale
self.dropout = extra_impl_args.get("dropout_p", 0.0)
self.packed_trailing_padding = extra_impl_args.get(
"packed_trailing_padding", False
)
def forward(
self,
@@ -125,10 +114,14 @@ class SageAttentionImpl(AttentionImpl):
) -> torch.Tensor:
# MiniMax-H3 packs one live document as bounds=(0, used, total):
# [0, used) are real tokens; [used, total) is 64-aligned tail padding.
used = _trailing_padding_used_len(
total_tokens=query.shape[0],
max_seqlen=max_seqlen,
bounds=bounds,
used = (
trailing_padding_used_len(
total_tokens=query.shape[0],
max_seqlen=max_seqlen,
bounds=bounds,
)
if self.packed_trailing_padding
else None
)
if used is not None:
live_out = self.forward(
@@ -0,0 +1,214 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from functools import lru_cache
from typing import TYPE_CHECKING
import torch
from sglang.multimodal_gen.configs.sample.sampling_params import SkipSoftmaxParams
from sglang.multimodal_gen.runtime.managers.forward_context import (
get_forward_context_or_none,
)
if TYPE_CHECKING:
from sglang.multimodal_gen.runtime.pipelines_core import Req
_WORKSPACE_BYTES = 128 * 1024 * 1024
_REQUEST_KEY = "_skip_softmax_params"
_workspaces: dict[tuple[int, int], torch.Tensor] = {}
@dataclass(frozen=True)
class SkipSoftmaxSequenceMetadata:
seq_lens: torch.Tensor
cu_seqlens_q: torch.Tensor
cu_seqlens_kv: torch.Tensor
q_seq_lens_cpu: torch.Tensor
kv_seq_lens_cpu: torch.Tensor
def set_request_skip_softmax_params(
batch: "Req", params: SkipSoftmaxParams | None
) -> None:
batch.extra[_REQUEST_KEY] = params
def get_request_skip_softmax_params() -> SkipSoftmaxParams | None:
context = get_forward_context_or_none()
if context is None:
return None
batch = context.forward_batch
if batch is None:
return None
params = batch.extra.get(_REQUEST_KEY)
assert params is None or isinstance(params, SkipSoftmaxParams)
return params
def get_fixed_sequence_metadata(
device: torch.device,
batch_size: int,
query_length: int,
kv_length: int,
) -> SkipSoftmaxSequenceMetadata:
device_index = device.index
if device_index is None:
device_index = torch.cuda.current_device()
return _cached_fixed_sequence_metadata(
device_index, batch_size, query_length, kv_length
)
@lru_cache(maxsize=32)
def _cached_fixed_sequence_metadata(
device_index: int,
batch_size: int,
query_length: int,
kv_length: int,
) -> SkipSoftmaxSequenceMetadata:
device = torch.device("cuda", device_index)
seq_lens = torch.full((batch_size,), kv_length, dtype=torch.int32, device=device)
cu_seqlens_q = torch.arange(
0,
(batch_size + 1) * query_length,
query_length,
dtype=torch.int32,
device=device,
)
cu_seqlens_kv = torch.arange(
0,
(batch_size + 1) * kv_length,
kv_length,
dtype=torch.int32,
device=device,
)
return SkipSoftmaxSequenceMetadata(
seq_lens=seq_lens,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_kv=cu_seqlens_kv,
q_seq_lens_cpu=torch.full((batch_size,), query_length, dtype=torch.int32),
kv_seq_lens_cpu=torch.full((batch_size,), kv_length, dtype=torch.int32),
)
@lru_cache(maxsize=32)
def get_host_sequence_lengths(cu_seqlens: tuple[int, ...]) -> torch.Tensor:
return torch.tensor(
[stop - start for start, stop in zip(cu_seqlens[:-1], cu_seqlens[1:])],
dtype=torch.int32,
)
def run_skip_softmax(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
*,
seq_lens: torch.Tensor,
cu_seqlens_q: torch.Tensor,
cu_seqlens_kv: torch.Tensor,
max_seqlen_q: int,
max_seqlen_kv: int,
softmax_scale: float,
causal: bool,
threshold_scale_factor: float | None,
q_seq_lens_cpu: torch.Tensor | None = None,
kv_seq_lens_cpu: torch.Tensor | None = None,
) -> torch.Tensor:
"""Run FlashInfer TRTLLM attention, optionally with BLASST sparsity."""
_validate_inputs(query, key, value)
capability = torch.cuda.get_device_capability(query.device)
workspace = _get_workspace(query.device)
query, key, value = (tensor.contiguous() for tensor in (query, key, value))
common_kwargs = dict(
seq_lens=seq_lens,
max_q_len=max_seqlen_q,
max_kv_len=max_seqlen_kv,
bmm1_scale=softmax_scale,
bmm2_scale=1.0,
batch_size=cu_seqlens_q.numel() - 1,
cum_seq_lens_q=cu_seqlens_q,
cum_seq_lens_kv=cu_seqlens_kv,
skip_softmax_threshold_scale_factor=threshold_scale_factor,
)
if capability == (9, 0):
from flashinfer.prefill import trtllm_fmha_v2_prefill
return trtllm_fmha_v2_prefill(
(query, torch.stack((key, value), dim=1)),
"CONTIGUOUS_Q_KV",
workspace_buffer=workspace,
mask_mode="causal" if causal else "padding",
**common_kwargs,
)
if capability in ((10, 0), (10, 3), (10, 7)):
from flashinfer.prefill import trtllm_ragged_attention_deepseek
output = torch.empty(
(query.shape[0], query.shape[1], value.shape[2]),
dtype=query.dtype,
device=query.device,
)
return trtllm_ragged_attention_deepseek(
query,
key,
value,
workspace,
o_sf_scale=-1.0,
window_left=-1,
enable_pdl=None,
is_causal=causal,
return_lse=False,
out=output,
q_seq_lens_cpu=q_seq_lens_cpu,
kv_seq_lens_cpu=kv_seq_lens_cpu,
**common_kwargs,
)
raise ValueError(
"Skip Softmax requires Hopper SM90 or Blackwell SM100/SM103/SM107; "
f"found SM{capability[0]}{capability[1]}."
)
def _validate_inputs(
query: torch.Tensor, key: torch.Tensor, value: torch.Tensor
) -> None:
if query.device.type != "cuda":
raise ValueError("Skip Softmax requires a CUDA device.")
if key.device != query.device or value.device != query.device:
raise ValueError("Skip Softmax requires Q/K/V on the same CUDA device.")
if query.ndim != 3 or key.ndim != 3 or value.ndim != 3:
raise ValueError("Skip Softmax expects packed [tokens, heads, head_dim] Q/K/V.")
if query.dtype not in (torch.float16, torch.bfloat16):
raise ValueError(
f"Skip Softmax requires FP16 or BF16 Q/K/V, got {query.dtype}."
)
if query.dtype != key.dtype or query.dtype != value.dtype:
raise ValueError("Skip Softmax requires Q/K/V to have the same dtype.")
if query.shape[-1] != key.shape[-1] or key.shape[-1] != value.shape[-1]:
raise ValueError("Skip Softmax requires matching Q/K/V head dimensions.")
if key.shape[:2] != value.shape[:2]:
raise ValueError("Skip Softmax requires matching K/V token and head counts.")
if query.shape[1] % key.shape[1] != 0:
raise ValueError(
"Skip Softmax requires the query head count to divide by KV heads."
)
if query.shape[-1] not in (128, 256):
raise ValueError(
f"Skip Softmax supports head dimensions 128 and 256; got {query.shape[-1]}."
)
def _get_workspace(device: torch.device) -> torch.Tensor:
device_index = device.index
if device_index is None:
device_index = torch.cuda.current_device()
key = (device_index, torch.cuda.current_stream(device).cuda_stream)
workspace = _workspaces.get(key)
if workspace is None:
workspace = torch.empty(_WORKSPACE_BYTES, dtype=torch.uint8, device=device)
_workspaces[key] = workspace
return workspace
@@ -42,6 +42,9 @@ from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend i
AttentionImpl,
wrap_attention_impl_forward,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.skip_softmax import (
get_request_skip_softmax_params,
)
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend
from sglang.multimodal_gen.runtime.layers.attention.turbo_layer import (
async_a2a_communicate,
@@ -344,6 +347,18 @@ def apply_attention_backend_override(
layer.backend = target
def supports_skip_softmax(layer: nn.Module) -> bool:
return (
not layer.is_cross_attention
and layer.head_size in (128, 256)
and layer.dtype in (torch.float16, torch.bfloat16)
and (
layer._required_attention_backend is None
or layer.backend is AttentionBackendEnum.FA
)
)
class UlyssesAttention(nn.Module):
"""Ulysses-style SequenceParallelism attention layer."""
@@ -357,6 +372,7 @@ class UlyssesAttention(nn.Module):
supported_attention_backends: set[AttentionBackendEnum] | None = None,
required_attention_backend: AttentionBackendEnum | None = None,
prefix: str = "",
is_cross_attention: bool = False,
**extra_impl_args,
) -> None:
super().__init__()
@@ -392,6 +408,7 @@ class UlyssesAttention(nn.Module):
softmax_scale=self.softmax_scale,
num_kv_heads=num_kv_heads,
prefix=f"{prefix}.impl",
is_cross_attention=is_cross_attention,
**extra_impl_args,
)
self.attn_impl = impl_cls(**self._attn_impl_ctor_kwargs)
@@ -404,6 +421,7 @@ class UlyssesAttention(nn.Module):
self._attn_impl_by_backend = {self.backend: self.attn_impl}
self._supported_attention_backends = supported_attention_backends
self._required_attention_backend = required_attention_backend
self.is_cross_attention = is_cross_attention
self.dtype = dtype
self.causal = causal
self.sp_attention_mode, self.sp_attention_mode_is_auto = (
@@ -658,6 +676,7 @@ class LocalAttention(nn.Module):
softmax_scale=self.softmax_scale,
num_kv_heads=num_kv_heads,
causal=causal,
is_cross_attention=is_cross_attention,
**extra_impl_args,
)
self.attn_impl = impl_cls(**self._attn_impl_ctor_kwargs)
@@ -670,6 +689,7 @@ class LocalAttention(nn.Module):
self._attn_impl_by_backend = {self.backend: self.attn_impl}
self._supported_attention_backends = supported_attention_backends
self._required_attention_backend = required_attention_backend
self.is_cross_attention = is_cross_attention
self.dtype = dtype
def forward(
@@ -697,6 +717,13 @@ class LocalAttention(nn.Module):
ctx_attn_metadata = forward_context.attn_metadata
if attn_mask is not None:
if (
not self.is_cross_attention
and get_request_skip_softmax_params() is not None
):
raise NotImplementedError(
"Skip Softmax does not support LocalAttention masks."
)
q_ = q.transpose(1, 2)
k_ = k.transpose(1, 2)
v_ = v.transpose(1, 2)
@@ -821,6 +848,7 @@ class USPAttention(nn.Module):
softmax_scale=self.softmax_scale,
num_kv_heads=num_kv_heads,
prefix=f"{prefix}.impl",
is_cross_attention=is_cross_attention,
**extra_impl_args,
)
self.attn_impl = impl_cls(**self._attn_impl_ctor_kwargs)
@@ -833,6 +861,7 @@ class USPAttention(nn.Module):
self._attn_impl_by_backend = {self.backend: self.attn_impl}
self._supported_attention_backends = supported_attention_backends
self._required_attention_backend = required_attention_backend
self.is_cross_attention = is_cross_attention
self.dtype = dtype
self.causal = causal
self.dropout_p = dropout_rate
@@ -897,6 +926,14 @@ class USPAttention(nn.Module):
"""
forward_context: ForwardContext = get_forward_context()
ctx_attn_metadata = forward_context.attn_metadata
if (
attn_mask is not None
and get_request_skip_softmax_params() is not None
and not self.is_cross_attention
):
raise NotImplementedError(
"Skip Softmax does not support USPAttention masks."
)
effective_skip_sp = (
self.skip_sequence_parallel or skip_sequence_parallel_override
)
@@ -61,6 +61,10 @@ def get_forward_context() -> "ForwardContext":
return _forward_context
def get_forward_context_or_none() -> "ForwardContext | None":
return _forward_context
# TODO(will): finalize the interface
@contextmanager
def set_forward_context(
@@ -846,6 +846,7 @@ class MiniMaxH3Attention(nn.Module):
softmax_scale=self.softmax_scale,
num_kv_heads=self.num_heads,
prefix=self.prefix,
packed_trailing_padding=True,
)
# Ring only supports FA (see _minimax_h3_attention_core_impl); keep
# the resolved enum alongside the impl instance instead of a second
@@ -55,6 +55,7 @@ from sglang.multimodal_gen.configs.pipeline_configs.flux import (
from sglang.multimodal_gen.configs.pipeline_configs.zimage import ZImagePipelineConfig
from sglang.multimodal_gen.configs.sample.sampling_params import (
quality_allows_kernel_fusions,
resolve_skip_softmax_params,
)
from sglang.multimodal_gen.runtime.breakable_cuda_graph import (
prompt_padding as bcg_utils,
@@ -96,12 +97,16 @@ from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_classifier_free_guidance_world_size,
world_group_is_initialized,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.skip_softmax import (
set_request_skip_softmax_params,
)
from sglang.multimodal_gen.runtime.layers.attention.layer import (
LocalAttention,
UlyssesAttention,
USPAttention,
apply_attention_backend_override,
prepare_attention_backend_override,
supports_skip_softmax,
)
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend
from sglang.multimodal_gen.runtime.layers.attention.STA_configuration import (
@@ -383,6 +388,7 @@ class DenoisingStage(PipelineStage, RolloutDenoisingMixin):
self._attn_backend_default = self.attn_backend
self._attn_metadata_head_size = attn_head_size
self._attention_backend_active_override: AttentionBackendEnum | None = None
self._skip_softmax_forced_fa = False
# cfg
self.guidance = None
@@ -570,13 +576,37 @@ class DenoisingStage(PipelineStage, RolloutDenoisingMixin):
self, num_inference_steps: int | tuple[int, int], batch: Req
) -> None:
"""Apply request-dependent transformer acceleration in trace-safe order."""
self._maybe_override_attention_backend(batch)
skip_softmax_params = resolve_skip_softmax_params(
batch.sampling_params.skip_softmax_params
)
if skip_softmax_params is not None:
capability = current_platform.get_device_capability()
capability_tuple = (
(capability.major, capability.minor) if capability is not None else None
)
if capability_tuple not in ((9, 0), (10, 0), (10, 3), (10, 7)):
found = capability.as_version_str() if capability else "unknown"
raise ValueError(
"skip_softmax_params requires Hopper SM90 or Blackwell "
f"SM100/SM103/SM107; found {found}."
)
if (self.server_args.ring_degree or 1) > 1:
raise ValueError(
"skip_softmax_params does not support Ring Attention because "
"the ring merge requires dense per-hop softmax statistics."
)
set_request_skip_softmax_params(batch, skip_softmax_params)
self._maybe_override_attention_backend(
batch, force_fa_for_self_attention=skip_softmax_params is not None
)
self._maybe_toggle_quality_fusions(batch)
self._maybe_enable_cache_dit(num_inference_steps, batch)
for transformer in filter(None, [self.transformer, self.transformer_2]):
self._maybe_torch_compile(transformer)
def _maybe_override_attention_backend(self, batch: Req) -> None:
def _maybe_override_attention_backend(
self, batch: Req, *, force_fa_for_self_attention: bool = False
) -> None:
"""Two-phase per-request backend switch: prepare all layers (may
raise, mutates nothing), then flip all — a rejected request leaves the
transformers untouched. Safe at this batch boundary because the field
@@ -584,21 +614,59 @@ class DenoisingStage(PipelineStage, RolloutDenoisingMixin):
target = self._parse_attention_backend_override(
batch.sampling_params.attention_backend_override
)
if target == self._attention_backend_active_override:
if force_fa_for_self_attention and target not in (
None,
AttentionBackendEnum.FA,
):
raise ValueError(
"skip_softmax_params requires the FA attention backend; "
f"attention_backend_override={target.name.lower()!r} is incompatible."
)
if (
target == self._attention_backend_active_override
and force_fa_for_self_attention == self._skip_softmax_forced_fa
):
return
layers = self._request_switchable_attention_layers()
stage_backend = self._attn_backend_default
layer_targets: list[tuple[nn.Module, AttentionBackendEnum | None]]
if target is not None:
stage_backend = self._validate_attention_backend_override(target, layers)
for layer in layers:
prepare_attention_backend_override(layer, target)
for layer in layers:
apply_attention_backend_override(layer, target)
layer_targets = [(layer, target) for layer in layers]
elif force_fa_for_self_attention:
self_attention_layers = [
layer for layer in layers if supports_skip_softmax(layer)
]
if self_attention_layers:
stage_backend = self._validate_attention_backend_override(
AttentionBackendEnum.FA, self_attention_layers
)
elif layers or self.attn_backend.get_enum() is not AttentionBackendEnum.FA:
self._validate_attention_backend_override(
AttentionBackendEnum.FA, self_attention_layers
)
layer_targets = [
(
layer,
(AttentionBackendEnum.FA if supports_skip_softmax(layer) else None),
)
for layer in layers
]
else:
layer_targets = [(layer, None) for layer in layers]
for layer, layer_target in layer_targets:
if layer_target is not None:
prepare_attention_backend_override(layer, layer_target)
for layer, layer_target in layer_targets:
apply_attention_backend_override(layer, layer_target)
self.attn_backend = stage_backend
self._attention_backend_active_override = target
self._skip_softmax_forced_fa = force_fa_for_self_attention
logger.debug(
"Attention backend for this batch: %s (%d layers switched)",
"Attention backend for this batch: %s%s (%d layers considered)",
target.name.lower() if target else "server default",
"; FA for self-attention" if force_fa_for_self_attention else "",
len(layers),
)
@@ -6,7 +6,15 @@ import unittest
from types import SimpleNamespace
from unittest.mock import patch
import torch
from sglang.multimodal_gen.runtime.layers.attention import layer as layer_module
from sglang.multimodal_gen.runtime.layers.attention.backends import (
flash_attn as flash_attn_module,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.flash_attn import (
FlashAttentionImpl,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages import (
denoising as denoising_module,
)
@@ -30,7 +38,9 @@ def _fake_backend_cls(enum, *, ring_capable=True):
)
def _fake_layer(default=AttentionBackendEnum.FA) -> SimpleNamespace:
def _fake_layer(
default=AttentionBackendEnum.FA, *, is_cross_attention=False
) -> SimpleNamespace:
return SimpleNamespace(
backend=default,
_default_attn_backend=default,
@@ -39,11 +49,38 @@ def _fake_layer(default=AttentionBackendEnum.FA) -> SimpleNamespace:
_required_attention_backend=None,
_attn_impl_ctor_kwargs={"num_heads": 2},
attn_impl=f"{default.name.lower()}_impl",
head_size=64,
dtype="bf16",
head_size=128,
dtype=torch.bfloat16,
is_cross_attention=is_cross_attention,
)
class TestSkipSoftmaxDispatch(unittest.TestCase):
def test_dense_trtllm_precedes_skip_threshold(self):
impl = FlashAttentionImpl(
num_heads=2,
head_size=128,
causal=False,
softmax_scale=128**-0.5,
)
params = SimpleNamespace(start_step=14, threshold_scale_factor=500.0)
with patch.object(
flash_attn_module, "get_request_skip_softmax_params", return_value=params
):
with patch.object(
flash_attn_module,
"get_forward_context",
return_value=SimpleNamespace(current_timestep=13),
):
self.assertEqual(impl._request_skip_softmax_threshold(), (True, None))
with patch.object(
flash_attn_module,
"get_forward_context",
return_value=SimpleNamespace(current_timestep=14),
):
self.assertEqual(impl._request_skip_softmax_threshold(), (True, 500.0))
class TestMaybeOverrideAttentionBackend(unittest.TestCase):
def setUp(self):
self.default_backend_cls = _fake_backend_cls(AttentionBackendEnum.FA)
@@ -59,6 +96,7 @@ class TestMaybeOverrideAttentionBackend(unittest.TestCase):
self.stage._attn_backend_default = self.default_backend_cls
self.stage._attn_metadata_head_size = 64
self.stage._attention_backend_active_override = None
self.stage._skip_softmax_forced_fa = False
self.layers = [_fake_layer(), _fake_layer()]
self.prepare_calls = []
@@ -122,6 +160,38 @@ class TestMaybeOverrideAttentionBackend(unittest.TestCase):
self.assertIs(self.stage.attn_backend, self.default_backend_cls)
self.assertIsNone(self.stage._attention_backend_active_override)
def test_skip_softmax_forces_fa_only_for_self_attention(self):
self.layers[1] = _fake_layer(is_cross_attention=True)
self.stage._maybe_override_attention_backend(
_batch(None), force_fa_for_self_attention=True
)
self.assertEqual(
self.prepare_calls,
[(self.layers[0], AttentionBackendEnum.FA)],
)
self.assertEqual(
self.apply_calls,
[
(self.layers[0], AttentionBackendEnum.FA),
(self.layers[1], None),
],
)
def test_skip_softmax_rejects_non_fa_override(self):
with self.assertRaisesRegex(ValueError, "requires the FA attention backend"):
self.stage._maybe_override_attention_backend(
_batch("sage_attn"), force_fa_for_self_attention=True
)
def test_skip_softmax_uses_custom_model_fa_without_shared_layers(self):
self.layers.clear()
self.stage._maybe_override_attention_backend(
_batch(None), force_fa_for_self_attention=True
)
self.assertEqual(self.prepare_calls, [])
self.assertEqual(self.apply_calls, [])
self.assertIs(self.stage.attn_backend, self.default_backend_cls)
def test_unknown_backend_name_rejected(self):
with self.assertRaisesRegex(ValueError, "Unknown attention_backend_override"):
self.stage._maybe_override_attention_backend(_batch("bogus_attn"))
@@ -3,6 +3,9 @@
import unittest
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
trailing_padding_used_len,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.packed_sequence import (
minimax_h3_packed_sequence,
minimax_h3_packed_sequence_ref2va_blocks,
@@ -10,6 +13,11 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.m
class TestMiniMaxH3PackedSequence(unittest.TestCase):
def test_trailing_padding_layout(self):
self.assertEqual(trailing_padding_used_len(64, 61, (0, 61, 64)), 61)
self.assertIsNone(trailing_padding_used_len(64, 31, (0, 32, 64)))
self.assertIsNone(trailing_padding_used_len(64, 61, (0, 61)))
def test_t2va_structure(self):
built = minimax_h3_packed_sequence(
text_len=97,
@@ -28,8 +28,10 @@ from sglang.multimodal_gen.configs.sample.qwenimage import QwenImageSamplingPara
from sglang.multimodal_gen.configs.sample.sampling_params import (
QUALITY_LEVELS,
SamplingParams,
SkipSoftmaxParams,
_json_safe,
quality_allows_kernel_fusions,
resolve_skip_softmax_params,
)
from sglang.multimodal_gen.configs.sample.spectrum import SpectrumParams
from sglang.multimodal_gen.configs.sample.teacache import TeaCacheParams
@@ -69,6 +71,29 @@ class TestSamplingParamsValidate(unittest.TestCase):
with self.assertRaisesRegex(ValueError, r"quality must be one of"):
SamplingParams(quality=bad) # type: ignore[arg-type]
def test_skip_softmax_params(self):
params = {"threshold_scale_factor": 500, "start_step": 14}
self.assertEqual(
resolve_skip_softmax_params(params),
SkipSoftmaxParams(threshold_scale_factor=500.0, start_step=14),
)
self.assertEqual(
SamplingParams(skip_softmax_params=params).skip_softmax_params, params
)
def test_skip_softmax_params_reject_invalid_values(self):
invalid = (
{},
{"threshold_scale_factor": 0},
{"threshold_scale_factor": math.inf},
{"threshold_scale_factor": 1, "start_step": -1},
{"threshold_scale_factor": 1, "unknown": True},
)
for params in invalid:
with self.subTest(params=params):
with self.assertRaisesRegex(ValueError, "skip_softmax_params"):
SamplingParams(skip_softmax_params=params)
def test_seed_accepts_int_or_non_empty_int_list(self):
self.assertEqual(SamplingParams(seed=7).seed, 7)
self.assertEqual(SamplingParams(seed=[7, 8]).seed, [7, 8])