[Diffusion] Optimize Qwen-Image-Edit attention on Hopper (#38584)

Co-authored-by: Mick Qian <mickqian@users.noreply.github.com>
This commit is contained in:
Xiaoyu Zhang
2026-09-13 09:15:01 +08:00
committed by GitHub
co-authored by Mick Qian
parent fa663e7297
commit 3e035a3513
9 changed files with 362 additions and 48 deletions
@@ -66,6 +66,7 @@ from sglang.kernels.ops.diffusion.common.platform import is_cuda
from sglang.multimodal_gen.configs.models.vaes.stablediffusion3 import (
StableDiffusion3VAEConfig,
)
from sglang.multimodal_gen.runtime.layers.attention.backends import sdpa as sdpa_backend
from sglang.multimodal_gen.runtime.layers.layernorm import (
RMSNorm,
RMSNormNoWeight,
@@ -157,6 +158,92 @@ def _seed_cuda():
torch.cuda.manual_seed(0)
@pytest.mark.skipif(
not is_cuda()
or torch.cuda.get_device_capability()[0] != 9
or sdpa_backend.torch_varlen_attn is None,
reason="packed Flash SDPA requires Hopper and PyTorch varlen attention",
)
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize("causal", [False, True])
@pytest.mark.parametrize(
"lengths,head_dim",
[
(([64] * 7 + [32]) * 11 + [16] * 7 + [8], 80),
([0, 64, 64, 0, 32, 0], 80),
([32] * 8, 64),
([128, 64, 128], 128),
([2048], 80),
],
)
def test_packed_sdpa_uses_native_varlen_without_changing_values(
dtype, causal, lengths, head_dim
):
bounds = [0]
for length in lengths:
bounds.append(bounds[-1] + length)
packed = torch.randn(bounds[-1], 3, 16, head_dim, device="cuda", dtype=dtype)
q, k, v = packed.unbind(1)
q, k = q.contiguous(), k.contiguous()
attention = sdpa_backend.SDPAImpl(
16, head_dim, causal=causal, softmax_scale=head_dim**-0.5
)
cu = torch.tensor(bounds, device="cuda", dtype=torch.int32)
with (
torch.no_grad(),
torch.nn.attention.sdpa_kernel(torch.nn.attention.SDPBackend.FLASH_ATTENTION),
):
expected = torch.cat(
[
attention.forward(q[a:b][None], k[a:b][None], v[a:b][None], None)[0]
for a, b in zip(bounds[:-1], bounds[1:])
if a != b
]
)
with patch.object(attention, "forward", wraps=attention.forward) as forward:
actual = attention.forward_varlen(
q,
k,
v,
cu_seqlens=cu,
max_seqlen=max(lengths),
cu_seqlens_host=tuple(bounds),
)
assert forward.call_count == (1 if len(lengths) == 1 else 0)
assert torch.equal(actual, expected)
@pytest.mark.skipif(not is_cuda(), reason="CUDA packed SDPA fallback test")
def test_packed_sdpa_preserves_training_dropout_and_missing_api_fallbacks():
attention = sdpa_backend.SDPAImpl(4, 64, causal=True, softmax_scale=64**-0.5)
qkv = torch.randn(
64, 3, 4, 64, device="cuda", dtype=torch.bfloat16, requires_grad=True
)
q, k, v = qkv.unbind(1)
cu = torch.tensor([0, 32, 64], device="cuda", dtype=torch.int32)
kwargs = dict(cu_seqlens=cu, max_seqlen=32, cu_seqlens_host=(0, 32, 64))
with patch.object(attention, "forward", wraps=attention.forward) as forward:
result = attention.forward_varlen(q, k, v, **kwargs)
assert forward.call_count == 2
result.float().square().mean().backward()
assert qkv.grad is not None and torch.isfinite(qkv.grad).all()
attention.dropout = 0.1
with (
torch.no_grad(),
patch.object(attention, "forward", wraps=attention.forward) as forward,
):
attention.forward_varlen(q, k, v, **kwargs)
assert forward.call_count == 2
attention.dropout = 0.0
with (
torch.no_grad(),
patch.object(sdpa_backend, "torch_varlen_attn", None),
patch.object(attention, "forward", wraps=attention.forward) as forward,
):
attention.forward_varlen(q, k, v, **kwargs)
assert forward.call_count == 2
def test_bitexact_norm_guards_follow_platform():
# Runs on both lanes, with shapes inside every guard's contract so only the
# platform decides: engaged on CUDA, rejected on ROCm. A fatal LLVM error
@@ -4,6 +4,7 @@ from unittest.mock import patch
import pytest
import torch
import sglang.multimodal_gen.runtime.models.dits.qwen_image as qwen_image
from sglang.kernels.ops.diffusion import try_fused_qwen_qkv_epilogue
from sglang.multimodal_gen.runtime.layers.layernorm import (
RMSNorm,
@@ -12,10 +13,11 @@ from sglang.multimodal_gen.runtime.layers.layernorm import (
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=15, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
pytestmark = pytest.mark.skipif(
not torch.cuda.is_available() or torch.cuda.get_device_capability()[0] < 10,
reason="Qwen-Image QKV epilogue requires SM100+",
not torch.cuda.is_available() or torch.cuda.get_device_capability()[0] < 9,
reason="Qwen-Image QKV epilogue requires SM90+",
)
@@ -24,11 +26,12 @@ def _seed_cuda():
torch.cuda.manual_seed(0)
def test_qwen_qkv_epilogue_is_bit_exact():
heads = 4
@pytest.mark.parametrize(
"img_tokens,txt_tokens,heads",
[(17, 7, 1), (17, 7, 3), (17, 7, 4), (8152, 1365, 24)],
)
def test_qwen_qkv_epilogue_is_bit_exact(img_tokens, txt_tokens, heads):
head_dim = 128
img_tokens = 17
txt_tokens = 7
img_qkv = [
torch.randn(
1,
@@ -55,6 +58,9 @@ def test_qwen_qkv_epilogue_is_bit_exact():
RMSNorm(head_dim, eps=1e-6).to(device="cuda", dtype=torch.bfloat16)
for _ in range(4)
]
with torch.no_grad():
for norm in norms:
norm.weight.copy_(torch.randn_like(norm.weight))
def cache(tokens):
angles = torch.randn(tokens, head_dim // 2, device="cuda")
@@ -120,23 +126,30 @@ def test_qwen_qkv_epilogue_is_bit_exact():
]
assert all(not tensor.is_contiguous() for tensor in (*img_views, *txt_views))
packed_actual = try_fused_qwen_qkv_epilogue(
*img_views,
*txt_views,
norms[0].weight,
norms[1].weight,
norms[2].weight,
norms[3].weight,
img_cache,
txt_cache,
1e-6,
1e-6,
)
assert packed_actual is not None
assert all(
torch.equal(result, reference)
for result, reference in zip(packed_actual, expected)
)
# Unquantized image projections and packed text projections can use
# different token strides; each family also works when both are packed.
for img_inputs, txt_inputs in (
(img_views, txt_views),
(img_qkv, txt_views),
(img_views, txt_qkv),
):
packed_actual = try_fused_qwen_qkv_epilogue(
*img_inputs,
*txt_inputs,
norms[0].weight,
norms[1].weight,
norms[2].weight,
norms[3].weight,
img_cache,
txt_cache,
1e-6,
1e-6,
)
assert packed_actual is not None
assert all(
torch.equal(result, reference)
for result, reference in zip(packed_actual, expected)
)
def test_qwen_qkv_epilogue_rejects_compile():
@@ -190,5 +203,125 @@ def test_qwen_qkv_epilogue_rejects_unsupported_head_dim():
)
@pytest.mark.parametrize("mode", ["dense", "attention_mask", "text_mask", "sharded"])
def test_qwen_attention_preserves_normalized_segments(mode):
heads, head_dim, img_tokens, txt_tokens = 4, 128, 17, 7
dim = heads * head_dim
img = torch.randn(1, img_tokens, dim, device="cuda", dtype=torch.bfloat16)
txt = torch.randn(1, txt_tokens, dim, device="cuda", dtype=torch.bfloat16)
projections = [torch.randn_like(img) for _ in range(3)] + [
torch.randn_like(txt) for _ in range(3)
]
class IdentityProjection(torch.nn.Module):
def forward(self, x):
return x, None
class CaptureAttention(torch.nn.Module):
sp_attention_mode = "kv_gather"
def forward(self, q, k, v, **kwargs):
tensors = [q, k, v]
if kwargs["q_prefix"] is not None:
tensors = [
torch.cat([kwargs[name], tensor], dim=1)
for name, tensor in zip(
("q_prefix", "k_prefix", "v_prefix"), tensors
)
]
self.inputs = tuple(tensor.clone() for tensor in tensors)
return tensors[0]
module = object.__new__(qwen_image.QwenImageCrossAttention)
torch.nn.Module.__init__(module)
module._unquantized_added_qkv_is_packed = False
module.local_num_heads = heads
module.head_dim = head_dim
module.qk_norm = True
for name in ("norm_q", "norm_k", "norm_added_q", "norm_added_k"):
setattr(module, name, RMSNorm(head_dim).to(device="cuda", dtype=img.dtype))
module.attn = CaptureAttention()
module.to_out = torch.nn.ModuleList([IdentityProjection()])
module.to_add_out = IdentityProjection()
def cache(tokens):
angle = torch.randn(tokens, head_dim // 2, device="cuda")
return torch.cat([angle.cos(), angle.sin()], dim=-1)
caches = (cache(img_tokens), cache(txt_tokens))
kwargs = {}
if mode == "attention_mask":
kwargs["attn_mask"] = torch.ones(1, img_tokens + txt_tokens, device="cuda")
elif mode == "text_mask":
kwargs["encoder_hidden_states_mask"] = torch.ones(1, txt_tokens, device="cuda")
elif mode == "sharded":
kwargs["sp_text_sharded"] = True
with patch.object(
qwen_image,
"_get_qkv_projections",
side_effect=lambda *a, **kw: tuple(t.clone() for t in projections),
):
module.use_fused_qkv_epilogue = False
expected = module(img, txt, image_rotary_emb=caches, **kwargs)
expected_inputs = module.attn.inputs
module.use_fused_qkv_epilogue = True
with patch.object(
qwen_image,
"try_fused_qwen_qkv_epilogue",
wraps=try_fused_qwen_qkv_epilogue,
) as fused:
actual = module(img, txt, image_rotary_emb=caches, **kwargs)
assert fused.call_count == (1 if mode == "dense" else 0)
assert all(torch.equal(a, b) for a, b in zip(actual[:2], expected[:2]))
assert all(torch.equal(a, b) for a, b in zip(module.attn.inputs, expected_inputs))
@pytest.mark.parametrize("misaligned_image", [True, False])
def test_qwen_qkv_epilogue_rejects_misaligned_token_stride(misaligned_image):
tensor = torch.empty(1, 2, 2, 128, device="cuda", dtype=torch.bfloat16)
row = torch.empty(128, device="cuda", dtype=torch.bfloat16)
cache = torch.empty(2, 128, device="cuda", dtype=torch.float32)
pitched = torch.empty_strided(
(1, 2, 2, 128), (520, 260, 128, 1), device="cuda", dtype=torch.bfloat16
)
assert pitched.data_ptr() % 32 == 0
assert pitched.stride(1) * pitched.element_size() % 16 == 8
img = pitched if misaligned_image else tensor
txt = tensor if misaligned_image else pitched
assert (
try_fused_qwen_qkv_epilogue(
*([img] * 3),
*([txt] * 3),
*([row] * 4),
cache,
cache,
1e-6,
1e-6,
)
is None
)
@pytest.mark.parametrize("misaligned_image", [True, False])
def test_qwen_qkv_epilogue_rejects_misaligned_cache(misaligned_image):
tensor = torch.empty(1, 1, 1, 128, device="cuda", dtype=torch.bfloat16)
row = torch.empty(128, device="cuda", dtype=torch.bfloat16)
cache = torch.empty(1, 128, device="cuda", dtype=torch.float32)
misaligned = torch.empty(129, device="cuda", dtype=torch.float32)[1:].view(1, 128)
assert misaligned.is_contiguous() and misaligned.data_ptr() % 8 != 0
assert (
try_fused_qwen_qkv_epilogue(
*([tensor] * 6),
*([row] * 4),
misaligned if misaligned_image else cache,
cache if misaligned_image else misaligned,
1e-6,
1e-6,
)
is None
)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -45,6 +45,22 @@ def test_unknown_op_or_backend_raises():
K.select_kernel("gemm.fp8_scaled_mm", backend=KernelBackend.TRITON)
@pytest.mark.parametrize(
"platform, eligible",
[
(_SM90, True),
(_SM100, True),
(PlatformInfo(device_type="cuda", cuda_arch_major=8, cuda_arch_minor=0), False),
(_CPU, False),
(_HIP, False),
],
)
def test_qwen_qkv_registry_accepts_hopper(platform, eligible):
spec = K.select_kernel("diffusion.qwen_qkv_epilogue")
assert spec.backend is KernelBackend.JIT
assert K.capabilities_satisfied(spec.capabilities, platform) is eligible
def test_multi_backend_requires_explicit_backend(monkeypatch):
# Device is a hard eligibility filter, not a ranking: >1 usable backend on
# the current device means selection must name one.