[diffusion] Fuse LongCat-Image QKNorm and interleaved RoPE (#35995)

This commit is contained in:
Xiaoyu Zhang
2026-08-24 12:07:26 +08:00
committed by GitHub
parent 09592f5889
commit 8dcfb3b5e7
7 changed files with 394 additions and 53 deletions
@@ -163,6 +163,32 @@ SGL_DEVICE T rotary_sub(T x, T cos, T y, T sin) {
#endif #endif
} }
template <typename T>
SGL_DEVICE T rotary_add_fp32(T x, float cos, T y, float sin) {
const float x_fp32 = device::cast<fp32_t>(x);
const float y_fp32 = device::cast<fp32_t>(y);
#ifdef USE_ROCM
return device::cast<T>(x_fp32 * cos + y_fp32 * sin);
#else
const float lhs = __fmul_rn(x_fp32, cos);
const float rhs = __fmul_rn(y_fp32, sin);
return device::cast<T>(__fadd_rn(lhs, rhs));
#endif
}
template <typename T>
SGL_DEVICE T rotary_sub_fp32(T x, float cos, T y, float sin) {
const float x_fp32 = device::cast<fp32_t>(x);
const float y_fp32 = device::cast<fp32_t>(y);
#ifdef USE_ROCM
return device::cast<T>(x_fp32 * cos - y_fp32 * sin);
#else
const float lhs = __fmul_rn(x_fp32, cos);
const float rhs = __fmul_rn(-y_fp32, sin);
return device::cast<T>(__fadd_rn(lhs, rhs));
#endif
}
template < template <
int64_t kHeadDim, int64_t kHeadDim,
int64_t kRopeDim, int64_t kRopeDim,
@@ -196,8 +222,8 @@ __global__ void fused_qknorm_rope_warp(const QKNormRopeParamsT<kPackKV> __grid_c
!kIsNeox || (kRotaryLanes >= 2 && kRotaryLanes % 2 == 0), !kIsNeox || (kRotaryLanes >= 2 && kRotaryLanes % 2 == 0),
"NeoX fused qknorm+rope requires an even rotary lane count"); "NeoX fused qknorm+rope requires an even rotary lane count");
static_assert( static_assert(
!kRoundNormBeforeRope || std::is_same_v<DType, CacheDType>, !kRoundNormBeforeRope || std::is_same_v<DType, CacheDType> || std::is_same_v<CacheDType, fp32_t>,
"Rounded QKNorm+RoPE requires cache and activation dtypes to match"); "Rounded QKNorm+RoPE requires cache and activation dtypes to match or an FP32 cache");
using Packed = packed_t<DType>; using Packed = packed_t<DType>;
using Storage = AlignedVector<Packed, kVecSize>; using Storage = AlignedVector<Packed, kVecSize>;
@@ -307,23 +333,37 @@ __global__ void fused_qknorm_rope_warp(const QKNormRopeParamsT<kPackKV> __grid_c
(kCacheHasFullWidth ? lane_id : lane_id % kHalfRotaryLanes) * kElemsPerThread + 2 * j + i; (kCacheHasFullWidth ? lane_id : lane_id % kHalfRotaryLanes) * kElemsPerThread + 2 * j + i;
const auto cos = load_cache_value(cos_ptr, cache_idx); const auto cos = load_cache_value(cos_ptr, cache_idx);
const auto sin = load_cache_value(sin_ptr, cache_idx); const auto sin = load_cache_value(sin_ptr, cache_idx);
if constexpr (std::is_same_v<CacheDType, fp32_t>) {
values[i] = lane_id < kHalfRotaryLanes ? rotary_sub_fp32(values[i], cos, partner_values[i], sin)
: rotary_add_fp32(values[i], cos, partner_values[i], sin);
} else {
values[i] = lane_id < kHalfRotaryLanes ? rotary_sub(values[i], cos, partner_values[i], sin) values[i] = lane_id < kHalfRotaryLanes ? rotary_sub(values[i], cos, partner_values[i], sin)
: rotary_add(values[i], cos, partner_values[i], sin); : rotary_add(values[i], cos, partner_values[i], sin);
} }
} }
} }
}
} else { } else {
if (lane_id < kRotaryLanes) { if (lane_id < kRotaryLanes) {
#pragma unroll #pragma unroll
for (uint32_t j = 0; j < kVecSize; ++j) { for (uint32_t j = 0; j < kVecSize; ++j) {
auto& values = unpack(output_vec[j]); auto& values = unpack(output_vec[j]);
const auto half_idx = lane_id * kElemsPerThread / 2 + j; const auto cache_idx_0 =
const auto cos = load_cache_value(cos_ptr, half_idx); kCacheHasFullWidth ? lane_id * kElemsPerThread + 2 * j : lane_id * kElemsPerThread / 2 + j;
const auto sin = load_cache_value(sin_ptr, half_idx); const auto cache_idx_1 = kCacheHasFullWidth ? cache_idx_0 + 1 : cache_idx_0;
const auto cos_0 = load_cache_value(cos_ptr, cache_idx_0);
const auto sin_0 = load_cache_value(sin_ptr, cache_idx_0);
const auto cos_1 = load_cache_value(cos_ptr, cache_idx_1);
const auto sin_1 = load_cache_value(sin_ptr, cache_idx_1);
const auto x = values[0]; const auto x = values[0];
const auto y = values[1]; const auto y = values[1];
values[0] = rotary_sub(x, cos, y, sin); if constexpr (std::is_same_v<CacheDType, fp32_t>) {
values[1] = rotary_add(y, cos, x, sin); values[0] = rotary_sub_fp32(x, cos_0, y, sin_0);
values[1] = rotary_add_fp32(y, cos_1, x, sin_1);
} else {
values[0] = rotary_sub(x, cos_0, y, sin_0);
values[1] = rotary_add(y, cos_1, x, sin_1);
}
} }
} }
} }
@@ -383,11 +423,15 @@ __global__ void fused_qknorm_rope_warp(const QKNormRopeParamsT<kPackKV> __grid_c
for (uint32_t i = 0; i < kElemsPerThread; i += 2) { for (uint32_t i = 0; i < kElemsPerThread; i += 2) {
const float x = elems[i]; const float x = elems[i];
const float y = elems[i + 1]; const float y = elems[i + 1];
const int half_idx = static_cast<int>(lane_id * kElemsPerThread + i) / 2; const auto cache_idx_0 =
const float cos = cast<fp32_t>(load_cache_value(cos_ptr, half_idx)); kCacheHasFullWidth ? lane_id * kElemsPerThread + i : (lane_id * kElemsPerThread + i) / 2;
const float sin = cast<fp32_t>(load_cache_value(sin_ptr, half_idx)); const auto cache_idx_1 = kCacheHasFullWidth ? cache_idx_0 + 1 : cache_idx_0;
elems[i] = x * cos - y * sin; const float cos_0 = cast<fp32_t>(load_cache_value(cos_ptr, cache_idx_0));
elems[i + 1] = y * cos + x * sin; const float sin_0 = cast<fp32_t>(load_cache_value(sin_ptr, cache_idx_0));
const float cos_1 = cast<fp32_t>(load_cache_value(cos_ptr, cache_idx_1));
const float sin_1 = cast<fp32_t>(load_cache_value(sin_ptr, cache_idx_1));
elems[i] = x * cos_0 - y * sin_0;
elems[i + 1] = y * cos_1 + x * sin_1;
} }
} }
} }
@@ -114,7 +114,7 @@ Several norms look interchangeable and are not. Start here.
| Entry point | Backend | Contract | | Entry point | Backend | Contract |
|---|---|---| |---|---|---|
| `fused_inplace_qknorm_rope` | JIT CUDA | one bf16 rounding step vs split baseline; `round_norm_before_rope=True` makes it exact | | `fused_inplace_qknorm_rope` | JIT CUDA | one bf16 rounding step vs split baseline; `round_norm_before_rope=True` makes it exact; supports compact and full-width NeoX/interleaved caches |
| `fused_qknorm_rope_pack_kv` | JIT CUDA | as above, also packs prefix K/V | | `fused_qknorm_rope_pack_kv` | JIT CUDA | as above, also packs prefix K/V |
| `fused_rope_rotate_half_bitexact` | Triton | bit-exact (elementwise only) | | `fused_rope_rotate_half_bitexact` | Triton | bit-exact (elementwise only) |
| `fused_interleaved_rope_fp64` | JIT CUDA | bit-exact vs paired SANA-Video fp64 RoPE | | `fused_interleaved_rope_fp64` | JIT CUDA | bit-exact vs paired SANA-Video fp64 RoPE |
@@ -96,15 +96,13 @@ def _can_use_fused_qknorm_rope(
rotary_lanes, rotary_lanes,
) )
return False return False
elif cache_has_full_width:
logger.warning("Full-width cos/sin caches are only supported for NeoX RoPE")
return False
if pack_kv and cache_has_full_width: if pack_kv and cache_has_full_width:
logger.warning("KV packing does not support full-width cos/sin caches") logger.warning("KV packing does not support full-width cos/sin caches")
return False return False
if round_norm_before_rope and cache_dtype != dtype: if round_norm_before_rope and cache_dtype not in (dtype, torch.float32):
logger.warning( logger.warning(
"Exact fused QKNorm+RoPE requires cache dtype %s to match activation dtype %s", "Exact fused QKNorm+RoPE requires cache dtype %s to match activation "
"dtype %s or use float32",
cache_dtype, cache_dtype,
dtype, dtype,
) )
@@ -11,11 +11,9 @@ and feeds them directly to the timestep embedder. The diffusers pipeline passes
SGLang's DenoisingStage passes the raw timestep instead, so the value reaching SGLang's DenoisingStage passes the raw timestep instead, so the value reaching
the embedder is identical and no division is needed here. the embedder is identical and no division is needed here.
Attention alignment: uses USPAttention (FA3/FA4 on Hopper/Blackwell) with Attention alignment: uses USPAttention (FA3/FA4 on Hopper/Blackwell) and the
SGLang fused RMSNorm (apply_qk_norm). RoPE is applied separately via SGLang fused QKNorm+RoPE kernel. LongCat's full-width, interleaved RoPE cache is
diffusers apply_rotary_emb because LongCat's axes_dims_rope=[16,56,56] handled directly instead of materializing the Diffusers rotate-pair chain.
sums to head_dim=128 (full rotation), which is incompatible with flashinfer's
cos_sin_cache format that requires rotary_dim <= head_dim.
""" """
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
@@ -34,9 +32,18 @@ from diffusers.models.normalization import (
AdaLayerNormZeroSingle, AdaLayerNormZeroSingle,
) )
from sglang.kernels.ops.diffusion import (
BitExactFusionGate,
can_use_fused_inplace_qknorm_rope,
tensors_equal,
)
from sglang.multimodal_gen.runtime.distributed import get_tp_world_size from sglang.multimodal_gen.runtime.distributed import get_tp_world_size
from sglang.multimodal_gen.runtime.layers.attention import USPAttention from sglang.multimodal_gen.runtime.layers.attention import USPAttention
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm, apply_qk_norm from sglang.multimodal_gen.runtime.layers.layernorm import (
RMSNorm,
apply_qk_norm,
apply_qk_norm_rope,
)
from sglang.multimodal_gen.runtime.layers.linear import ( from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear, ColumnParallelLinear,
RowParallelLinear, RowParallelLinear,
@@ -49,6 +56,124 @@ from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__) logger = init_logger(__name__)
_LONGCAT_QKNORM_ROPE = BitExactFusionGate("LongCat fused QKNorm+RoPE")
def _longcat_qknorm_rope_reference(
q: torch.Tensor,
k: torch.Tensor,
q_norm: RMSNorm,
k_norm: RMSNorm,
head_dim: int,
image_rotary_emb: Tuple[torch.Tensor, torch.Tensor],
) -> Tuple[torch.Tensor, torch.Tensor]:
q, k = apply_qk_norm(q, k, q_norm, k_norm, head_dim)
q = apply_rotary_emb(q, image_rotary_emb, sequence_dim=1)
k = apply_rotary_emb(k, image_rotary_emb, sequence_dim=1)
return q, k
def _apply_longcat_qknorm_rope(
q: torch.Tensor,
k: torch.Tensor,
q_norm: RMSNorm,
k_norm: RMSNorm,
head_dim: int,
image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]],
cos_sin_cache: Optional[torch.Tensor],
positions: Optional[torch.Tensor],
) -> Tuple[torch.Tensor, torch.Tensor]:
if image_rotary_emb is None:
return apply_qk_norm(q, k, q_norm, k_norm, head_dim)
q_eps = q_norm.variance_epsilon
k_eps = k_norm.variance_epsilon
can_fuse = (
cos_sin_cache is not None
and positions is not None
and q.is_cuda
and not torch.compiler.is_compiling()
and q_eps == k_eps
and q.dtype in (torch.float16, torch.bfloat16)
and k.dtype == q.dtype
and q_norm.weight.dtype == q.dtype
and k_norm.weight.dtype == k.dtype
and q.is_contiguous()
and k.is_contiguous()
and can_use_fused_inplace_qknorm_rope(
head_dim=head_dim,
rope_dim=head_dim,
is_neox=False,
dtype=q.dtype,
cache_dtype=cos_sin_cache.dtype,
round_norm_before_rope=True,
cache_has_full_width=True,
)
)
verified = _LONGCAT_QKNORM_ROPE.verified
if (
can_fuse
and not _LONGCAT_QKNORM_ROPE.disabled
and (verified or _LONGCAT_QKNORM_ROPE.can_attempt_once())
):
if q.shape[0] > 1:
positions = positions.repeat(q.shape[0])
q_input = q.clone() if not verified else q
k_input = k.clone() if not verified else k
try:
out = apply_qk_norm_rope(
q=q,
k=k,
q_norm=q_norm,
k_norm=k_norm,
head_dim=head_dim,
cos_sin_cache=cos_sin_cache,
is_neox=False,
positions=positions,
round_norm_before_rope=True,
cache_has_full_width=True,
)
except Exception as exc:
_LONGCAT_QKNORM_ROPE.on_exception(exc, logger=logger)
return _longcat_qknorm_rope_reference(
q_input,
k_input,
q_norm,
k_norm,
head_dim,
image_rotary_emb,
)
else:
if verified:
return out
ref = _longcat_qknorm_rope_reference(
q_input,
k_input,
q_norm,
k_norm,
head_dim,
image_rotary_emb,
)
return _LONGCAT_QKNORM_ROPE.accept_or_fallback(
out,
ref,
equal=tensors_equal,
logger=logger,
mismatch_msg=(
"LongCat fused QKNorm+RoPE is not bit-exact on this "
"platform; falling back to the Diffusers chain"
),
)
return _longcat_qknorm_rope_reference(
q,
k,
q_norm,
k_norm,
head_dim,
image_rotary_emb,
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# FFN # FFN
@@ -108,9 +233,9 @@ class _LongCatFFN(nn.Module):
class _LongCatJointAttention(nn.Module): class _LongCatJointAttention(nn.Module):
"""Double-stream (joint) attention for _TransformerBlock. """Double-stream (joint) attention for _TransformerBlock.
img and txt tokens are projected separately, QK-norm applied via SGLang img and txt tokens are projected separately, passed through fused QKNorm
fused kernel, RoPE applied via diffusers apply_rotary_emb (supports full and full-width interleaved RoPE, then concatenated (txt first) before
head_dim rotation), then concatenated (txt first) before USPAttention. USPAttention.
TP: Q/K/V and add_q/k/v use ColumnParallelLinear (heads sharded across TP ranks). TP: Q/K/V and add_q/k/v use ColumnParallelLinear (heads sharded across TP ranks).
Output projections use RowParallelLinear (all-reduce after matmul). Output projections use RowParallelLinear (all-reduce after matmul).
@@ -200,6 +325,8 @@ class _LongCatJointAttention(nn.Module):
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor, encoder_hidden_states: torch.Tensor,
image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
cos_sin_cache: Optional[torch.Tensor] = None,
positions: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]: ) -> Tuple[torch.Tensor, torch.Tensor]:
txt_seq_len = encoder_hidden_states.shape[1] txt_seq_len = encoder_hidden_states.shape[1]
@@ -217,10 +344,34 @@ class _LongCatJointAttention(nn.Module):
ek = ek.unflatten(-1, (self.num_local_heads, self.head_dim)) ek = ek.unflatten(-1, (self.num_local_heads, self.head_dim))
ev = ev.unflatten(-1, (self.num_local_heads, self.head_dim)) ev = ev.unflatten(-1, (self.num_local_heads, self.head_dim))
# SGLang fused QK-norm if image_rotary_emb is None:
q, k = apply_qk_norm(q, k, self.norm_q, self.norm_k, self.head_dim) image_rotary_emb_txt = image_rotary_emb_img = None
eq, ek = apply_qk_norm( else:
eq, ek, self.norm_added_q, self.norm_added_k, self.head_dim cos, sin = image_rotary_emb
image_rotary_emb_txt = (cos[:txt_seq_len], sin[:txt_seq_len])
image_rotary_emb_img = (cos[txt_seq_len:], sin[txt_seq_len:])
positions_txt = positions[:txt_seq_len] if positions is not None else None
positions_img = positions[txt_seq_len:] if positions is not None else None
q, k = _apply_longcat_qknorm_rope(
q,
k,
self.norm_q,
self.norm_k,
self.head_dim,
image_rotary_emb_img,
cos_sin_cache,
positions_img,
)
eq, ek = _apply_longcat_qknorm_rope(
eq,
ek,
self.norm_added_q,
self.norm_added_k,
self.head_dim,
image_rotary_emb_txt,
cos_sin_cache,
positions_txt,
) )
# Concatenate: txt first, then img (matches diffusers convention) # Concatenate: txt first, then img (matches diffusers convention)
@@ -228,12 +379,6 @@ class _LongCatJointAttention(nn.Module):
k = torch.cat([ek, k], dim=1) k = torch.cat([ek, k], dim=1)
v = torch.cat([ev, v], dim=1) v = torch.cat([ev, v], dim=1)
# RoPE applied after concat, over the full [txt+img] sequence.
# image_rotary_emb shape: [txt_len+img_len, head_dim] — matches q/k dim=1.
if image_rotary_emb is not None:
q = apply_rotary_emb(q, image_rotary_emb, sequence_dim=1)
k = apply_rotary_emb(k, image_rotary_emb, sequence_dim=1)
x = self.attn(q, k, v, num_replicated_prefix=txt_seq_len) x = self.attn(q, k, v, num_replicated_prefix=txt_seq_len)
x = x.flatten(2, 3).to(q.dtype) x = x.flatten(2, 3).to(q.dtype)
@@ -298,6 +443,8 @@ class _LongCatSingleAttention(nn.Module):
self, self,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
cos_sin_cache: Optional[torch.Tensor] = None,
positions: Optional[torch.Tensor] = None,
) -> torch.Tensor: ) -> torch.Tensor:
q, _ = self.to_q(hidden_states) q, _ = self.to_q(hidden_states)
k, _ = self.to_k(hidden_states) k, _ = self.to_k(hidden_states)
@@ -306,13 +453,16 @@ class _LongCatSingleAttention(nn.Module):
k = k.unflatten(-1, (self.num_local_heads, self.head_dim)) k = k.unflatten(-1, (self.num_local_heads, self.head_dim))
v = v.unflatten(-1, (self.num_local_heads, self.head_dim)) v = v.unflatten(-1, (self.num_local_heads, self.head_dim))
# SGLang fused QK-norm q, k = _apply_longcat_qknorm_rope(
q, k = apply_qk_norm(q, k, self.norm_q, self.norm_k, self.head_dim) q,
k,
# RoPE via diffusers (supports full head_dim rotation, sequence_dim=1) self.norm_q,
if image_rotary_emb is not None: self.norm_k,
q = apply_rotary_emb(q, image_rotary_emb, sequence_dim=1) self.head_dim,
k = apply_rotary_emb(k, image_rotary_emb, sequence_dim=1) image_rotary_emb,
cos_sin_cache,
positions,
)
x = self.attn(q, k, v) x = self.attn(q, k, v)
return x.flatten(2, 3).to(q.dtype) return x.flatten(2, 3).to(q.dtype)
@@ -400,6 +550,8 @@ class _SingleTransformerBlock(nn.Module):
encoder_hidden_states: torch.Tensor, encoder_hidden_states: torch.Tensor,
temb: torch.Tensor, temb: torch.Tensor,
image_rotary_emb=None, image_rotary_emb=None,
cos_sin_cache=None,
positions=None,
**kwargs, **kwargs,
): ):
text_seq_len = encoder_hidden_states.shape[1] text_seq_len = encoder_hidden_states.shape[1]
@@ -412,6 +564,8 @@ class _SingleTransformerBlock(nn.Module):
attn_output = self.attn( attn_output = self.attn(
hidden_states=norm_hidden_states, hidden_states=norm_hidden_states,
image_rotary_emb=image_rotary_emb, image_rotary_emb=image_rotary_emb,
cos_sin_cache=cos_sin_cache,
positions=positions,
) )
hidden_states = torch.cat([attn_output, mlp_hidden_states], dim=2) hidden_states = torch.cat([attn_output, mlp_hidden_states], dim=2)
gate = gate.unsqueeze(1) gate = gate.unsqueeze(1)
@@ -462,6 +616,8 @@ class _TransformerBlock(nn.Module):
encoder_hidden_states: torch.Tensor, encoder_hidden_states: torch.Tensor,
temb: torch.Tensor, temb: torch.Tensor,
image_rotary_emb=None, image_rotary_emb=None,
cos_sin_cache=None,
positions=None,
**kwargs, **kwargs,
): ):
norm_hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.norm1( norm_hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.norm1(
@@ -475,6 +631,8 @@ class _TransformerBlock(nn.Module):
hidden_states=norm_hidden_states, hidden_states=norm_hidden_states,
encoder_hidden_states=norm_encoder_hidden_states, encoder_hidden_states=norm_encoder_hidden_states,
image_rotary_emb=image_rotary_emb, image_rotary_emb=image_rotary_emb,
cos_sin_cache=cos_sin_cache,
positions=positions,
) )
attn_output = gate_msa.unsqueeze(1) * attn_output attn_output = gate_msa.unsqueeze(1) * attn_output
@@ -680,6 +838,9 @@ class LongCatImageTransformer2DModel(BaseDiT, LayerwiseOffloadableModuleMixin):
image_rotary_emb = kwargs.get("image_rotary_emb") or self.pos_embed( image_rotary_emb = kwargs.get("image_rotary_emb") or self.pos_embed(
torch.cat((txt_ids, img_ids), dim=0) torch.cat((txt_ids, img_ids), dim=0)
) )
cos, sin = image_rotary_emb
cos_sin_cache = torch.cat((cos, sin), dim=-1).contiguous()
positions = torch.arange(cos.shape[0], device=cos.device, dtype=torch.int64)
for block in self.transformer_blocks: for block in self.transformer_blocks:
encoder_hidden_states, hidden_states = block( encoder_hidden_states, hidden_states = block(
@@ -687,6 +848,8 @@ class LongCatImageTransformer2DModel(BaseDiT, LayerwiseOffloadableModuleMixin):
encoder_hidden_states=encoder_hidden_states, encoder_hidden_states=encoder_hidden_states,
temb=temb, temb=temb,
image_rotary_emb=image_rotary_emb, image_rotary_emb=image_rotary_emb,
cos_sin_cache=cos_sin_cache,
positions=positions,
) )
for block in self.single_transformer_blocks: for block in self.single_transformer_blocks:
@@ -695,6 +858,8 @@ class LongCatImageTransformer2DModel(BaseDiT, LayerwiseOffloadableModuleMixin):
encoder_hidden_states=encoder_hidden_states, encoder_hidden_states=encoder_hidden_states,
temb=temb, temb=temb,
image_rotary_emb=image_rotary_emb, image_rotary_emb=image_rotary_emb,
cos_sin_cache=cos_sin_cache,
positions=positions,
) )
hidden_states = self.norm_out(hidden_states, temb) hidden_states = self.norm_out(hidden_states, temb)
@@ -14,7 +14,7 @@ from sglang.kernels.jit.benchmark.utils import (
from sglang.test.ci.ci_register import register_cuda_ci from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci( register_cuda_ci(
est_time=13, stage="base-b-kernel-benchmark", runner_config="1-gpu-large" est_time=15, stage="base-b-kernel-benchmark", runner_config="1-gpu-large"
) )
MAX_SEQ_LEN = 131072 MAX_SEQ_LEN = 131072
@@ -30,6 +30,8 @@ class CaseSpec:
head_dim: int head_dim: int
rope_dim: int rope_dim: int
is_neox: bool is_neox: bool
cache_has_full_width: bool = False
round_norm_before_rope: bool = False
BENCH_CASES = ( BENCH_CASES = (
@@ -38,6 +40,7 @@ BENCH_CASES = (
CaseSpec("qwen_image_partial", 1, 4096, 32, 128, 64, False), CaseSpec("qwen_image_partial", 1, 4096, 32, 128, 64, False),
# Z-Image-Turbo default 1024x1024 config: dim=3840, num_heads=30 -> head_dim=128. # Z-Image-Turbo default 1024x1024 config: dim=3840, num_heads=30 -> head_dim=128.
CaseSpec("zimage_1024", 1, 4096, 30, 128, 128, False), CaseSpec("zimage_1024", 1, 4096, 30, 128, 128, False),
CaseSpec("longcat_1024", 1, 4608, 24, 128, 128, False, True, True),
CaseSpec("batch2_medium", 2, 2048, 24, 128, 128, False), CaseSpec("batch2_medium", 2, 2048, 24, 128, 128, False),
) )
CASE_BY_NAME = {case.name: case for case in BENCH_CASES} CASE_BY_NAME = {case.name: case for case in BENCH_CASES}
@@ -46,7 +49,7 @@ CASE_NAMES = get_benchmark_range(
ci_range=[case.name for case in BENCH_CASES], ci_range=[case.name for case in BENCH_CASES],
) )
LINE_VALS = ["split", "fused"] LINE_VALS = ["split", "fused"]
LINE_NAMES = ["JIT QKNorm + FlashInfer RoPE", "SGL JIT Fused QKNorm+RoPE"] LINE_NAMES = ["Split QKNorm + RoPE", "SGL JIT Fused QKNorm+RoPE"]
STYLES = [("red", "-"), ("blue", "--")] STYLES = [("red", "-"), ("blue", "--")]
@@ -77,6 +80,13 @@ def make_inputs(case: CaseSpec) -> dict[str, torch.Tensor | bool]:
) )
generator = torch.Generator(device=DEFAULT_DEVICE) generator = torch.Generator(device=DEFAULT_DEVICE)
generator.manual_seed(seed) generator.manual_seed(seed)
cos_sin_cache = create_cos_sin_cache(case.rope_dim)
if case.cache_has_full_width:
cos, sin = cos_sin_cache.chunk(2, dim=-1)
cos_sin_cache = torch.cat(
(cos.repeat_interleave(2, dim=-1), sin.repeat_interleave(2, dim=-1)),
dim=-1,
).contiguous()
return { return {
"q": torch.randn( "q": torch.randn(
case.batch_size * case.num_tokens, case.batch_size * case.num_tokens,
@@ -114,8 +124,10 @@ def make_inputs(case: CaseSpec) -> dict[str, torch.Tensor | bool]:
dtype=torch.int64, dtype=torch.int64,
generator=generator, generator=generator,
), ),
"cos_sin_cache": create_cos_sin_cache(case.rope_dim), "cos_sin_cache": cos_sin_cache,
"is_neox": case.is_neox, "is_neox": case.is_neox,
"cache_has_full_width": case.cache_has_full_width,
"round_norm_before_rope": case.round_norm_before_rope,
} }
@@ -128,7 +140,9 @@ def clone_inputs(
return out return out
def split_qknorm_rope(inputs: dict[str, torch.Tensor | bool]) -> None: def split_qknorm_rope(
inputs: dict[str, torch.Tensor | bool],
) -> tuple[torch.Tensor, torch.Tensor] | None:
from flashinfer.rope import apply_rope_with_cos_sin_cache_inplace from flashinfer.rope import apply_rope_with_cos_sin_cache_inplace
from sglang.kernels.ops.layernorm.norm import fused_inplace_qknorm from sglang.kernels.ops.layernorm.norm import fused_inplace_qknorm
@@ -142,6 +156,18 @@ def split_qknorm_rope(inputs: dict[str, torch.Tensor | bool]) -> None:
is_neox = bool(inputs["is_neox"]) is_neox = bool(inputs["is_neox"])
fused_inplace_qknorm(q, k, q_weight, k_weight) fused_inplace_qknorm(q, k, q_weight, k_weight)
if inputs["cache_has_full_width"]:
cos, sin = cos_sin_cache.chunk(2, dim=-1)
cos = cos[positions]
sin = sin[positions]
def apply_interleaved(x: torch.Tensor) -> torch.Tensor:
x_real, x_imag = x.float().reshape(*x.shape[:-1], -1, 2).unbind(-1)
x_rotated = torch.stack((-x_imag, x_real), dim=-1).flatten(-2)
return (x.float() * cos[:, None] + x_rotated * sin[:, None]).to(x.dtype)
return apply_interleaved(q), apply_interleaved(k)
apply_rope_with_cos_sin_cache_inplace( apply_rope_with_cos_sin_cache_inplace(
positions=positions, positions=positions,
query=q.view(q.shape[0], -1), query=q.view(q.shape[0], -1),
@@ -163,7 +189,13 @@ def fused_qknorm_rope(inputs: dict[str, torch.Tensor | bool]) -> None:
inputs["cos_sin_cache"], inputs["cos_sin_cache"],
inputs["positions"], inputs["positions"],
is_neox=bool(inputs["is_neox"]), is_neox=bool(inputs["is_neox"]),
rope_dim=inputs["cos_sin_cache"].shape[-1], rope_dim=(
inputs["cos_sin_cache"].shape[-1] // 2
if inputs["cache_has_full_width"]
else inputs["cos_sin_cache"].shape[-1]
),
round_norm_before_rope=bool(inputs["round_norm_before_rope"]),
cache_has_full_width=bool(inputs["cache_has_full_width"]),
) )
@@ -34,6 +34,7 @@ import sglang.multimodal_gen.runtime.models.dits.ernie_image as ernie_image
import sglang.multimodal_gen.runtime.models.dits.flux as flux import sglang.multimodal_gen.runtime.models.dits.flux as flux
import sglang.multimodal_gen.runtime.models.dits.flux_2 as flux2 import sglang.multimodal_gen.runtime.models.dits.flux_2 as flux2
import sglang.multimodal_gen.runtime.models.dits.glm_image as glm_image import sglang.multimodal_gen.runtime.models.dits.glm_image as glm_image
import sglang.multimodal_gen.runtime.models.dits.longcat_image as longcat_image
import sglang.multimodal_gen.runtime.models.dits.ltx_2 as ltx2_module import sglang.multimodal_gen.runtime.models.dits.ltx_2 as ltx2_module
import sglang.multimodal_gen.runtime.models.dits.sana as sana import sglang.multimodal_gen.runtime.models.dits.sana as sana
from sglang.kernels.ops.diffusion import ( from sglang.kernels.ops.diffusion import (
@@ -56,7 +57,11 @@ from sglang.kernels.ops.diffusion.common.platform import is_cuda
from sglang.multimodal_gen.configs.models.vaes.stablediffusion3 import ( from sglang.multimodal_gen.configs.models.vaes.stablediffusion3 import (
StableDiffusion3VAEConfig, StableDiffusion3VAEConfig,
) )
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm, RMSNormNoWeight from sglang.multimodal_gen.runtime.layers.layernorm import (
RMSNorm,
RMSNormNoWeight,
apply_qk_norm,
)
from sglang.multimodal_gen.runtime.layers.rotary_embedding.utils import ( from sglang.multimodal_gen.runtime.layers.rotary_embedding.utils import (
_apply_rotary_emb, _apply_rotary_emb,
) )
@@ -85,6 +90,9 @@ from sglang.multimodal_gen.runtime.models.dits.hunyuanvideo import (
_hunyuan_pack_qkv, _hunyuan_pack_qkv,
_hunyuan_qknorm, _hunyuan_qknorm,
) )
from sglang.multimodal_gen.runtime.models.dits.longcat_image import (
_apply_longcat_qknorm_rope,
)
from sglang.multimodal_gen.runtime.models.dits.ltx_2 import _ltx2_rms_norm_modulate from sglang.multimodal_gen.runtime.models.dits.ltx_2 import _ltx2_rms_norm_modulate
from sglang.multimodal_gen.runtime.models.dits.sana import ( from sglang.multimodal_gen.runtime.models.dits.sana import (
_eager_ln_modulate as _sana_eager_ln_modulate, _eager_ln_modulate as _sana_eager_ln_modulate,
@@ -490,6 +498,54 @@ def test_ernie_qknorm_rope_first_attempt_exception_uses_pristine_inputs():
assert ernie_image._ERNIE_QKNORM_ROPE.disabled assert ernie_image._ERNIE_QKNORM_ROPE.disabled
# -------------------------------------------------------------------------
# LongCat-Image -- full-width interleaved QKNorm + RoPE
# -------------------------------------------------------------------------
@requires_inline_ptx
def test_longcat_qknorm_rope_is_bit_exact():
torch.manual_seed(3)
batch, seq, heads, head_dim = 2, 17, 24, 128
offset = 11
q = torch.randn(batch, seq, heads, head_dim, device="cuda", dtype=torch.bfloat16)
k = torch.randn_like(q)
q_norm = RMSNorm(head_dim, eps=1e-6).to(device="cuda", dtype=torch.bfloat16)
k_norm = RMSNorm(head_dim, eps=1e-6).to(device="cuda", dtype=torch.bfloat16)
with torch.no_grad():
q_norm.weight.copy_(torch.randn_like(q_norm.weight))
k_norm.weight.copy_(torch.randn_like(k_norm.weight))
cos = torch.randn(offset + seq, head_dim, device="cuda")
sin = torch.randn_like(cos)
image_rotary_emb = (cos[offset:], sin[offset:])
cache = torch.cat((cos, sin), dim=-1).contiguous()
positions = torch.arange(offset, offset + seq, device="cuda", dtype=torch.int64)
q_ref, k_ref = apply_qk_norm(q.clone(), k.clone(), q_norm, k_norm, head_dim)
q_ref = longcat_image.apply_rotary_emb(q_ref, image_rotary_emb, sequence_dim=1)
k_ref = longcat_image.apply_rotary_emb(k_ref, image_rotary_emb, sequence_dim=1)
q_fused, k_fused = q.clone(), k.clone()
q_out, k_out = _apply_longcat_qknorm_rope(
q_fused,
k_fused,
q_norm,
k_norm,
head_dim,
image_rotary_emb,
cache,
positions,
)
assert q_out.data_ptr() == q_fused.data_ptr()
assert k_out.data_ptr() == k_fused.data_ptr()
assert torch.equal(q_out, q_ref)
assert torch.equal(k_out, k_ref)
assert longcat_image._LONGCAT_QKNORM_ROPE.verified
assert not longcat_image._LONGCAT_QKNORM_ROPE.disabled
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# LTX-2 -- weightless RMSNorm + modulate (quality-gated) # LTX-2 -- weightless RMSNorm + modulate (quality-gated)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -7,7 +7,8 @@ Two families with different oracles:
sgl_kernel RoPE). In the default mode the two differ by about one bf16 sgl_kernel RoPE). In the default mode the two differ by about one bf16
rounding step, so those cases use a tolerance; with rounding step, so those cases use a tolerance; with
``round_norm_before_rope=True`` the fused kernel reproduces the split ``round_norm_before_rope=True`` the fused kernel reproduces the split
rounding exactly and ``torch.equal`` applies. rounding exactly and ``torch.equal`` applies. Full-width interleaved caches
use the Diffusers float32 RoPE chain as their oracle.
The LTX-2 split-RoPE kernel lives in ``test_rope_ltx2.py``: it is validated on The LTX-2 split-RoPE kernel lives in ``test_rope_ltx2.py``: it is validated on
B200 and registered on that lane alone, which the cases here cannot share -- B200 and registered on that lane alone, which the cases here cannot share --
their oracle is the *split* baseline (a separate qknorm kernel plus sgl_kernel their oracle is the *split* baseline (a separate qknorm kernel plus sgl_kernel
@@ -270,6 +271,51 @@ def test_qknorm_rope_preserves_full_width_neox_cache() -> None:
assert torch.equal(k, k_ref) assert torch.equal(k, k_ref)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_qknorm_rope_preserves_full_width_interleaved_cache(
dtype: torch.dtype,
) -> None:
from sglang.kernels.ops.layernorm.norm import fused_inplace_qknorm
num_tokens, num_heads, head_dim = 257, 24, 128
q = torch.randn(num_tokens, num_heads, head_dim, device=DEVICE, dtype=dtype)
k = torch.randn_like(q)
q_weight = torch.randn(head_dim, device=DEVICE, dtype=dtype)
k_weight = torch.randn(head_dim, device=DEVICE, dtype=dtype)
positions = torch.randperm(num_tokens, device=DEVICE, dtype=torch.int64)
cos = torch.randn(num_tokens, head_dim, device=DEVICE)
sin = torch.randn_like(cos)
cache = torch.cat((cos, sin), dim=-1).contiguous()
def apply_interleaved_rope(x: torch.Tensor) -> torch.Tensor:
x_real, x_imag = x.float().reshape(*x.shape[:-1], -1, 2).unbind(-1)
x_rotated = torch.stack((-x_imag, x_real), dim=-1).flatten(-2)
selected_cos = cos[positions, None]
selected_sin = sin[positions, None]
return (x.float() * selected_cos + x_rotated * selected_sin).to(dtype)
q_ref, k_ref = q.clone(), k.clone()
fused_inplace_qknorm(q_ref, k_ref, q_weight, k_weight, eps=1e-6)
q_ref = apply_interleaved_rope(q_ref)
k_ref = apply_interleaved_rope(k_ref)
fused_inplace_qknorm_rope(
q,
k,
q_weight,
k_weight,
cache,
positions,
is_neox=False,
eps=1e-6,
round_norm_before_rope=True,
cache_has_full_width=True,
)
assert torch.equal(q, q_ref)
assert torch.equal(k, k_ref)
def test_qknorm_rope_requires_opt_in_for_strided_packed_gqa() -> None: def test_qknorm_rope_requires_opt_in_for_strided_packed_gqa() -> None:
from sglang.multimodal_gen.runtime.layers.layernorm import ( from sglang.multimodal_gen.runtime.layers.layernorm import (
RMSNorm, RMSNorm,