[diffusion] perf: apply mul add fusion for Qwen-Image (#16299)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
陈一涵
2026-01-28 09:40:13 +08:00
committed by GitHub
co-authored by gemini-code-assist[bot]
parent 32ea7bcdd8
commit 647428d8d6
7 changed files with 72 additions and 51 deletions
@@ -0,0 +1,35 @@
import torch
from sglang.multimodal_gen.runtime.layers.custom_op import CustomOp
from sglang.multimodal_gen.runtime.layers.triton_ops import fuse_scale_shift_kernel
class MulAdd(CustomOp):
"""
Fuse elementwise mul and add
Input: a, b, c, OptionalInt[k]
Output: a * (k + b) + c
"""
def __init__(self, prefix: str = ""):
super().__init__()
def forward_native(
self, a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, k: int = 0
) -> torch.Tensor:
# a.shape: [batch_size, seq_len, inner_dim]
if b.dim() == 4:
# b.shape: [batch_size, num_frames, 1, inner_dim]
num_frames = b.shape[1]
frame_seqlen = a.shape[1] // num_frames
return c + (
a.unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * b
).flatten(1, 2)
else:
# b.shape: [batch_size, 1, inner_dim]
return c + a * (k + b)
def forward_cuda(
self, a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, k: int = 0
):
return fuse_scale_shift_kernel(a, b, c, scale_constant=k)
@@ -238,31 +238,6 @@ class LayerNorm(CustomOp):
return s return s
class ScaleResidual(nn.Module):
"""
Applies gated residual connection.
"""
def __init__(self, prefix: str = ""):
super().__init__()
def forward(
self, residual: torch.Tensor, x: torch.Tensor, gate: torch.Tensor
) -> torch.Tensor:
"""Apply gated residual connection."""
# x.shape: [batch_size, seq_len, inner_dim]
if gate.dim() == 4:
# gate.shape: [batch_size, num_frames, 1, inner_dim]
num_frames = gate.shape[1]
frame_seqlen = x.shape[1] // num_frames
return residual + (
x.unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * gate
).flatten(1, 2)
else:
# gate.shape: [batch_size, 1, inner_dim]
return residual + x * gate
# adapted from Diffusers: https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/normalization.py # adapted from Diffusers: https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/normalization.py
# NOTE(will): Needed to match behavior of diffusers and wan2.1 even while using # NOTE(will): Needed to match behavior of diffusers and wan2.1 even while using
# FSDP's MixedPrecisionPolicy # FSDP's MixedPrecisionPolicy
@@ -25,6 +25,7 @@ def _fused_scale_shift_4d_kernel(
normalized_ptr, normalized_ptr,
scale_ptr, scale_ptr,
shift_ptr, shift_ptr,
scale_constant: tl.constexpr, # scale_constant is either 0 or 1.
rows, rows,
inner_dim, inner_dim,
seq_len, seq_len,
@@ -56,8 +57,8 @@ def _fused_scale_shift_4d_kernel(
scale = tl.load(scale_ptrs, mask=mask, other=0.0) scale = tl.load(scale_ptrs, mask=mask, other=0.0)
shift = tl.load(shift_ptrs, mask=mask, other=0.0) shift = tl.load(shift_ptrs, mask=mask, other=0.0)
one = tl.full([BLOCK_N], 1.0, dtype=scale.dtype) scale_const_tensor = tl.full([BLOCK_N], scale_constant, dtype=scale.dtype)
output = normalized * (one + scale) + shift output = normalized * (scale_const_tensor + scale) + shift
tl.store(out_ptrs, output, mask=mask) tl.store(out_ptrs, output, mask=mask)
@@ -67,6 +68,7 @@ def fuse_scale_shift_kernel_blc_opt(
x_ptr, x_ptr,
shift_ptr, shift_ptr,
scale_ptr, scale_ptr,
scale_constant: tl.constexpr, # scale_constant is either 0 or 1.,
y_ptr, y_ptr,
B, B,
L, L,
@@ -125,7 +127,7 @@ def fuse_scale_shift_kernel_blc_opt(
) )
scale = tl.load(scale_ptr + sc_off, mask=mask, other=0) scale = tl.load(scale_ptr + sc_off, mask=mask, other=0)
y = x * (1 + scale) + shift y = x * (scale_constant + scale) + shift
tl.store(y_ptr + x_off, y, mask=mask) tl.store(y_ptr + x_off, y, mask=mask)
@@ -221,6 +223,7 @@ def fuse_scale_shift_kernel(
x: torch.Tensor, x: torch.Tensor,
scale: torch.Tensor, scale: torch.Tensor,
shift: torch.Tensor, shift: torch.Tensor,
scale_constant: float = 1.0,
block_l: int = 128, block_l: int = 128,
block_c: int = 128, block_c: int = 128,
): ):
@@ -251,6 +254,7 @@ def fuse_scale_shift_kernel(
x_2d, x_2d,
scale_reshaped, scale_reshaped,
shift_reshaped, shift_reshaped,
scale_constant,
rows, rows,
C, C,
L, L,
@@ -306,6 +310,7 @@ def fuse_scale_shift_kernel(
x, x,
shift_blc if need_shift_scalar else shift_exp, shift_blc if need_shift_scalar else shift_exp,
scale_blc if need_scale_scalar else scale_exp, scale_blc if need_scale_scalar else scale_exp,
scale_constant,
output, output,
B, B,
L, L,
@@ -26,11 +26,11 @@ import torch.distributed as dist
from sglang.multimodal_gen.configs.models.dits import WanVideoConfig from sglang.multimodal_gen.configs.models.dits import WanVideoConfig
from sglang.multimodal_gen.runtime.distributed.parallel_state import get_sp_world_size from sglang.multimodal_gen.runtime.distributed.parallel_state import get_sp_world_size
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention from sglang.multimodal_gen.runtime.layers.attention import LocalAttention
from sglang.multimodal_gen.runtime.layers.elementwise import MulAdd
from sglang.multimodal_gen.runtime.layers.layernorm import ( from sglang.multimodal_gen.runtime.layers.layernorm import (
FP32LayerNorm, FP32LayerNorm,
LayerNormScaleShift, LayerNormScaleShift,
RMSNorm, RMSNorm,
ScaleResidual,
ScaleResidualLayerNormScaleShift, ScaleResidualLayerNormScaleShift,
) )
from sglang.multimodal_gen.runtime.layers.linear import ReplicatedLinear from sglang.multimodal_gen.runtime.layers.linear import ReplicatedLinear
@@ -318,7 +318,7 @@ class CausalWanTransformerBlock(nn.Module):
# 3. Feed-forward # 3. Feed-forward
self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh") self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh")
self.mlp_residual = ScaleResidual() self.mlp_residual = MulAdd()
self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5) self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
@@ -417,7 +417,7 @@ class CausalWanTransformerBlock(nn.Module):
# 3. Feed-forward # 3. Feed-forward
ff_output = self.ffn(norm_hidden_states) ff_output = self.ffn(norm_hidden_states)
hidden_states = self.mlp_residual(hidden_states, ff_output, c_gate_msa) hidden_states = self.mlp_residual(ff_output, c_gate_msa, hidden_states)
hidden_states = hidden_states.to(orig_dtype) hidden_states = hidden_states.to(orig_dtype)
return hidden_states return hidden_states
@@ -15,10 +15,10 @@ from sglang.multimodal_gen.runtime.layers.attention import (
LocalAttention, LocalAttention,
UlyssesAttention, UlyssesAttention,
) )
from sglang.multimodal_gen.runtime.layers.elementwise import MulAdd
from sglang.multimodal_gen.runtime.layers.layernorm import ( from sglang.multimodal_gen.runtime.layers.layernorm import (
LayerNormScaleShift, LayerNormScaleShift,
RMSNorm, RMSNorm,
ScaleResidual,
ScaleResidualLayerNormScaleShift, ScaleResidualLayerNormScaleShift,
) )
from sglang.multimodal_gen.runtime.layers.linear import ReplicatedLinear from sglang.multimodal_gen.runtime.layers.linear import ReplicatedLinear
@@ -81,7 +81,7 @@ class MMDoubleStreamBlock(nn.Module):
self.img_attn_residual_mlp_norm = ScaleResidualLayerNormScaleShift( self.img_attn_residual_mlp_norm = ScaleResidualLayerNormScaleShift(
hidden_size, norm_type="layer", elementwise_affine=False, dtype=dtype hidden_size, norm_type="layer", elementwise_affine=False, dtype=dtype
) )
self.img_mlp_residual = ScaleResidual() self.img_mlp_residual = MulAdd()
# Image attention components # Image attention components
self.img_attn_qkv = ReplicatedLinear( self.img_attn_qkv = ReplicatedLinear(
@@ -127,7 +127,7 @@ class MMDoubleStreamBlock(nn.Module):
self.txt_attn_residual_mlp_norm = ScaleResidualLayerNormScaleShift( self.txt_attn_residual_mlp_norm = ScaleResidualLayerNormScaleShift(
hidden_size, norm_type="layer", elementwise_affine=False, dtype=dtype hidden_size, norm_type="layer", elementwise_affine=False, dtype=dtype
) )
self.txt_mlp_residual = ScaleResidual() self.txt_mlp_residual = MulAdd()
# Text attention components # Text attention components
self.txt_attn_qkv = ReplicatedLinear( self.txt_attn_qkv = ReplicatedLinear(
@@ -231,7 +231,7 @@ class MMDoubleStreamBlock(nn.Module):
# Process image MLP # Process image MLP
img_mlp_out = self.img_mlp(img_mlp_input) img_mlp_out = self.img_mlp(img_mlp_input)
img = self.img_mlp_residual(img_residual, img_mlp_out, img_mlp_gate) img = self.img_mlp_residual(img_mlp_out, img_mlp_gate, img_residual)
# Process text attention output # Process text attention output
txt_attn_out, _ = self.txt_attn_proj( txt_attn_out, _ = self.txt_attn_proj(
@@ -245,7 +245,7 @@ class MMDoubleStreamBlock(nn.Module):
# Process text MLP # Process text MLP
txt_mlp_out = self.txt_mlp(txt_mlp_input) txt_mlp_out = self.txt_mlp(txt_mlp_input)
txt = self.txt_mlp_residual(txt_residual, txt_mlp_out, txt_mlp_gate) txt = self.txt_mlp_residual(txt_mlp_out, txt_mlp_gate, txt_residual)
return img, txt return img, txt
@@ -304,7 +304,7 @@ class MMSingleStreamBlock(nn.Module):
elementwise_affine=False, elementwise_affine=False,
dtype=dtype, dtype=dtype,
) )
self.output_residual = ScaleResidual() self.output_residual = MulAdd()
# Activation function # Activation function
self.mlp_act = nn.GELU(approximate="tanh") self.mlp_act = nn.GELU(approximate="tanh")
@@ -384,7 +384,7 @@ class MMSingleStreamBlock(nn.Module):
output, _ = self.linear2(combined) output, _ = self.linear2(combined)
# Apply residual connection with gating using fused operation # Apply residual connection with gating using fused operation
return self.output_residual(x, output, mod_gate) return self.output_residual(output, mod_gate, x)
class HunyuanVideoTransformer3DModel(CachableDiT, OffloadableDiTMixin): class HunyuanVideoTransformer3DModel(CachableDiT, OffloadableDiTMixin):
@@ -17,6 +17,7 @@ from diffusers.models.normalization import AdaLayerNormContinuous
from sglang.multimodal_gen.configs.models.dits.qwenimage import QwenImageDitConfig from sglang.multimodal_gen.configs.models.dits.qwenimage import QwenImageDitConfig
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
from sglang.multimodal_gen.runtime.layers.attention import USPAttention from sglang.multimodal_gen.runtime.layers.attention import USPAttention
from sglang.multimodal_gen.runtime.layers.elementwise import MulAdd
from sglang.multimodal_gen.runtime.layers.layernorm import ( from sglang.multimodal_gen.runtime.layers.layernorm import (
LayerNorm, LayerNorm,
RMSNorm, RMSNorm,
@@ -28,7 +29,6 @@ from sglang.multimodal_gen.runtime.layers.rotary_embedding import (
) )
from sglang.multimodal_gen.runtime.layers.triton_ops import ( from sglang.multimodal_gen.runtime.layers.triton_ops import (
fuse_scale_shift_gate_select01_kernel, fuse_scale_shift_gate_select01_kernel,
fuse_scale_shift_kernel,
) )
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.platforms import ( from sglang.multimodal_gen.runtime.platforms import (
@@ -350,7 +350,7 @@ class QwenEmbedLayer3DRope(nn.Module):
if idx != layer_num: if idx != layer_num:
video_freq = self._compute_video_freqs(frame, height, width, idx) video_freq = self._compute_video_freqs(frame, height, width, idx)
else: else:
### For the condition image, we set the layer index to -1 # For the condition image, we set the layer index to -1
video_freq = self._compute_condition_freqs(frame, height, width) video_freq = self._compute_condition_freqs(frame, height, width)
video_freq = video_freq.to(device) video_freq = video_freq.to(device)
vid_freqs.append(video_freq) vid_freqs.append(video_freq)
@@ -673,6 +673,8 @@ class QwenImageTransformerBlock(nn.Module):
self.txt_mlp = FeedForward( self.txt_mlp = FeedForward(
dim=dim, dim_out=dim, activation_fn="gelu-approximate" dim=dim, dim_out=dim, activation_fn="gelu-approximate"
) )
# Utils
self.fuse_mul_add = MulAdd()
def _modulate(self, x, mod_params, index=None): def _modulate(self, x, mod_params, index=None):
shift, scale, gate = mod_params.chunk(3, dim=-1) shift, scale, gate = mod_params.chunk(3, dim=-1)
@@ -714,14 +716,14 @@ class QwenImageTransformerBlock(nn.Module):
) )
gate_result = torch.where(mask, gate0.unsqueeze(1), gate1.unsqueeze(1)) gate_result = torch.where(mask, gate0.unsqueeze(1), gate1.unsqueeze(1))
return ( return (
fuse_scale_shift_kernel(x, scale_result, shift_result), self.fuse_mul_add(x, scale_result, shift_result, k=1.0),
gate_result, gate_result,
) )
else: else:
shift_result = shift.unsqueeze(1) shift_result = shift.unsqueeze(1)
scale_result = scale.unsqueeze(1) scale_result = scale.unsqueeze(1)
gate_result = gate.unsqueeze(1) gate_result = gate.unsqueeze(1)
return fuse_scale_shift_kernel(x, scale_result, shift_result), gate_result return self.fuse_mul_add(x, scale_result, shift_result, k=1.0), gate_result
def forward( def forward(
self, self,
@@ -759,8 +761,10 @@ class QwenImageTransformerBlock(nn.Module):
# 4. Splits results back to separate streams # 4. Splits results back to separate streams
joint_attention_kwargs = joint_attention_kwargs or {} joint_attention_kwargs = joint_attention_kwargs or {}
attn_output = self.attn( attn_output = self.attn(
hidden_states=img_modulated, # Image stream (will be processed as "sample") # Image stream (will be processed as "sample")
encoder_hidden_states=txt_modulated, # Text stream (will be processed as "context") hidden_states=img_modulated,
# Text stream (will be processed as "context")
encoder_hidden_states=txt_modulated,
encoder_hidden_states_mask=encoder_hidden_states_mask, encoder_hidden_states_mask=encoder_hidden_states_mask,
image_rotary_emb=image_rotary_emb, image_rotary_emb=image_rotary_emb,
**joint_attention_kwargs, **joint_attention_kwargs,
@@ -780,13 +784,15 @@ class QwenImageTransformerBlock(nn.Module):
img_normed2, img_mod2, modulate_index img_normed2, img_mod2, modulate_index
) )
img_mlp_output = self.img_mlp(img_modulated2) img_mlp_output = self.img_mlp(img_modulated2)
hidden_states = hidden_states + img_gate2 * img_mlp_output hidden_states = self.fuse_mul_add(img_mlp_output, img_gate2, hidden_states)
# Process text stream - norm2 + MLP # Process text stream - norm2 + MLP
txt_normed2 = self.txt_norm2(encoder_hidden_states) txt_normed2 = self.txt_norm2(encoder_hidden_states)
txt_modulated2, txt_gate2 = self._modulate(txt_normed2, txt_mod2) txt_modulated2, txt_gate2 = self._modulate(txt_normed2, txt_mod2)
txt_mlp_output = self.txt_mlp(txt_modulated2) txt_mlp_output = self.txt_mlp(txt_modulated2)
encoder_hidden_states = encoder_hidden_states + txt_gate2 * txt_mlp_output encoder_hidden_states = self.fuse_mul_add(
txt_mlp_output, txt_gate2, encoder_hidden_states
)
# Clip to prevent overflow for fp16 # Clip to prevent overflow for fp16
if encoder_hidden_states.dtype == torch.float16: if encoder_hidden_states.dtype == torch.float16:
@@ -20,11 +20,11 @@ from sglang.multimodal_gen.runtime.layers.attention import (
UlyssesAttention_VSA, UlyssesAttention_VSA,
USPAttention, USPAttention,
) )
from sglang.multimodal_gen.runtime.layers.elementwise import MulAdd
from sglang.multimodal_gen.runtime.layers.layernorm import ( from sglang.multimodal_gen.runtime.layers.layernorm import (
FP32LayerNorm, FP32LayerNorm,
LayerNormScaleShift, LayerNormScaleShift,
RMSNorm, RMSNorm,
ScaleResidual,
ScaleResidualLayerNormScaleShift, ScaleResidualLayerNormScaleShift,
tensor_parallel_rms_norm, tensor_parallel_rms_norm,
) )
@@ -382,7 +382,7 @@ class WanTransformerBlock(nn.Module):
# 3. Feed-forward # 3. Feed-forward
self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh") self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh")
self.mlp_residual = ScaleResidual() self.mlp_residual = MulAdd()
self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5) self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
@@ -488,7 +488,7 @@ class WanTransformerBlock(nn.Module):
# 3. Feed-forward # 3. Feed-forward
ff_output = self.ffn(norm_hidden_states) ff_output = self.ffn(norm_hidden_states)
hidden_states = self.mlp_residual(hidden_states, ff_output, c_gate_msa) hidden_states = self.mlp_residual(ff_output, c_gate_msa, hidden_states)
hidden_states = hidden_states.to(orig_dtype) hidden_states = hidden_states.to(orig_dtype)
return hidden_states return hidden_states
@@ -582,7 +582,7 @@ class WanTransformerBlock_VSA(nn.Module):
# 3. Feed-forward # 3. Feed-forward
self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh") self.ffn = MLP(dim, ffn_dim, act_type="gelu_pytorch_tanh")
self.mlp_residual = ScaleResidual() self.mlp_residual = MulAdd()
self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5) self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
@@ -669,7 +669,7 @@ class WanTransformerBlock_VSA(nn.Module):
# 3. Feed-forward # 3. Feed-forward
ff_output = self.ffn(norm_hidden_states) ff_output = self.ffn(norm_hidden_states)
hidden_states = self.mlp_residual(hidden_states, ff_output, c_gate_msa) hidden_states = self.mlp_residual(ff_output, c_gate_msa, hidden_states)
hidden_states = hidden_states.to(orig_dtype) hidden_states = hidden_states.to(orig_dtype)
return hidden_states return hidden_states