[diffusion] feat: pick the attention backend by measuring it (#38689)
Co-authored-by: Mick Qian <mickqian@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Mick Qian
Claude Opus 5
parent
dc3171c322
commit
7b89b95168
@@ -0,0 +1,179 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Choose a layer's attention backend by timing the candidates on its own tensors.
|
||||||
|
|
||||||
|
Which backend is fastest is not a property of the GPU alone. On sm12x no FA
|
||||||
|
kernel exists and cuDNN beats torch's flash path at head_dim 128 but loses to it
|
||||||
|
at head_dim 64 and long sequences; on Hopper the FA backend beats cuDNN. Nor do
|
||||||
|
synthetic timings settle it: the tensors here are non-contiguous views into a
|
||||||
|
packed QKV buffer and backends differ in how they take that, so the measurement
|
||||||
|
uses what the layer was actually handed, on its first forward large enough to be
|
||||||
|
worth deciding on.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
|
||||||
|
AttentionImpl,
|
||||||
|
wrap_attention_impl_forward,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
||||||
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
# Backends only separate on the calls that carry the runtime; tuning on a short
|
||||||
|
# text or audio stream picks the wrong winner for the long video one.
|
||||||
|
_MIN_TUNE_NUMEL = 4 << 20
|
||||||
|
# A candidate has to beat the incumbent by more than the spread of these timings.
|
||||||
|
_MIN_RELATIVE_GAIN = 0.02
|
||||||
|
_WARMUP_ITERS = 3
|
||||||
|
_TIMED_ITERS = 8
|
||||||
|
# A backend that disagrees this much is not computing the same attention,
|
||||||
|
# whatever its timing says.
|
||||||
|
_MAX_OUTPUT_DEVIATION = 0.05
|
||||||
|
|
||||||
|
_reported = False
|
||||||
|
|
||||||
|
|
||||||
|
def _timed(impl: AttentionImpl, args, kwargs) -> float:
|
||||||
|
for _ in range(_WARMUP_ITERS):
|
||||||
|
impl.forward(*args, **kwargs)
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
start, end = torch.cuda.Event(True), torch.cuda.Event(True)
|
||||||
|
start.record()
|
||||||
|
for _ in range(_TIMED_ITERS):
|
||||||
|
impl.forward(*args, **kwargs)
|
||||||
|
end.record()
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
return start.elapsed_time(end) / _TIMED_ITERS
|
||||||
|
|
||||||
|
|
||||||
|
def _leading_tensor(out):
|
||||||
|
return out[0] if isinstance(out, (tuple, list)) else out
|
||||||
|
|
||||||
|
|
||||||
|
def _agrees(candidate_out, reference_out) -> bool:
|
||||||
|
got, want = _leading_tensor(candidate_out), _leading_tensor(reference_out)
|
||||||
|
if not (isinstance(got, torch.Tensor) and isinstance(want, torch.Tensor)):
|
||||||
|
return False
|
||||||
|
if got.shape != want.shape:
|
||||||
|
return False
|
||||||
|
scale = want.float().abs().max().clamp_min(1e-3)
|
||||||
|
return bool(
|
||||||
|
((got.float() - want.float()).abs().max() / scale) <= _MAX_OUTPUT_DEVIATION
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _candidates(layer) -> list[tuple[str, AttentionImpl, AttentionBackendEnum]]:
|
||||||
|
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend
|
||||||
|
|
||||||
|
ctor_kwargs = layer._attn_impl_ctor_kwargs
|
||||||
|
built: list[tuple[str, AttentionImpl, AttentionBackendEnum]] = []
|
||||||
|
|
||||||
|
# Whether SDPA is allowed to reach for cuDNN is a backend choice of its own,
|
||||||
|
# and on sm12x it is the only one there is.
|
||||||
|
if layer.backend is AttentionBackendEnum.TORCH_SDPA:
|
||||||
|
flipped = not ctor_kwargs.get("allow_cudnn_sdp", False)
|
||||||
|
built.append(
|
||||||
|
(
|
||||||
|
f"torch_sdpa(cudnn={flipped})",
|
||||||
|
type(layer.attn_impl)(**{**ctor_kwargs, "allow_cudnn_sdp": flipped}),
|
||||||
|
layer.backend,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for target in sorted(
|
||||||
|
layer._supported_attention_backends or (), key=lambda backend: backend.name
|
||||||
|
):
|
||||||
|
if target is layer.backend:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
backend_cls = get_attn_backend(
|
||||||
|
layer.head_size,
|
||||||
|
layer.dtype,
|
||||||
|
supported_attention_backends=layer._supported_attention_backends,
|
||||||
|
selected_attention_backend=target,
|
||||||
|
)
|
||||||
|
if backend_cls.get_enum() is not target:
|
||||||
|
continue
|
||||||
|
built.append(
|
||||||
|
(target.name.lower(), backend_cls.get_impl_cls()(**ctor_kwargs), target)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("attention autotune: %s unavailable (%s)", target, exc)
|
||||||
|
return built
|
||||||
|
|
||||||
|
|
||||||
|
def _choose(layer, args, kwargs) -> tuple[AttentionImpl, AttentionBackendEnum] | None:
|
||||||
|
"""The fastest candidate that agrees with the incumbent, or None to keep it."""
|
||||||
|
global _reported
|
||||||
|
|
||||||
|
incumbent = layer.attn_impl
|
||||||
|
reference = incumbent.forward(*args, **kwargs)
|
||||||
|
incumbent_label = f"{layer.backend.name.lower()} (current)"
|
||||||
|
timings: dict[str, tuple[float, AttentionImpl | None, AttentionBackendEnum]] = {
|
||||||
|
incumbent_label: (_timed(incumbent, args, kwargs), None, layer.backend)
|
||||||
|
}
|
||||||
|
|
||||||
|
for label, candidate, enum in _candidates(layer):
|
||||||
|
try:
|
||||||
|
output = candidate.forward(*args, **kwargs)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("attention autotune: %s failed (%s)", label, exc)
|
||||||
|
continue
|
||||||
|
if not _agrees(output, reference):
|
||||||
|
logger.debug(
|
||||||
|
"attention autotune: %s disagrees with %s", label, incumbent_label
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
timings[label] = (_timed(candidate, args, kwargs), candidate, enum)
|
||||||
|
|
||||||
|
incumbent_ms = timings[incumbent_label][0]
|
||||||
|
best_label = min(timings, key=lambda label: timings[label][0])
|
||||||
|
best_ms = timings[best_label][0]
|
||||||
|
|
||||||
|
say = logger.debug if _reported else logger.info
|
||||||
|
_reported = True
|
||||||
|
report = ", ".join(f"{label} {ms:.3f}ms" for label, (ms, *_) in timings.items())
|
||||||
|
if best_label == incumbent_label or best_ms > incumbent_ms * (
|
||||||
|
1 - _MIN_RELATIVE_GAIN
|
||||||
|
):
|
||||||
|
say("attention autotune: keeping %s (%s)", incumbent_label, report)
|
||||||
|
return None
|
||||||
|
say(
|
||||||
|
"attention autotune: %s -> %s, %.1f%% faster (%s)",
|
||||||
|
incumbent_label,
|
||||||
|
best_label,
|
||||||
|
100 * (1 - best_ms / incumbent_ms),
|
||||||
|
report,
|
||||||
|
)
|
||||||
|
return timings[best_label][1], timings[best_label][2]
|
||||||
|
|
||||||
|
|
||||||
|
def install(layer) -> None:
|
||||||
|
"""Tune this layer on its first forward worth measuring, then step aside."""
|
||||||
|
impl = layer.attn_impl
|
||||||
|
default_forward = impl.forward
|
||||||
|
|
||||||
|
def tuning_forward(*args, **kwargs):
|
||||||
|
query = args[0] if args else kwargs.get("query")
|
||||||
|
if not isinstance(query, torch.Tensor) or query.numel() < _MIN_TUNE_NUMEL:
|
||||||
|
return default_forward(*args, **kwargs)
|
||||||
|
impl.forward = default_forward
|
||||||
|
try:
|
||||||
|
winner = _choose(layer, args, kwargs)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning_once(
|
||||||
|
f"attention autotune failed, keeping the default: {exc}"
|
||||||
|
)
|
||||||
|
return default_forward(*args, **kwargs)
|
||||||
|
if winner is None:
|
||||||
|
return default_forward(*args, **kwargs)
|
||||||
|
impl_choice, backend_choice = winner
|
||||||
|
layer.attn_impl = wrap_attention_impl_forward(impl_choice)
|
||||||
|
layer.backend = backend_choice
|
||||||
|
return layer.attn_impl.forward(*args, **kwargs)
|
||||||
|
|
||||||
|
impl.forward = tuning_forward
|
||||||
@@ -65,7 +65,7 @@ class SDPAImpl(AttentionImpl):
|
|||||||
|
|
||||||
def _sdpa_context(self, query: torch.Tensor):
|
def _sdpa_context(self, query: torch.Tensor):
|
||||||
if self.allow_cudnn_sdp and query.device.type == "cuda":
|
if self.allow_cudnn_sdp and query.device.type == "cuda":
|
||||||
return sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
return sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS, set_priority=True)
|
||||||
return nullcontext()
|
return nullcontext()
|
||||||
|
|
||||||
def forward(
|
def forward(
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ class UlyssesAttention(nn.Module):
|
|||||||
)
|
)
|
||||||
self.attn_impl = impl_cls(**self._attn_impl_ctor_kwargs)
|
self.attn_impl = impl_cls(**self._attn_impl_ctor_kwargs)
|
||||||
wrap_attention_impl_forward(self.attn_impl)
|
wrap_attention_impl_forward(self.attn_impl)
|
||||||
|
_maybe_install_backend_autotune(self, attn_backend.get_enum())
|
||||||
self.num_heads = num_heads
|
self.num_heads = num_heads
|
||||||
self.head_size = head_size
|
self.head_size = head_size
|
||||||
self.num_kv_heads = num_kv_heads
|
self.num_kv_heads = num_kv_heads
|
||||||
@@ -681,6 +682,7 @@ class LocalAttention(nn.Module):
|
|||||||
)
|
)
|
||||||
self.attn_impl = impl_cls(**self._attn_impl_ctor_kwargs)
|
self.attn_impl = impl_cls(**self._attn_impl_ctor_kwargs)
|
||||||
wrap_attention_impl_forward(self.attn_impl)
|
wrap_attention_impl_forward(self.attn_impl)
|
||||||
|
_maybe_install_backend_autotune(self, attn_backend.get_enum())
|
||||||
self.num_heads = num_heads
|
self.num_heads = num_heads
|
||||||
self.head_size = head_size
|
self.head_size = head_size
|
||||||
self.num_kv_heads = num_kv_heads
|
self.num_kv_heads = num_kv_heads
|
||||||
@@ -748,7 +750,7 @@ class LocalAttention(nn.Module):
|
|||||||
v_ = v_.repeat_interleave(repeat_factor, dim=1)
|
v_ = v_.repeat_interleave(repeat_factor, dim=1)
|
||||||
|
|
||||||
sdpa_context = (
|
sdpa_context = (
|
||||||
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS, set_priority=True)
|
||||||
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
||||||
else nullcontext()
|
else nullcontext()
|
||||||
)
|
)
|
||||||
@@ -853,6 +855,7 @@ class USPAttention(nn.Module):
|
|||||||
)
|
)
|
||||||
self.attn_impl = impl_cls(**self._attn_impl_ctor_kwargs)
|
self.attn_impl = impl_cls(**self._attn_impl_ctor_kwargs)
|
||||||
wrap_attention_impl_forward(self.attn_impl)
|
wrap_attention_impl_forward(self.attn_impl)
|
||||||
|
_maybe_install_backend_autotune(self, attn_backend.get_enum())
|
||||||
self.num_heads = num_heads
|
self.num_heads = num_heads
|
||||||
self.head_size = head_size
|
self.head_size = head_size
|
||||||
self.num_kv_heads = num_kv_heads
|
self.num_kv_heads = num_kv_heads
|
||||||
@@ -1185,7 +1188,7 @@ class USPAttention(nn.Module):
|
|||||||
v_ = v.transpose(1, 2)
|
v_ = v.transpose(1, 2)
|
||||||
mask = _prepare_sdpa_mask(attn_mask, dtype=q_.dtype, device=q_.device)
|
mask = _prepare_sdpa_mask(attn_mask, dtype=q_.dtype, device=q_.device)
|
||||||
sdpa_context = (
|
sdpa_context = (
|
||||||
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS, set_priority=True)
|
||||||
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
||||||
else nullcontext()
|
else nullcontext()
|
||||||
)
|
)
|
||||||
@@ -1357,7 +1360,7 @@ class USPAttention(nn.Module):
|
|||||||
v_ = v.transpose(1, 2)
|
v_ = v.transpose(1, 2)
|
||||||
mask = _prepare_sdpa_mask(gathered_mask, dtype=q_.dtype, device=q_.device)
|
mask = _prepare_sdpa_mask(gathered_mask, dtype=q_.dtype, device=q_.device)
|
||||||
sdpa_context = (
|
sdpa_context = (
|
||||||
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS, set_priority=True)
|
||||||
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
||||||
else nullcontext()
|
else nullcontext()
|
||||||
)
|
)
|
||||||
@@ -1644,7 +1647,7 @@ class USPAttention(nn.Module):
|
|||||||
v_ = v_.repeat_interleave(repeat_factor, dim=1)
|
v_ = v_.repeat_interleave(repeat_factor, dim=1)
|
||||||
|
|
||||||
sdpa_context = (
|
sdpa_context = (
|
||||||
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS, set_priority=True)
|
||||||
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
||||||
else nullcontext()
|
else nullcontext()
|
||||||
)
|
)
|
||||||
@@ -1863,7 +1866,7 @@ class USPAttention(nn.Module):
|
|||||||
v_ = v.transpose(1, 2)
|
v_ = v.transpose(1, 2)
|
||||||
mask = _prepare_sdpa_mask(attn_mask, dtype=q_.dtype, device=q_.device)
|
mask = _prepare_sdpa_mask(attn_mask, dtype=q_.dtype, device=q_.device)
|
||||||
sdpa_context = (
|
sdpa_context = (
|
||||||
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS, set_priority=True)
|
||||||
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
||||||
else nullcontext()
|
else nullcontext()
|
||||||
)
|
)
|
||||||
@@ -2097,3 +2100,21 @@ for _attn_cls in (
|
|||||||
):
|
):
|
||||||
_attn_cls.forward = _make_breakable_attention_forward(_attn_cls.forward)
|
_attn_cls.forward = _make_breakable_attention_forward(_attn_cls.forward)
|
||||||
del _attn_cls
|
del _attn_cls
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_install_backend_autotune(layer, backend) -> None:
|
||||||
|
"""Opt-in: let the layer pick its backend by measurement on its first big call."""
|
||||||
|
from sglang.multimodal_gen.runtime.server_args import get_global_server_args
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not get_global_server_args().enable_attention_backend_autotune:
|
||||||
|
return
|
||||||
|
except Exception: # no ServerArgs yet (unit tests, tooling)
|
||||||
|
return
|
||||||
|
if getattr(layer, "_required_attention_backend", None) is not None:
|
||||||
|
return
|
||||||
|
from sglang.multimodal_gen.runtime.layers.attention.autotune import install
|
||||||
|
|
||||||
|
layer.backend = backend
|
||||||
|
layer._default_attn_backend = backend
|
||||||
|
install(layer)
|
||||||
|
|||||||
@@ -275,6 +275,10 @@ class ServerArgs(DisaggServerArgsMixin):
|
|||||||
|
|
||||||
# Attention
|
# Attention
|
||||||
attention_backend: str = None
|
attention_backend: str = None
|
||||||
|
# Time the viable attention backends on the model's own tensors during
|
||||||
|
# warmup and keep the fastest per layer. Off by default: it has only been
|
||||||
|
# measured on sm90 and sm12x.
|
||||||
|
enable_attention_backend_autotune: bool = False
|
||||||
attention_backend_config: addict.Dict | None = None
|
attention_backend_config: addict.Dict | None = None
|
||||||
component_attention_backends: dict[str, str] | str | None = field(
|
component_attention_backends: dict[str, str] | str | None = field(
|
||||||
default_factory=dict
|
default_factory=dict
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""The rules the autotuner has to obey: only switch on a clear, correct win.
|
||||||
|
|
||||||
|
Timing is stubbed here so the rules are what is under test, not the GPU.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.layers.attention import autotune
|
||||||
|
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
||||||
|
|
||||||
|
QUERY = torch.zeros(4, 4)
|
||||||
|
REFERENCE = torch.ones(2, 2)
|
||||||
|
|
||||||
|
|
||||||
|
class _Impl:
|
||||||
|
def __init__(self, output=REFERENCE):
|
||||||
|
self._output = output
|
||||||
|
self.calls = 0
|
||||||
|
|
||||||
|
def forward(self, *args, **kwargs):
|
||||||
|
self.calls += 1
|
||||||
|
return self._output
|
||||||
|
|
||||||
|
|
||||||
|
def _layer(incumbent):
|
||||||
|
return SimpleNamespace(
|
||||||
|
attn_impl=incumbent,
|
||||||
|
backend=AttentionBackendEnum.TORCH_SDPA,
|
||||||
|
head_size=128,
|
||||||
|
dtype=torch.bfloat16,
|
||||||
|
_attn_impl_ctor_kwargs={},
|
||||||
|
_supported_attention_backends=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def stub(monkeypatch):
|
||||||
|
"""Drive _choose with fixed candidates and fixed timings."""
|
||||||
|
|
||||||
|
def install(candidates: list[tuple[str, _Impl, object]], timings: dict[int, float]):
|
||||||
|
monkeypatch.setattr(autotune, "_candidates", lambda layer: candidates)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
autotune, "_timed", lambda impl, args, kwargs: timings[id(impl)]
|
||||||
|
)
|
||||||
|
|
||||||
|
return install
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_the_incumbent_without_a_clear_win(stub):
|
||||||
|
incumbent, rival = _Impl(), _Impl()
|
||||||
|
stub(
|
||||||
|
[("rival", rival, AttentionBackendEnum.FA)],
|
||||||
|
{id(incumbent): 10.0, id(rival): 9.9}, # 1%, under the margin
|
||||||
|
)
|
||||||
|
assert autotune._choose(_layer(incumbent), (QUERY,), {}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_switches_when_a_candidate_wins_by_more_than_the_margin(stub):
|
||||||
|
incumbent, rival = _Impl(), _Impl()
|
||||||
|
stub(
|
||||||
|
[("rival", rival, AttentionBackendEnum.FA)],
|
||||||
|
{id(incumbent): 10.0, id(rival): 8.0},
|
||||||
|
)
|
||||||
|
chosen = autotune._choose(_layer(incumbent), (QUERY,), {})
|
||||||
|
assert chosen is not None
|
||||||
|
assert chosen[0] is rival
|
||||||
|
assert chosen[1] is AttentionBackendEnum.FA
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_faster_candidate_that_disagrees_is_rejected(stub):
|
||||||
|
incumbent = _Impl()
|
||||||
|
wrong = _Impl(output=REFERENCE * 5)
|
||||||
|
stub(
|
||||||
|
[("wrong", wrong, AttentionBackendEnum.FA)],
|
||||||
|
{id(incumbent): 10.0, id(wrong): 1.0},
|
||||||
|
)
|
||||||
|
assert autotune._choose(_layer(incumbent), (QUERY,), {}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_candidate_that_raises_is_skipped(stub, monkeypatch):
|
||||||
|
incumbent = _Impl()
|
||||||
|
broken = _Impl()
|
||||||
|
|
||||||
|
def explode(*args, **kwargs):
|
||||||
|
raise RuntimeError("unsupported here")
|
||||||
|
|
||||||
|
broken.forward = explode
|
||||||
|
stub([("broken", broken, AttentionBackendEnum.FA)], {id(incumbent): 10.0})
|
||||||
|
assert autotune._choose(_layer(incumbent), (QUERY,), {}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_small_calls_stay_on_the_default_and_leave_the_tuner_armed(monkeypatch):
|
||||||
|
incumbent = _Impl()
|
||||||
|
layer = _layer(incumbent)
|
||||||
|
called = []
|
||||||
|
monkeypatch.setattr(autotune, "_choose", lambda *a, **k: called.append(1))
|
||||||
|
|
||||||
|
autotune.install(layer)
|
||||||
|
small = torch.zeros(8, 8)
|
||||||
|
assert small.numel() < autotune._MIN_TUNE_NUMEL
|
||||||
|
incumbent.forward(small)
|
||||||
|
|
||||||
|
assert called == [], "tuning must wait for a call worth measuring"
|
||||||
|
assert layer.attn_impl is incumbent
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""``allow_cudnn_sdp`` has to override torch's own backend choice.
|
||||||
|
|
||||||
|
The backend list is written cuDNN-first, but ``sdpa_kernel`` treats it as an
|
||||||
|
allow-set unless ``set_priority`` is passed -- and it is the same set torch
|
||||||
|
already chooses from, so without that flag the context is inert. Only the kernel
|
||||||
|
that actually ran distinguishes the two, so that is what these assert on.
|
||||||
|
|
||||||
|
Where the allow-set alone already lands on cuDNN (Hopper, for one) the flag has
|
||||||
|
nothing left to do and the check skips rather than asserting something it cannot
|
||||||
|
observe.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import nullcontext
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from torch.nn.attention import SDPBackend, sdpa_kernel
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.layers.attention.backends.sdpa import (
|
||||||
|
_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS,
|
||||||
|
SDPAImpl,
|
||||||
|
)
|
||||||
|
|
||||||
|
NUM_HEADS, HEAD_DIM, SEQ_LEN = 8, 128, 512
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not torch.cuda.is_available(), reason="attention dispatch needs CUDA"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _qkv():
|
||||||
|
shape = (1, NUM_HEADS, SEQ_LEN, HEAD_DIM)
|
||||||
|
return tuple(
|
||||||
|
torch.randn(shape, device="cuda", dtype=torch.bfloat16) for _ in range(3)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _kernel_under(context, q, k, v) -> str:
|
||||||
|
"""Which family of attention kernel ran inside ``context``."""
|
||||||
|
from torch.profiler import ProfilerActivity, profile
|
||||||
|
|
||||||
|
with profile(activities=[ProfilerActivity.CUDA]) as prof:
|
||||||
|
with context:
|
||||||
|
F.scaled_dot_product_attention(q, k, v)
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
for event in prof.events():
|
||||||
|
if event.device_type.name != "CUDA" or not event.self_device_time_total:
|
||||||
|
continue
|
||||||
|
name = event.key.lower()
|
||||||
|
# cuDNN names its own kernels `cudnn_generated_..._flash_...`, so cuDNN
|
||||||
|
# has to be checked first.
|
||||||
|
if "cudnn" in name:
|
||||||
|
return "cudnn"
|
||||||
|
if "flash" in name or "fmha" in name:
|
||||||
|
return "flash"
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _impl(allow_cudnn_sdp: bool) -> SDPAImpl:
|
||||||
|
return SDPAImpl(
|
||||||
|
num_heads=NUM_HEADS,
|
||||||
|
head_size=HEAD_DIM,
|
||||||
|
causal=False,
|
||||||
|
softmax_scale=HEAD_DIM**-0.5,
|
||||||
|
allow_cudnn_sdp=allow_cudnn_sdp,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_allow_cudnn_sdp_beats_the_backend_torch_would_pick():
|
||||||
|
q, k, v = _qkv()
|
||||||
|
try:
|
||||||
|
with sdpa_kernel(SDPBackend.CUDNN_ATTENTION):
|
||||||
|
F.scaled_dot_product_attention(q, k, v)
|
||||||
|
except RuntimeError:
|
||||||
|
pytest.skip("no cuDNN attention kernel for this shape on this GPU")
|
||||||
|
|
||||||
|
# The allow-set on its own is what this code did before priority was passed.
|
||||||
|
without_priority = sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
||||||
|
if _kernel_under(without_priority, q, k, v) == "cudnn":
|
||||||
|
pytest.skip("the allow-set alone already lands on cuDNN on this GPU")
|
||||||
|
|
||||||
|
assert _kernel_under(_impl(True)._sdpa_context(q), q, k, v) == "cudnn"
|
||||||
|
|
||||||
|
|
||||||
|
def test_opting_out_leaves_backend_selection_alone():
|
||||||
|
q, _, _ = _qkv()
|
||||||
|
assert isinstance(_impl(False)._sdpa_context(q), type(nullcontext()))
|
||||||
|
|
||||||
|
|
||||||
|
def test_cpu_tensors_do_not_get_a_cuda_context():
|
||||||
|
cpu_q = torch.randn(1, NUM_HEADS, 16, HEAD_DIM)
|
||||||
|
assert isinstance(_impl(True)._sdpa_context(cpu_q), type(nullcontext()))
|
||||||
Reference in New Issue
Block a user