[diffusion] Z-Image bit-exact fused qk-norm (H200 Turbo 1024px e2e -6.4%) (#33886)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Xiaoyu Zhang
2026-08-07 17:03:28 +08:00
committed by GitHub
co-authored by Claude Fable 5
parent 5e60363960
commit 572434e2f6
3 changed files with 286 additions and 13 deletions
@@ -180,3 +180,163 @@ def zimage_rmsnorm_tanh_residual(
num_warps=8,
)
return y
@triton.jit
def _qk_rmsnorm_native_kernel(
y_ptr,
x_ptr,
weight_ptr,
token_stride,
nheads,
n_rows,
head_dim: tl.constexpr,
eps: tl.constexpr,
rows_per_prog: tl.constexpr,
):
"""Per-head RMSNorm replicating the eager ZImageRMSNorm kernel chain bit-for-bit.
The eager path (`ZImageRMSNorm.forward` on a `[rows, head_dim]` bf16 tensor)
lowers to aten pow/mean/rsqrt/mul kernels. For bf16 rows of 128, aten's
reduce_kernel vectorizes with 16-byte loads (8 bf16 elements per lane):
lane t serially accumulates contiguous elements 8t .. 8t+7 in fp32
(squares rounded to bf16 first, matching aten::pow), then a shfl-down
butterfly tree combines the 32 lane partials (halves 16/8/4/2/1). Every
intermediate below is rounded to bf16 at exactly the same points as the
aten kernels, so the output satisfies `torch.equal` against the eager path
(verified over 9M+ random rows across scales 0.005-200).
"""
prog = tl.program_id(0)
row_offs = tl.arange(0, rows_per_prog)
rows = prog * rows_per_prog + row_offs
row_mask = rows < n_rows
tokens = rows // nheads
heads = rows % nheads
base = x_ptr + tokens * token_stride + heads * head_dim
offs = tl.arange(0, head_dim)
x = tl.load(base[:, None] + offs[None, :], mask=row_mask[:, None], other=0.0)
sq = (x * x).to(tl.bfloat16)
# Lane t's serial accumulation of contiguous elements 8t..8t+7: peel the
# 8-element groups apart with ordered tl.split chains (loads stay one
# coalesced pass) and add them in exact serial order in fp32.
g = tl.reshape(sq, (rows_per_prog, head_dim // 8, 4, 2), can_reorder=False)
p0, p1 = tl.split(g) # elements (0,2,4,6) / (1,3,5,7)
p00, p01 = tl.split(
tl.reshape(p0, (rows_per_prog, head_dim // 8, 2, 2), can_reorder=False)
) # (0,4) / (2,6)
p10, p11 = tl.split(
tl.reshape(p1, (rows_per_prog, head_dim // 8, 2, 2), can_reorder=False)
) # (1,5) / (3,7)
s0, s4 = tl.split(
tl.reshape(p00, (rows_per_prog, head_dim // 8, 1, 2), can_reorder=False)
)
s2, s6 = tl.split(
tl.reshape(p01, (rows_per_prog, head_dim // 8, 1, 2), can_reorder=False)
)
s1, s5 = tl.split(
tl.reshape(p10, (rows_per_prog, head_dim // 8, 1, 2), can_reorder=False)
)
s3, s7 = tl.split(
tl.reshape(p11, (rows_per_prog, head_dim // 8, 1, 2), can_reorder=False)
)
acc = tl.reshape(s0, (rows_per_prog, head_dim // 8)).to(tl.float32)
acc = acc + tl.reshape(s1, (rows_per_prog, head_dim // 8)).to(tl.float32)
acc = acc + tl.reshape(s2, (rows_per_prog, head_dim // 8)).to(tl.float32)
acc = acc + tl.reshape(s3, (rows_per_prog, head_dim // 8)).to(tl.float32)
acc = acc + tl.reshape(s4, (rows_per_prog, head_dim // 8)).to(tl.float32)
acc = acc + tl.reshape(s5, (rows_per_prog, head_dim // 8)).to(tl.float32)
acc = acc + tl.reshape(s6, (rows_per_prog, head_dim // 8)).to(tl.float32)
acc = acc + tl.reshape(s7, (rows_per_prog, head_dim // 8)).to(tl.float32)
# shfl-down butterfly over the 16 lane partials (lanes 16..31 hold the
# additive identity in aten's 32-lane warp, so their stage is an exact
# no-op): acc[i] += acc[i + half] for half in (8, 4, 2, 1).
acc = tl.sum(tl.reshape(acc, (rows_per_prog, 2, 8), can_reorder=False), axis=1)
acc = tl.sum(tl.reshape(acc, (rows_per_prog, 2, 4), can_reorder=False), axis=1)
acc = tl.sum(tl.reshape(acc, (rows_per_prog, 2, 2), can_reorder=False), axis=1)
ssum = tl.sum(acc, axis=1)
ms = (ssum / head_dim).to(tl.bfloat16)
# aten adds the python-float eps in fp32 opmath, then rsqrt rounds to bf16.
rstd = tl.rsqrt((ms.to(tl.float32) + eps).to(tl.bfloat16).to(tl.float32)).to(
tl.bfloat16
)
weight = tl.load(weight_ptr + offs)
y = ((x.to(tl.float32) * rstd.to(tl.float32)[:, None]).to(tl.bfloat16) * weight).to(
tl.bfloat16
)
tl.store(
y_ptr + rows[:, None] * head_dim + offs[None, :], y, mask=row_mask[:, None]
)
def _qk_head_token_stride(x: torch.Tensor, head_dim: int) -> int | None:
"""Token stride for a `[B, S, H, head_dim]` view whose head block is packed.
Accepts the strided views produced by slicing a fused qkv projection: the
last dim must be contiguous, heads packed (`stride(-2) == head_dim`), and
tokens uniformly strided across the flattened `(B, S)` dims.
"""
if x.dim() != 4 or x.shape[-1] != head_dim:
return None
if x.stride(-1) != 1 or x.stride(-2) != head_dim:
return None
token_stride = x.stride(1)
if x.shape[0] > 1 and x.stride(0) != x.shape[1] * token_stride:
return None
return token_stride
def can_use_qk_rmsnorm_native(
x: torch.Tensor, weight: torch.Tensor, head_dim: int
) -> bool:
return (
x.is_cuda
and weight.is_cuda
and x.dtype == torch.bfloat16
and head_dim == 128
and weight.numel() == head_dim
and weight.is_contiguous()
and _qk_head_token_stride(x, head_dim) is not None
)
def zimage_qk_rmsnorm_native(
x: torch.Tensor,
weight: torch.Tensor,
eps: float,
) -> torch.Tensor | None:
"""Fused, bit-exact ZImageRMSNorm over q/k heads; returns a contiguous tensor.
Reads strided `[B, S, H, head_dim]` views (e.g. fused-qkv slices) directly,
absorbing the `.contiguous()` materialization the eager path needs.
Returns None when the input is not supported (caller falls back).
"""
head_dim = x.shape[-1]
if not can_use_qk_rmsnorm_native(x, weight, head_dim):
return None
token_stride = _qk_head_token_stride(x, head_dim)
if token_stride is None:
return None
if weight.dtype != x.dtype:
weight = weight.to(dtype=x.dtype)
nheads = x.shape[2]
n_rows = x.shape[0] * x.shape[1] * nheads
rows_per_prog = 8
y = torch.empty(x.shape, dtype=x.dtype, device=x.device)
grid = (triton.cdiv(n_rows, rows_per_prog),)
with torch.get_device_module().device(x.device):
_qk_rmsnorm_native_kernel[grid](
y,
x,
weight,
token_stride,
nheads,
n_rows,
head_dim,
eps,
rows_per_prog=rows_per_prog,
num_warps=8,
)
return y
@@ -126,6 +126,38 @@ def zimage_rmsnorm_scale(
return norm(x) * scale
def zimage_native_qk_rmsnorm(
q: torch.Tensor,
k: torch.Tensor,
norm_q: ZImageRMSNorm,
norm_k: ZImageRMSNorm,
head_dim: int,
) -> tuple[torch.Tensor, torch.Tensor] | None:
"""Fused per-head ZImageRMSNorm for q/k, bit-exact vs the eager fallback.
Replaces `apply_qk_norm`'s eager chain (`q_norm(q.reshape(-1, head_dim))`)
with one Triton launch per tensor that reads the strided fused-qkv slices
directly. Returns contiguous (q, k) or None when unsupported.
"""
from sglang.kernels.ops.diffusion.triton.zimage_native_norm import (
can_use_qk_rmsnorm_native,
zimage_qk_rmsnorm_native,
)
q_weight = norm_q.weight.data
k_weight = norm_k.weight.data
if not (
can_use_qk_rmsnorm_native(q, q_weight, head_dim)
and can_use_qk_rmsnorm_native(k, k_weight, head_dim)
):
return None
q_out = zimage_qk_rmsnorm_native(q, q_weight, norm_q.variance_epsilon)
k_out = zimage_qk_rmsnorm_native(k, k_weight, norm_k.variance_epsilon)
if q_out is None or k_out is None:
return None
return q_out, k_out
class TimestepEmbedder(nn.Module):
def __init__(self, out_size, mid_size=None, frequency_embedding_size=256):
super().__init__()
@@ -316,6 +348,15 @@ class ZImageAttention(nn.Module):
num_replicated_suffix: int = 0,
skip_sequence_parallel_override: bool = False,
):
# The fused native qk-norm kernel reads the strided fused-qkv slices
# directly and writes contiguous outputs, so q/k materialization is
# deferred to it on the main (rope-cache) path.
try_native_qk_norm = (
self.qk_norm
and rope_cos_sin_cache is not None
and self.enable_zimage_qk_fusion
and not torch.compiler.is_compiling()
)
if self.use_fused_qkv:
qkv, _ = self.to_qkv(hidden_states)
q, k, v = qkv.split(
@@ -326,8 +367,9 @@ class ZImageAttention(nn.Module):
],
dim=-1,
)
q = q.contiguous()
k = k.contiguous()
if not try_native_qk_norm:
q = q.contiguous()
k = k.contiguous()
v = v.contiguous()
else:
q, _ = self.to_q(hidden_states)
@@ -339,17 +381,37 @@ class ZImageAttention(nn.Module):
if rope_cos_sin_cache is not None:
if self.qk_norm:
q, k = apply_qk_norm_with_optional_rope(
q=q,
k=k,
q_norm=self.norm_q,
k_norm=self.norm_k,
head_dim=self.head_dim,
cos_sin_cache=rope_cos_sin_cache,
is_neox=False,
positions=rope_positions,
allow_inplace=False,
)
fused_qk = None
if try_native_qk_norm:
fused_qk = zimage_native_qk_rmsnorm(
q, k, self.norm_q, self.norm_k, self.head_dim
)
if fused_qk is not None:
q, k = fused_qk
# positions=None is handled identically to the eager
# fallback (arange over seqlen, repeated across batch).
q, k = apply_flashinfer_rope_qk_inplace(
q,
k,
rope_cos_sin_cache,
head_size=self.head_dim,
is_neox=False,
positions=rope_positions,
)
else:
q = q.contiguous()
k = k.contiguous()
q, k = apply_qk_norm_with_optional_rope(
q=q,
k=k,
q_norm=self.norm_q,
k_norm=self.norm_k,
head_dim=self.head_dim,
cos_sin_cache=rope_cos_sin_cache,
is_neox=False,
positions=rope_positions,
allow_inplace=False,
)
else:
q, k = apply_flashinfer_rope_qk_inplace(
q,
@@ -0,0 +1,51 @@
import unittest
import torch
from sglang.multimodal_gen.runtime.models.dits.zimage import (
ZImageRMSNorm,
zimage_native_qk_rmsnorm,
)
HEAD_DIM = 128
@unittest.skipUnless(torch.cuda.is_available(), "requires CUDA")
class TestZImageQkNormFusion(unittest.TestCase):
def _make_norm(self, seed):
torch.manual_seed(seed)
norm = ZImageRMSNorm(HEAD_DIM, eps=1e-5)
with torch.no_grad():
norm.weight.copy_(torch.randn(HEAD_DIM) * 0.5 + 1.0)
return norm.to(device="cuda", dtype=torch.bfloat16)
def test_bit_exact_on_strided_qkv_view(self):
norm_q, norm_k = self._make_norm(1), self._make_norm(2)
heads, kv_heads = 4, 4
total = (heads + 2 * kv_heads) * HEAD_DIM
qkv = (torch.randn(1, 64, total, device="cuda")).to(torch.bfloat16)
q = qkv[..., : heads * HEAD_DIM].view(1, 64, heads, HEAD_DIM)
k = qkv[..., heads * HEAD_DIM : (heads + kv_heads) * HEAD_DIM].view(
1, 64, kv_heads, HEAD_DIM
)
fused = zimage_native_qk_rmsnorm(q, k, norm_q, norm_k, HEAD_DIM)
self.assertIsNotNone(fused)
q_out, k_out = fused
self.assertTrue(q_out.is_contiguous() and k_out.is_contiguous())
self.assertTrue(
torch.equal(q_out, norm_q(q.reshape(-1, HEAD_DIM)).view(q.shape))
)
self.assertTrue(
torch.equal(k_out, norm_k(k.reshape(-1, HEAD_DIM)).view(k.shape))
)
def test_unsupported_head_dim_falls_back(self):
norm = ZImageRMSNorm(64, eps=1e-5).to(device="cuda", dtype=torch.bfloat16)
q = torch.randn(1, 32, 4, 64, device="cuda", dtype=torch.bfloat16)
k = torch.randn(1, 32, 4, 64, device="cuda", dtype=torch.bfloat16)
self.assertIsNone(zimage_native_qk_rmsnorm(q, k, norm, norm, 64))
if __name__ == "__main__":
unittest.main()