[diffusion] fix: fix z-Image accuracy (#29742)

Co-authored-by: Mick <mickjagger19@icloud.com>
This commit is contained in:
Chi McIsaac
2026-07-08 09:08:32 +08:00
committed by GitHub
co-authored by Mick
parent d7dcdf3efd
commit fa185ed84d
11 changed files with 1065 additions and 145 deletions
@@ -0,0 +1,182 @@
import torch
import triton # type: ignore
import triton.language as tl # type: ignore
@triton.jit
def _tanh(x):
return 2.0 / (1.0 + tl.exp(-2.0 * x)) - 1.0
@triton.jit
def _rmsnorm_scale_kernel(
y_ptr,
x_ptr,
weight_ptr,
scale_ptr,
x_row_stride,
scale_row_stride,
seq_len,
dim: tl.constexpr,
eps: tl.constexpr,
block_dim: tl.constexpr,
):
row = tl.program_id(0)
offsets = tl.arange(0, block_dim)
mask = offsets < dim
x = tl.load(x_ptr + row * x_row_stride + offsets, mask=mask, other=0.0)
square = (x * x).to(tl.bfloat16)
mean_square = (tl.sum(square, axis=0) / dim).to(tl.bfloat16)
rstd = tl.rsqrt((mean_square + eps).to(tl.bfloat16).to(tl.float32)).to(tl.bfloat16)
batch = row // seq_len
weight = tl.load(weight_ptr + offsets, mask=mask, other=0.0)
scale = tl.load(
scale_ptr + batch * scale_row_stride + offsets, mask=mask, other=0.0
)
y = (((x * rstd).to(tl.bfloat16) * weight).to(tl.bfloat16) * scale).to(tl.bfloat16)
tl.store(y_ptr + row * dim + offsets, y, mask=mask)
@triton.jit
def _rmsnorm_tanh_residual_kernel(
y_ptr,
x_ptr,
gate_ptr,
residual_ptr,
weight_ptr,
x_row_stride,
gate_row_stride,
residual_row_stride,
seq_len,
dim: tl.constexpr,
eps: tl.constexpr,
block_dim: tl.constexpr,
):
row = tl.program_id(0)
offsets = tl.arange(0, block_dim)
mask = offsets < dim
x = tl.load(x_ptr + row * x_row_stride + offsets, mask=mask, other=0.0)
square = (x * x).to(tl.bfloat16)
mean_square = (tl.sum(square, axis=0) / dim).to(tl.bfloat16)
rstd = tl.rsqrt((mean_square + eps).to(tl.bfloat16).to(tl.float32)).to(tl.bfloat16)
batch = row // seq_len
gate = tl.load(gate_ptr + batch * gate_row_stride + offsets, mask=mask, other=0.0)
residual = tl.load(
residual_ptr + row * residual_row_stride + offsets, mask=mask, other=0.0
)
weight = tl.load(weight_ptr + offsets, mask=mask, other=0.0)
norm = ((x * rstd).to(tl.bfloat16) * weight).to(tl.bfloat16)
gated = (_tanh(gate.to(tl.float32)).to(tl.bfloat16) * norm).to(tl.bfloat16)
y = (residual + gated).to(tl.bfloat16)
tl.store(y_ptr + row * dim + offsets, y, mask=mask)
def _flat_row_stride(x: torch.Tensor) -> int | None:
if x.dim() < 2 or x.stride(-1) != 1:
return None
row_stride = x.stride(-2)
expected_stride = row_stride * x.shape[-2]
for dim in range(x.dim() - 3, -1, -1):
if x.stride(dim) != expected_stride:
return None
expected_stride *= x.shape[dim]
return row_stride
def _can_use(x: torch.Tensor, weight: torch.Tensor, other: torch.Tensor) -> bool:
return (
x.is_cuda
and weight.is_cuda
and other.is_cuda
and x.dtype == torch.bfloat16
and weight.dtype == torch.bfloat16
and other.dtype == torch.bfloat16
and weight.is_contiguous()
and x.shape[-1] <= 8192
and _flat_row_stride(x) is not None
and _flat_row_stride(other) is not None
)
def zimage_rmsnorm_scale(
x: torch.Tensor,
weight: torch.Tensor,
scale: torch.Tensor,
eps: float,
) -> torch.Tensor | None:
if not _can_use(x, weight, scale):
return None
shape = x.shape
dim = shape[-1]
x_rows = x.numel() // dim
scale_rows = scale.numel() // dim
if x_rows % scale_rows != 0:
return None
seq_len = x_rows // scale_rows
x_row_stride = _flat_row_stride(x)
scale_row_stride = _flat_row_stride(scale)
if x_row_stride is None or scale_row_stride is None:
return None
y = torch.empty_like(x, memory_format=torch.contiguous_format)
with torch.get_device_module().device(x.device):
_rmsnorm_scale_kernel[(x_rows,)](
y.reshape(-1, dim),
x,
weight,
scale,
x_row_stride,
scale_row_stride,
seq_len,
dim,
eps,
block_dim=triton.next_power_of_2(dim),
num_warps=8,
)
return y
def zimage_rmsnorm_tanh_residual(
x: torch.Tensor,
gate: torch.Tensor,
residual: torch.Tensor,
weight: torch.Tensor,
eps: float,
) -> torch.Tensor | None:
if not (_can_use(x, weight, gate) and residual.is_cuda):
return None
if residual.dtype != x.dtype or _flat_row_stride(residual) is None:
return None
shape = x.shape
dim = shape[-1]
x_rows = x.numel() // dim
gate_rows = gate.numel() // dim
if x_rows % gate_rows != 0:
return None
seq_len = x_rows // gate_rows
x_row_stride = _flat_row_stride(x)
gate_row_stride = _flat_row_stride(gate)
residual_row_stride = _flat_row_stride(residual)
if x_row_stride is None or gate_row_stride is None or residual_row_stride is None:
return None
y = torch.empty_like(x, memory_format=torch.contiguous_format)
with torch.get_device_module().device(x.device):
_rmsnorm_tanh_residual_kernel[(x_rows,)](
y.reshape(-1, dim),
x,
gate,
residual,
weight,
x_row_stride,
gate_row_stride,
residual_row_stride,
seq_len,
dim,
eps,
block_dim=triton.next_power_of_2(dim),
num_warps=8,
)
return y
@@ -67,6 +67,7 @@ class ZImagePipelineConfig(ZImageRolloutPipelineMixin, ImagePipelineConfig):
task_type: ModelTaskType = ModelTaskType.T2I
dit_config: DiTConfig = field(default_factory=ZImageDitConfig)
vae_config: VAEConfig = field(default_factory=FluxVAEConfig)
enable_autocast: bool = False
vae_precision: str = "bf16"
text_encoder_precisions: tuple[str, ...] = field(default_factory=lambda: ("bf16",))
text_encoder_configs: tuple[EncoderConfig, ...] = field(
@@ -87,6 +88,9 @@ class ZImagePipelineConfig(ZImageRolloutPipelineMixin, ImagePipelineConfig):
def get_model_deployment_config(self) -> ModelDeploymentConfig:
return ModelDeploymentConfig(fsdp_auto_min_available_memory_gb=40)
def prepare_sigmas(self, sigmas, num_inference_steps):
return self._prepare_sigmas(sigmas, num_inference_steps)
def tokenize_prompt(self, prompts: list[str], tokenizer, tok_kwargs) -> dict:
rendered_prompts = [
tokenizer.apply_chat_template(
@@ -13,6 +13,8 @@ from sglang.multimodal_gen.runtime.layers.attention.layer import (
UlyssesAttention_VSA,
USPAttention,
build_varlen_mask_meta,
build_varlen_mask_meta_from_lengths,
build_varlen_mask_meta_from_ranges,
)
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend
from sglang.multimodal_gen.runtime.layers.attention.turbo_layer import MinimalA2AAttnOp
@@ -29,4 +31,6 @@ __all__ = [
# "AttentionState",
"get_attn_backend",
"build_varlen_mask_meta",
"build_varlen_mask_meta_from_lengths",
"build_varlen_mask_meta_from_ranges",
]
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: Apache-2.0
import os
from collections.abc import Sequence
from contextlib import nullcontext
from typing import Type
@@ -92,6 +93,84 @@ def build_varlen_mask_meta(
}
def build_varlen_mask_meta_from_lengths(
lengths: Sequence[int],
max_seqlen: int,
device: torch.device,
) -> dict:
"""Build varlen FA metadata for prefix-valid masks without CUDA nonzero.
This is equivalent to ``build_varlen_mask_meta`` for masks where row ``i`` is
true on ``[:lengths[i]]`` and false afterwards. Keeping the lengths on the
host lets callers avoid a GPU ``nonzero``/dynamic-shape path while still
producing the same packed indices.
"""
return build_varlen_mask_meta_from_ranges(
[[(0, int(length))] for length in lengths],
max_seqlen=max_seqlen,
device=device,
)
def build_varlen_mask_meta_from_ranges(
valid_ranges: Sequence[Sequence[tuple[int, int]]],
max_seqlen: int,
device: torch.device,
) -> dict:
"""Build varlen FA metadata from host-side valid token ranges.
``valid_ranges[i]`` contains half-open intervals in row-local coordinates.
The intervals are packed in the provided order, matching the flattened
``nonzero`` order for ordinary left-to-right masks.
"""
range_values = [
[(int(start), int(end)) for start, end in row_ranges]
for row_ranges in valid_ranges
]
if any(
start < 0 or end < start or end > max_seqlen
for row_ranges in range_values
for start, end in row_ranges
):
raise ValueError(
f"All ranges must be within [0, {max_seqlen}], got {range_values}"
)
bs = len(range_values)
length_values = [
sum(end - start for start, end in row_ranges) for row_ranges in range_values
]
valid_lens = torch.as_tensor(length_values, dtype=torch.int32, device=device)
cu_seqlens = torch.zeros(bs + 1, dtype=torch.int32, device=device)
cu_seqlens[1:] = torch.cumsum(valid_lens, dim=0)
index_parts = [
torch.arange(
row * max_seqlen + start,
row * max_seqlen + end,
dtype=torch.long,
device=device,
)
for row, row_ranges in enumerate(range_values)
for start, end in row_ranges
if end > start
]
if index_parts:
indices = torch.cat(index_parts, dim=0)
else:
indices = torch.empty((0,), dtype=torch.long, device=device)
inv_indices = build_inv_indices(indices, bs * max_seqlen)
return {
"cu_seqlens": cu_seqlens,
"indices": indices,
"inv_indices": inv_indices,
"max_seqlen": max_seqlen,
}
class UlyssesAttention(nn.Module):
"""Ulysses-style SequenceParallelism attention layer."""
@@ -18,11 +18,11 @@ from sglang.multimodal_gen.runtime.layers.activation import SiluAndMul
from sglang.multimodal_gen.runtime.layers.attention import (
UlyssesAttention,
USPAttention,
build_varlen_mask_meta_from_lengths,
build_varlen_mask_meta_from_ranges,
)
from sglang.multimodal_gen.runtime.layers.layernorm import (
RMSNorm,
apply_qk_norm_with_optional_rope,
apply_rmsnorm_tanh_mul_add,
)
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
@@ -60,6 +60,73 @@ ADALN_EMBED_DIM = 256
SEQ_MULTI_OF = 32
class ZImageRMSNorm(nn.Module):
"""RMSNorm that preserves Z-Image's native bf16 behavior.
Z-Image does not upcast hidden states to fp32 for RMSNorm.
"""
def __init__(self, dim: int, eps: float = 1e-5):
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.variance_epsilon = eps
self.hidden_size = dim
def forward(self, x: torch.Tensor) -> torch.Tensor:
orig_dtype = x.dtype
output = x * torch.rsqrt(
x.pow(2).mean(dim=-1, keepdim=True) + self.variance_epsilon
)
output = output * self.weight.to(device=x.device, dtype=x.dtype)
return output.to(orig_dtype)
def zimage_rmsnorm_tanh_mul_add(
x: torch.Tensor,
gate: torch.Tensor,
residual: torch.Tensor,
norm: ZImageRMSNorm,
enable_fused: bool = True,
) -> torch.Tensor:
if enable_fused:
from sglang.jit_kernel.diffusion.triton.zimage_native_norm import (
zimage_rmsnorm_tanh_residual,
)
y = zimage_rmsnorm_tanh_residual(
x,
gate,
residual,
norm.weight.data.to(device=x.device, dtype=x.dtype).contiguous(),
norm.variance_epsilon,
)
if y is not None:
return y
return residual + torch.tanh(gate) * norm(x)
def zimage_rmsnorm_scale(
x: torch.Tensor,
scale: torch.Tensor,
norm: ZImageRMSNorm,
enable_fused: bool = True,
) -> torch.Tensor:
if enable_fused:
from sglang.jit_kernel.diffusion.triton.zimage_native_norm import (
zimage_rmsnorm_scale as fused_zimage_rmsnorm_scale,
)
y = fused_zimage_rmsnorm_scale(
x,
norm.weight.data.to(device=x.device, dtype=x.dtype).contiguous(),
scale,
norm.variance_epsilon,
)
if y is not None:
return y
return norm(x) * scale
class SelectFirstElement(nn.Module):
def __init__(self):
super().__init__()
@@ -167,6 +234,7 @@ class ZImageAttention(nn.Module):
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads
self.qk_norm = qk_norm
self.enable_zimage_qk_fusion = quant_config is None
tp_size = get_tp_world_size()
assert (
@@ -217,8 +285,8 @@ class ZImageAttention(nn.Module):
)
if self.qk_norm:
self.norm_q = RMSNorm(self.head_dim, eps=eps)
self.norm_k = RMSNorm(self.head_dim, eps=eps)
self.norm_q = ZImageRMSNorm(self.head_dim, eps=eps)
self.norm_k = ZImageRMSNorm(self.head_dim, eps=eps)
else:
self.norm_q = None
self.norm_k = None
@@ -256,6 +324,10 @@ class ZImageAttention(nn.Module):
self,
hidden_states: torch.Tensor,
freqs_cis: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
rope_cos_sin_cache: Optional[torch.Tensor] = None,
rope_positions: Optional[torch.Tensor] = None,
attn_mask: Optional[torch.Tensor] = None,
attn_mask_meta: Optional[dict] = None,
num_replicated_prefix: int = 0,
num_replicated_suffix: int = 0,
skip_sequence_parallel_override: bool = False,
@@ -281,9 +353,58 @@ class ZImageAttention(nn.Module):
k = k.view(*k.shape[:-1], self.local_num_kv_heads, self.head_dim)
v = v.view(*v.shape[:-1], self.local_num_kv_heads, self.head_dim)
if freqs_cis is not None:
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,
)
else:
q, k = apply_flashinfer_rope_qk_inplace(
q,
k,
rope_cos_sin_cache,
is_neox=False,
positions=rope_positions,
)
elif freqs_cis is not None:
cos, sin = freqs_cis
if _is_cuda and q.shape == k.shape:
if cos.dim() == 3:
batch_size, seq_len = q.shape[:2]
cos_sin_cache = torch.cat(
[
cos.to(dtype=torch.float32).contiguous(),
sin.to(dtype=torch.float32).contiguous(),
],
dim=-1,
).reshape(batch_size * seq_len, -1)
positions = torch.arange(
batch_size * seq_len, device=q.device, dtype=torch.long
)
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=cos_sin_cache,
is_neox=False,
positions=positions,
allow_inplace=self.enable_zimage_qk_fusion,
)
else:
q, k = apply_flashinfer_rope_qk_inplace(
q, k, cos_sin_cache, is_neox=False, positions=positions
)
elif _is_cuda and q.shape == k.shape:
cos_sin_cache = torch.cat(
[
cos.to(dtype=torch.float32).contiguous(),
@@ -300,7 +421,7 @@ class ZImageAttention(nn.Module):
head_dim=self.head_dim,
cos_sin_cache=cos_sin_cache,
is_neox=False,
allow_inplace=True,
allow_inplace=self.enable_zimage_qk_fusion,
)
else:
q, k = apply_flashinfer_rope_qk_inplace(
@@ -314,7 +435,7 @@ class ZImageAttention(nn.Module):
q_norm=self.norm_q,
k_norm=self.norm_k,
head_dim=self.head_dim,
allow_inplace=True,
allow_inplace=self.enable_zimage_qk_fusion,
)
q = _apply_rotary_emb(q, cos, sin, is_neox_style=False)
k = _apply_rotary_emb(k, cos, sin, is_neox_style=False)
@@ -325,7 +446,7 @@ class ZImageAttention(nn.Module):
q_norm=self.norm_q,
k_norm=self.norm_k,
head_dim=self.head_dim,
allow_inplace=True,
allow_inplace=self.enable_zimage_qk_fusion,
)
if (
@@ -361,6 +482,8 @@ class ZImageAttention(nn.Module):
q,
k,
v,
attn_mask=attn_mask,
attn_mask_meta=attn_mask_meta,
num_replicated_prefix=num_replicated_prefix,
num_replicated_suffix=num_replicated_suffix,
skip_sequence_parallel_override=skip_sequence_parallel_override,
@@ -390,6 +513,7 @@ class ZImageTransformerBlock(nn.Module):
self.head_dim = dim // n_heads
self.layer_id = layer_id
self.modulation = modulation
self.enable_zimage_native_norm_fusion = quant_config is None
self.attention = ZImageAttention(
dim=dim,
@@ -438,11 +562,11 @@ class ZImageTransformerBlock(nn.Module):
prefix=f"{prefix}.feed_forward",
)
self.attention_norm1 = RMSNorm(dim, eps=norm_eps)
self.ffn_norm1 = RMSNorm(dim, eps=norm_eps)
self.attention_norm1 = ZImageRMSNorm(dim, eps=norm_eps)
self.ffn_norm1 = ZImageRMSNorm(dim, eps=norm_eps)
self.attention_norm2 = RMSNorm(dim, eps=norm_eps)
self.ffn_norm2 = RMSNorm(dim, eps=norm_eps)
self.attention_norm2 = ZImageRMSNorm(dim, eps=norm_eps)
self.ffn_norm2 = ZImageRMSNorm(dim, eps=norm_eps)
if modulation:
self.adaLN_modulation = nn.Sequential(
@@ -454,6 +578,10 @@ class ZImageTransformerBlock(nn.Module):
x: torch.Tensor,
freqs_cis: Tuple[torch.Tensor, torch.Tensor],
adaln_input: Optional[torch.Tensor] = None,
rope_cos_sin_cache: Optional[torch.Tensor] = None,
rope_positions: Optional[torch.Tensor] = None,
attn_mask: Optional[torch.Tensor] = None,
attn_mask_meta: Optional[dict] = None,
num_replicated_prefix: int = 0,
num_replicated_suffix: int = 0,
skip_sequence_parallel_override: bool = False,
@@ -468,51 +596,54 @@ class ZImageTransformerBlock(nn.Module):
# Attention block
attn_out = self.attention(
self.attention_norm1(x) * scale_msa,
zimage_rmsnorm_scale(
x,
scale_msa,
self.attention_norm1,
self.enable_zimage_native_norm_fusion,
),
freqs_cis=freqs_cis,
rope_cos_sin_cache=rope_cos_sin_cache,
rope_positions=rope_positions,
attn_mask=attn_mask,
attn_mask_meta=attn_mask_meta,
num_replicated_prefix=num_replicated_prefix,
num_replicated_suffix=num_replicated_suffix,
skip_sequence_parallel_override=skip_sequence_parallel_override,
)
if (
_is_cuda
and attn_out.is_cuda
and attn_out.shape[-1] % 256 == 0
and attn_out.shape[-1] <= 8192
and self.attention_norm2.variance_epsilon
== self.ffn_norm1.variance_epsilon
):
from sglang.jit_kernel.diffusion.cutedsl.norm_tanh_mul_add_norm_scale import (
fused_norm_tanh_mul_add_norm_scale,
)
x, ffn_in = fused_norm_tanh_mul_add_norm_scale(
attn_out.contiguous(),
self.attention_norm2.weight.data.contiguous(),
None,
gate_msa.contiguous(),
x.contiguous(),
self.ffn_norm1.weight.data.contiguous(),
None,
scale_mlp.contiguous(),
"rms",
self.attention_norm2.variance_epsilon,
)
else:
x = apply_rmsnorm_tanh_mul_add(
attn_out, gate_msa, x, self.attention_norm2
)
ffn_in = self.ffn_norm1(x) * (1.0 + scale_mlp)
x = zimage_rmsnorm_tanh_mul_add(
attn_out,
gate_msa,
x,
self.attention_norm2,
self.enable_zimage_native_norm_fusion,
)
ffn_in = zimage_rmsnorm_scale(
x,
1.0 + scale_mlp,
self.ffn_norm1,
self.enable_zimage_native_norm_fusion,
)
# FFN block
ffn_out = self.feed_forward(ffn_in)
x = apply_rmsnorm_tanh_mul_add(ffn_out, gate_mlp, x, self.ffn_norm2)
x = zimage_rmsnorm_tanh_mul_add(
ffn_out,
gate_mlp,
x,
self.ffn_norm2,
self.enable_zimage_native_norm_fusion,
)
else:
# Attention block
attn_input = self.attention_norm1(x)
attn_out = self.attention(
attn_input,
freqs_cis=freqs_cis,
rope_cos_sin_cache=rope_cos_sin_cache,
rope_positions=rope_positions,
attn_mask=attn_mask,
attn_mask_meta=attn_mask_meta,
num_replicated_prefix=num_replicated_prefix,
num_replicated_suffix=num_replicated_suffix,
skip_sequence_parallel_override=skip_sequence_parallel_override,
@@ -741,7 +872,7 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
)
self.cap_embedder = nn.Sequential(
RMSNorm(arch_config.cap_feat_dim, eps=arch_config.norm_eps),
ZImageRMSNorm(arch_config.cap_feat_dim, eps=arch_config.norm_eps),
ReplicatedLinear(arch_config.cap_feat_dim, self.dim, bias=True),
)
@@ -840,6 +971,8 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
all_cap_feats_out = []
all_image_valid_lens = []
all_cap_valid_lens = []
all_image_attn_lens = []
all_cap_attn_lens = []
image_records = []
cap_seq_len_target = max(
@@ -854,6 +987,7 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
for idx, cap_feat in enumerate(all_cap_feats):
cap_ori_len = cap_feat.size(0)
cap_attn_len = self._ceil_to_multiple(cap_ori_len, SEQ_MULTI_OF)
cap_padding_len = cap_seq_len_target - cap_ori_len
cap_padded_feat = torch.cat(
[cap_feat, cap_feat[-1:].repeat(cap_padding_len, 1)],
@@ -864,6 +998,7 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
all_cap_valid_lens.append(cap_ori_len)
else:
all_cap_valid_lens.append(caption_valid_lens[idx])
all_cap_attn_lens.append(cap_attn_len)
target_image_seq_len = image_seq_len_target or 0
for image in all_image:
@@ -878,13 +1013,17 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
F_tokens * H_tokens * W_tokens, pF * pH * pW * C
)
image_ori_len = image.size(0)
target_image_seq_len = max(
target_image_seq_len,
image_attn_len = max(
image_seq_len_target or 0,
self._ceil_to_multiple(image_ori_len, SEQ_MULTI_OF),
)
image_records.append((image, image_size, image_ori_len))
target_image_seq_len = max(
target_image_seq_len,
image_attn_len,
)
image_records.append((image, image_size, image_ori_len, image_attn_len))
for image, image_size, image_ori_len in image_records:
for image, image_size, image_ori_len, image_attn_len in image_records:
image_padding_len = target_image_seq_len - image_ori_len
image_padded_feat = torch.cat(
[image, image[-1:].repeat(image_padding_len, 1)],
@@ -893,6 +1032,7 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
all_image_out.append(image_padded_feat)
all_image_size.append(image_size)
all_image_valid_lens.append(image_ori_len)
all_image_attn_lens.append(image_attn_len)
cap_valid_lens_out = (
caption_valid_lens if caption_valid_lens is not None else all_cap_valid_lens
@@ -903,8 +1043,275 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
all_image_size,
all_image_valid_lens,
cap_valid_lens_out,
all_image_attn_lens,
all_cap_attn_lens,
)
def _build_single_sample_freqs_cis(
self,
image: torch.Tensor,
cap_feat: torch.Tensor,
patch_size: int,
f_patch_size: int,
) -> Tuple[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor, torch.Tensor]]:
device = image.device
cap_ori_len = int(cap_feat.size(0))
cap_padding_len = (-cap_ori_len) % SEQ_MULTI_OF
cap_pos_ids = self.create_coordinate_grid(
size=(cap_ori_len + cap_padding_len, 1, 1),
start=(1, 0, 0),
device=device,
).flatten(0, 2)
_, F, H, W = image.size()
pH = pW = patch_size
pF = f_patch_size
F_tokens, H_tokens, W_tokens = F // pF, H // pH, W // pW
image_ori_len = F_tokens * H_tokens * W_tokens
image_padding_len = (-image_ori_len) % SEQ_MULTI_OF
image_ori_pos_ids = self.create_coordinate_grid(
size=(F_tokens, H_tokens, W_tokens),
start=(cap_ori_len + cap_padding_len + 1, 0, 0),
device=device,
).flatten(0, 2)
image_padding_pos_ids = (
self.create_coordinate_grid(
size=(1, 1, 1),
start=(0, 0, 0),
device=device,
)
.flatten(0, 2)
.repeat(image_padding_len, 1)
)
image_pos_ids = torch.cat([image_ori_pos_ids, image_padding_pos_ids], dim=0)
return self.rotary_emb(cap_pos_ids), self.rotary_emb(image_pos_ids)
@staticmethod
def _pad_freqs_cis_to_length(
freqs_cis: Tuple[torch.Tensor, torch.Tensor], target_len: int
) -> Tuple[torch.Tensor, torch.Tensor]:
cos, sin = freqs_cis
pad_len = target_len - cos.shape[0]
if pad_len < 0:
raise ValueError(
f"Cannot pad RoPE freqs of length {cos.shape[0]} to shorter target {target_len}"
)
if pad_len == 0:
return cos, sin
return (
torch.cat([cos, cos.new_zeros(pad_len, cos.shape[-1])], dim=0),
torch.cat([sin, sin.new_zeros(pad_len, sin.shape[-1])], dim=0),
)
def _build_batched_freqs_cis(
self,
images: list[torch.Tensor],
cap_feats: list[torch.Tensor],
patch_size: int,
f_patch_size: int,
image_target_len: int,
cap_target_len: int,
) -> Tuple[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor, torch.Tensor]]:
cap_cos, cap_sin, image_cos, image_sin = [], [], [], []
for image, cap_feat in zip(images, cap_feats):
sample_cap_freqs, sample_image_freqs = self._build_single_sample_freqs_cis(
image,
cap_feat,
patch_size,
f_patch_size,
)
sample_cap_freqs = self._pad_freqs_cis_to_length(
sample_cap_freqs, cap_target_len
)
sample_image_freqs = self._pad_freqs_cis_to_length(
sample_image_freqs, image_target_len
)
cap_cos.append(sample_cap_freqs[0])
cap_sin.append(sample_cap_freqs[1])
image_cos.append(sample_image_freqs[0])
image_sin.append(sample_image_freqs[1])
return (
(torch.stack(cap_cos, dim=0), torch.stack(cap_sin, dim=0)),
(torch.stack(image_cos, dim=0), torch.stack(image_sin, dim=0)),
)
@staticmethod
def _device_cache_key(device: torch.device) -> tuple[str, int | None]:
device = torch.device(device)
return device.type, device.index
def _get_cached_batched_freqs_cis(
self,
images: list[torch.Tensor],
cap_feats: list[torch.Tensor],
patch_size: int,
f_patch_size: int,
image_target_len: int,
cap_target_len: int,
device: torch.device,
) -> Tuple[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor, torch.Tensor]]:
cache_key = (
tuple(tuple(image.shape) for image in images),
tuple(tuple(cap_feat.shape) for cap_feat in cap_feats),
int(patch_size),
int(f_patch_size),
int(image_target_len),
int(cap_target_len),
self._device_cache_key(device),
)
cached = getattr(self, "_cached_batched_freqs_cis", None)
if cached is not None and cached[0] == cache_key:
return cached[1]
freqs_cis = self._build_batched_freqs_cis(
images,
cap_feats,
patch_size,
f_patch_size,
image_target_len=image_target_len,
cap_target_len=cap_target_len,
)
self._cached_batched_freqs_cis = (cache_key, freqs_cis)
return freqs_cis
def _get_rope_cache(
self,
cache_attr: str,
freqs_cis: Optional[Tuple[torch.Tensor, torch.Tensor]],
) -> tuple[Optional[torch.Tensor], Optional[torch.Tensor]]:
if freqs_cis is None or not _is_cuda:
return None, None
cos, sin = freqs_cis
if not (cos.is_cuda and sin.is_cuda):
return None, None
cache_key = (
cos.data_ptr(),
sin.data_ptr(),
tuple(cos.shape),
tuple(sin.shape),
cos.dtype,
sin.dtype,
self._device_cache_key(cos.device),
)
cached = getattr(self, cache_attr, None)
if cached is not None and cached[0] == cache_key:
return cached[1]
if cos.dim() == 3:
batch_size, seq_len = cos.shape[:2]
cos_sin_cache = torch.cat(
[
cos.to(dtype=torch.float32).contiguous(),
sin.to(dtype=torch.float32).contiguous(),
],
dim=-1,
).reshape(batch_size * seq_len, -1)
positions = torch.arange(
batch_size * seq_len, device=cos.device, dtype=torch.long
)
elif cos.dim() == 2:
cos_sin_cache = torch.cat(
[
cos.to(dtype=torch.float32).contiguous(),
sin.to(dtype=torch.float32).contiguous(),
],
dim=-1,
)
positions = None
else:
return None, None
rope_cache = (cos_sin_cache, positions)
setattr(self, cache_attr, (cache_key, rope_cache))
return rope_cache
def _get_attn_mask_and_meta(
self, cache_attr: str, lengths: list[int], target_len: int, device: torch.device
) -> Tuple[Optional[torch.Tensor], Optional[dict]]:
length_key = tuple(int(length) for length in lengths)
if all(length == target_len for length in length_key):
return None, None
cache_key = (
length_key,
int(target_len),
self._device_cache_key(device),
)
cached = getattr(self, cache_attr, None)
if cached is not None and cached[0] == cache_key:
return cached[1]
positions = torch.arange(target_len, device=device).unsqueeze(0)
length_tensor = torch.as_tensor(
length_key, dtype=torch.long, device=device
).unsqueeze(1)
mask = positions < length_tensor
meta = build_varlen_mask_meta_from_lengths(length_key, target_len, device)
result = (mask, meta)
setattr(self, cache_attr, (cache_key, result))
return result
def _get_joint_attn_mask_and_meta(
self,
image_lengths: list[int],
image_target_len: int,
cap_lengths: list[int],
cap_target_len: int,
device: torch.device,
) -> Tuple[Optional[torch.Tensor], Optional[dict]]:
image_length_key = tuple(int(length) for length in image_lengths)
cap_length_key = tuple(int(length) for length in cap_lengths)
if all(length == image_target_len for length in image_length_key) and all(
length == cap_target_len for length in cap_length_key
):
return None, None
cache_key = (
image_length_key,
int(image_target_len),
cap_length_key,
int(cap_target_len),
self._device_cache_key(device),
)
cached = getattr(self, "_cached_joint_attn_mask_meta", None)
if cached is not None and cached[0] == cache_key:
return cached[1]
image_pos = torch.arange(image_target_len, device=device).unsqueeze(0)
cap_pos = torch.arange(cap_target_len, device=device).unsqueeze(0)
image_len = torch.as_tensor(
image_length_key, dtype=torch.long, device=device
).unsqueeze(1)
cap_len = torch.as_tensor(
cap_length_key, dtype=torch.long, device=device
).unsqueeze(1)
mask = torch.cat([image_pos < image_len, cap_pos < cap_len], dim=1)
valid_ranges = [
[
(0, image_length),
(image_target_len, image_target_len + cap_length),
]
for image_length, cap_length in zip(
image_length_key, cap_length_key, strict=True
)
]
meta = build_varlen_mask_meta_from_ranges(
valid_ranges,
image_target_len + cap_target_len,
device,
)
result = (mask, meta)
self._cached_joint_attn_mask_meta = (cache_key, result)
return result
@staticmethod
def _has_padding(valid_lens: list[int], target_len: int) -> bool:
return any(int(length) < target_len for length in valid_lens)
@staticmethod
def _as_image_list(hidden_states) -> list[torch.Tensor]:
"""Normalize 4D/5D image latents into per-sample tensors."""
@@ -939,6 +1346,9 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
pad_token: torch.Tensor,
) -> torch.Tensor:
"""Replace padded token rows after each valid sequence length."""
if not ZImageTransformer2DModel._has_padding(valid_lens, tensor.shape[1]):
return tensor
positions = torch.arange(tensor.shape[1], device=tensor.device).unsqueeze(0)
if torch.is_tensor(valid_lens):
lengths = valid_lens.to(device=tensor.device, dtype=torch.long)
@@ -946,9 +1356,8 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
lengths = torch.tensor(valid_lens, device=tensor.device)
lengths = lengths.unsqueeze(1)
pad_mask = positions >= lengths
if pad_mask.any():
tensor = tensor.clone()
tensor[pad_mask] = pad_token.to(device=tensor.device, dtype=tensor.dtype)
tensor = tensor.clone()
tensor[pad_mask] = pad_token.to(device=tensor.device, dtype=tensor.dtype)
return tensor
def forward(
@@ -969,6 +1378,9 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
x = self._as_image_list(hidden_states)
cap_feats = self._as_caption_list(encoder_hidden_states)
input_images = x
input_cap_feats = cap_feats
timestep = 1000.0 - timestep
t = timestep
t = self.t_embedder(t)
@@ -979,6 +1391,8 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
x_size,
x_valid_lens,
cap_valid_lens,
x_attn_lens,
cap_attn_lens,
) = self.patchify_and_embed(
x,
cap_feats,
@@ -989,11 +1403,36 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
)
x, _ = self.all_x_embedder[f"{patch_size}-{f_patch_size}"](x)
device = x.device
x = self._replace_padding_with_token(x, x_valid_lens, self.x_pad_token)
if len(input_images) > 1 and get_sp_world_size() == 1:
freqs_cis = self._get_cached_batched_freqs_cis(
input_images,
input_cap_feats,
patch_size,
f_patch_size,
image_target_len=x.shape[1],
cap_target_len=cap_feats.shape[1],
device=device,
)
x_freqs_cis = freqs_cis[1]
x_rope_cos_sin_cache, x_rope_positions = self._get_rope_cache(
"_cached_x_rope_cache", x_freqs_cis
)
x_attn_mask, x_attn_mask_meta = self._get_attn_mask_and_meta(
"_cached_x_attn_mask_meta", x_attn_lens, x.shape[1], device
)
for layer_id, layer in enumerate(self.noise_refiner):
x = layer(x, x_freqs_cis, adaln_input)
x = layer(
x,
x_freqs_cis,
adaln_input,
rope_cos_sin_cache=x_rope_cos_sin_cache,
rope_positions=x_rope_positions,
attn_mask=x_attn_mask,
attn_mask_meta=x_attn_mask_meta,
)
cap_feats, _ = self.cap_embedder(cap_feats)
cap_feats = self._replace_padding_with_token(
@@ -1001,11 +1440,21 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
)
cap_freqs_cis = freqs_cis[0]
cap_rope_cos_sin_cache, cap_rope_positions = self._get_rope_cache(
"_cached_cap_rope_cache", cap_freqs_cis
)
cap_attn_mask, cap_attn_mask_meta = self._get_attn_mask_and_meta(
"_cached_cap_attn_mask_meta", cap_attn_lens, cap_feats.shape[1], device
)
for layer_id, layer in enumerate(self.context_refiner):
cap_feats = layer(
cap_feats,
cap_freqs_cis,
rope_cos_sin_cache=cap_rope_cos_sin_cache,
rope_positions=cap_rope_positions,
attn_mask=cap_attn_mask,
attn_mask_meta=cap_attn_mask_meta,
)
cap_seq_len = cap_feats.shape[1]
@@ -1021,8 +1470,18 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
)
unified = torch.cat([x, cap_feats], dim=1)
unified_freqs_cis = (
torch.cat([x_freqs_cis[0], cap_freqs_cis[0]], dim=0),
torch.cat([x_freqs_cis[1], cap_freqs_cis[1]], dim=0),
torch.cat([x_freqs_cis[0], cap_freqs_cis[0]], dim=-2),
torch.cat([x_freqs_cis[1], cap_freqs_cis[1]], dim=-2),
)
unified_attn_mask, unified_attn_mask_meta = self._get_joint_attn_mask_and_meta(
x_attn_lens,
x.shape[1],
cap_attn_lens,
cap_seq_len,
device,
)
unified_rope_cos_sin_cache, unified_rope_positions = self._get_rope_cache(
"_cached_unified_rope_cache", unified_freqs_cis
)
num_replicated_suffix = cap_seq_len if not use_full_unified_sequence else 0
@@ -1031,6 +1490,10 @@ class ZImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
unified,
unified_freqs_cis,
adaln_input,
rope_cos_sin_cache=unified_rope_cos_sin_cache,
rope_positions=unified_rope_positions,
attn_mask=unified_attn_mask,
attn_mask_meta=unified_attn_mask_meta,
num_replicated_suffix=num_replicated_suffix,
skip_sequence_parallel_override=use_full_unified_sequence,
)
@@ -385,9 +385,11 @@ class Qwen3ForCausalLM(TextEncoder):
residual = None
if position_ids is None:
position_ids = torch.arange(
0, hidden_states.shape[1], device=hidden_states.device
).unsqueeze(0)
position_ids = (
torch.arange(0, hidden_states.shape[1], device=hidden_states.device)
.unsqueeze(0)
.expand(hidden_states.shape[0], -1)
)
attention_lengths = None
if attention_mask is not None:
@@ -653,78 +653,78 @@
},
"zimage_image_t2i": {
"stages_ms": {
"DecodingStage": 8.86,
"DecodingStage": 134.1,
"InputValidationStage": 0.04,
"DenoisingStage": 675.8,
"TextEncodingStage": 130.38,
"LatentPreparationStage": 0.14,
"TimestepPreparationStage": 31.38
"DenoisingStage": 771.7,
"TextEncodingStage": 130.68,
"LatentPreparationStage": 0.16,
"TimestepPreparationStage": 27.67
},
"denoise_step_ms": {
"0": 19.93,
"1": 28.65,
"2": 83.91,
"3": 83.48,
"4": 83.51,
"5": 83.69,
"6": 84.08,
"7": 84.04,
"8": 84.35
"0": 95.97,
"1": 29.8,
"2": 91.06,
"3": 91.41,
"4": 93.26,
"5": 90.11,
"6": 90.88,
"7": 91.93,
"8": 93.16
},
"expected_e2e_ms": 1027.94,
"expected_avg_denoise_ms": 74.6,
"expected_median_denoise_ms": 86.63,
"estimated_full_test_time_s": 121.1
"expected_e2e_ms": 1068.66,
"expected_avg_denoise_ms": 85.29,
"expected_median_denoise_ms": 91.41,
"estimated_full_test_time_s": 116.3
},
"zimage_image_t2i_fp8": {
"stages_ms": {
"TextEncodingStage": 129.84,
"DenoisingStage": 634.42,
"InputValidationStage": 0.03,
"LatentPreparationStage": 0.11,
"TimestepPreparationStage": 17.42,
"DecodingStage": 9.46
"InputValidationStage": 0.04,
"TextEncodingStage": 131.15,
"LatentPreparationStage": 0.18,
"TimestepPreparationStage": 26.29,
"DenoisingStage": 959.66,
"DecodingStage": 125.62
},
"denoise_step_ms": {
"0": 33.35,
"1": 36.43,
"2": 66.88,
"3": 78.15,
"4": 78.0,
"5": 78.36,
"6": 78.48,
"7": 78.32,
"8": 73.36
"0": 115.4,
"1": 56.32,
"2": 111.77,
"3": 110.02,
"4": 111.43,
"5": 111.32,
"6": 110.77,
"7": 110.53,
"8": 118.39
},
"expected_e2e_ms": 958.32,
"expected_avg_denoise_ms": 70.04,
"expected_median_denoise_ms": 81.35,
"estimated_full_test_time_s": 121.0
"expected_e2e_ms": 1247.73,
"expected_avg_denoise_ms": 106.22,
"expected_median_denoise_ms": 111.32,
"estimated_full_test_time_s": 123.7
},
"zimage_image_t2i_multi_lora": {
"stages_ms": {
"TimestepPreparationStage": 24.93,
"DenoisingStage": 673.95,
"DecodingStage": 8.43,
"LatentPreparationStage": 0.1,
"TextEncodingStage": 129.4,
"InputValidationStage": 0.03
"InputValidationStage": 0.04,
"TextEncodingStage": 130.25,
"LatentPreparationStage": 0.12,
"TimestepPreparationStage": 26.5,
"DenoisingStage": 841.93,
"DecodingStage": 142.78
},
"denoise_step_ms": {
"0": 25.08,
"1": 35.42,
"2": 77.56,
"3": 83.39,
"4": 82.18,
"5": 83.1,
"6": 82.39,
"7": 83.34,
"8": 85.82
"0": 102.18,
"1": 40.06,
"2": 99.39,
"3": 100.33,
"4": 103.95,
"5": 93.48,
"6": 99.6,
"7": 99.38,
"8": 99.77
},
"expected_e2e_ms": 1047.82,
"expected_avg_denoise_ms": 74.33,
"expected_median_denoise_ms": 86.33,
"estimated_full_test_time_s": 121.1
"expected_e2e_ms": 1145.53,
"expected_avg_denoise_ms": 93.12,
"expected_median_denoise_ms": 99.6,
"estimated_full_test_time_s": 162.1
},
"zimage_image_t2i_2_gpus": {
"stages_ms": {
@@ -34,7 +34,7 @@ if TYPE_CHECKING:
logger = init_logger(__name__)
SGL_TEST_FILES_CI_DATA_REVISION = "916cbff23aa4e89f78128397ede7ce29a73d6d8c"
SGL_TEST_FILES_CI_DATA_REVISION = "77bd016251220fee8917a30ec92e89da03794a8a"
if current_platform.is_npu():
SGL_TEST_FILES_CI_DATA_REVISION = "6b62f4b6825c76a25fd2ba28248df68f2b400e65"
@@ -96,6 +96,9 @@ DEFAULT_MEAN_ABS_DIFF_THRESHOLD_VIDEO = 10.0
_clip_model_cache: dict[str, Any] = {}
_consistency_gt_cache: dict[str, Any] = {}
_official_consistency_gt_outputs_cache: dict[str, frozenset[str]] | None = None
CONSISTENCY_GT_CASE_ALIASES = {
"fsdp-inference": "zimage_image_t2i_2_gpus",
}
OFFICIAL_CONSISTENCY_GT_SKIP_CASES = frozenset(
{
# Official references for these cases need regeneration or parity triage.
@@ -1016,10 +1019,15 @@ def output_format_to_ext(output_format: str | None) -> str:
return "png"
def get_consistency_gt_case_id(case_id: str) -> str:
return CONSISTENCY_GT_CASE_ALIASES.get(case_id, case_id)
def _consistency_gt_filenames(
case_id: str, num_gpus: int, is_video: bool, output_format: str | None = None
) -> list[str]:
"""Return the list of GT image filenames for a case. Reused by GT generation and consistency check."""
case_id = get_consistency_gt_case_id(case_id)
n = num_gpus
if is_video:
return [
@@ -1034,6 +1042,7 @@ def _consistency_gt_filenames(
def _base_consistency_gt_candidates(
case_id: str, num_gpus: int, is_video: bool, output_format: str | None = None
) -> list[str]:
case_id = get_consistency_gt_case_id(case_id)
n = num_gpus
if is_video:
return [
@@ -1053,9 +1062,9 @@ def get_consistency_gt_candidate_sets(
candidates = _base_consistency_gt_candidates(
case_id, num_gpus, is_video, output_format
)
platform = get_consistency_platform()
if platform == "h100":
if _is_ascend_consistency_case(case_id) or current_platform.is_npu():
return [candidates]
platform = get_consistency_platform()
return [[f"{platform}/{candidate}" for candidate in candidates], candidates]
@@ -1157,7 +1166,8 @@ def _is_official_consistency_gt_base_url(base_url: str) -> bool:
def _official_consistency_gt_candidate_is_declared(case_id: str, filename: str) -> bool:
return filename in _official_consistency_gt_outputs_for_case(case_id)
outputs = _official_consistency_gt_outputs_for_case(case_id)
return filename in outputs or filename.rsplit("/", 1)[-1] in outputs
def _remote_consistency_gt_base_urls(case_id: str) -> tuple[str, ...]:
@@ -62,9 +62,9 @@ def test_remote_video_gt_candidates_survive_inconclusive_probe(monkeypatch):
)
assert [filename for filename, _ in files] == [
"unit_video_1gpu_frame_0.png",
"unit_video_1gpu_frame_mid.png",
"unit_video_1gpu_frame_last.png",
"h100/unit_video_1gpu_frame_0.png",
"h100/unit_video_1gpu_frame_mid.png",
"h100/unit_video_1gpu_frame_last.png",
]
@@ -87,10 +87,10 @@ def test_remote_image_gt_prefers_official_when_present(monkeypatch):
assert files == [
(
expected_filename,
f"h100/{expected_filename}",
(
f"{test_utils.SGL_TEST_FILES_OFFICIAL_CONSISTENCY_GT_BASE}"
f"/{expected_filename}"
f"/h100/{expected_filename}"
),
)
]
@@ -116,10 +116,10 @@ def test_remote_image_gt_ignores_unmapped_official_file(monkeypatch):
assert files == [
(
expected_filename,
f"h100/{expected_filename}",
(
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}"
f"/{expected_filename}"
f"/h100/{expected_filename}"
),
)
]
@@ -141,24 +141,24 @@ def test_remote_video_gt_ignores_unmapped_official_files(monkeypatch):
assert files == [
(
f"{case_id}_2gpu_frame_0.png",
f"h100/{case_id}_2gpu_frame_0.png",
(
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}"
f"/{case_id}_2gpu_frame_0.png"
f"/h100/{case_id}_2gpu_frame_0.png"
),
),
(
f"{case_id}_2gpu_frame_mid.png",
f"h100/{case_id}_2gpu_frame_mid.png",
(
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}"
f"/{case_id}_2gpu_frame_mid.png"
f"/h100/{case_id}_2gpu_frame_mid.png"
),
),
(
f"{case_id}_2gpu_frame_last.png",
f"h100/{case_id}_2gpu_frame_last.png",
(
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}"
f"/{case_id}_2gpu_frame_last.png"
f"/h100/{case_id}_2gpu_frame_last.png"
),
),
]
@@ -178,7 +178,10 @@ def test_ltx_hq_remote_gt_uses_sglang_generated_when_official_declared(monkeypat
files = test_utils._find_remote_consistency_gt_files(case_id, 1, is_video=True)
assert files == [
(filename, f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}/{filename}")
(
f"h100/{filename}",
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}/h100/{filename}",
)
for filename in filenames
]
@@ -201,10 +204,10 @@ def test_remote_image_gt_falls_back_to_sglang_when_official_missing(monkeypatch)
assert files == [
(
expected_filename,
f"h100/{expected_filename}",
(
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}"
f"/{expected_filename}"
f"/h100/{expected_filename}"
),
)
]
@@ -230,10 +233,10 @@ def test_remote_image_gt_skips_official_for_quarantined_case(monkeypatch):
assert files == [
(
expected_filename,
f"h100/{expected_filename}",
(
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}"
f"/{expected_filename}"
f"/h100/{expected_filename}"
),
)
]
@@ -306,24 +309,24 @@ def test_remote_video_gt_skips_official_for_quarantined_case(monkeypatch):
assert files == [
(
f"{case_id}_2gpu_frame_0.png",
f"h100/{case_id}_2gpu_frame_0.png",
(
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}"
f"/{case_id}_2gpu_frame_0.png"
f"/h100/{case_id}_2gpu_frame_0.png"
),
),
(
f"{case_id}_2gpu_frame_mid.png",
f"h100/{case_id}_2gpu_frame_mid.png",
(
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}"
f"/{case_id}_2gpu_frame_mid.png"
f"/h100/{case_id}_2gpu_frame_mid.png"
),
),
(
f"{case_id}_2gpu_frame_last.png",
f"h100/{case_id}_2gpu_frame_last.png",
(
f"{test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE}"
f"/{case_id}_2gpu_frame_last.png"
f"/h100/{case_id}_2gpu_frame_last.png"
),
),
]
@@ -377,6 +380,39 @@ def test_platform_gt_candidates_prefer_platform_then_default(monkeypatch):
]
def test_h100_gt_candidates_prefer_platform_then_default(monkeypatch):
monkeypatch.setenv(test_utils.CONSISTENCY_PLATFORM_ENV, "h100")
assert test_utils.get_consistency_gt_candidates(
"unit_image",
1,
is_video=False,
output_format="png",
) == [
"h100/unit_image_1gpu.png",
"h100/unit_image_1gpu.jpg",
"h100/unit_image_1gpu.webp",
"unit_image_1gpu.png",
"unit_image_1gpu.jpg",
"unit_image_1gpu.webp",
]
def test_consistency_gt_case_alias_reuses_canonical_filename(monkeypatch):
monkeypatch.setenv(test_utils.CONSISTENCY_PLATFORM_ENV, "h100")
assert test_utils.get_consistency_gt_candidates(
"fsdp-inference",
2,
is_video=False,
output_format=None,
)[:3] == [
"h100/zimage_image_t2i_2_gpus_2gpu.jpg",
"h100/zimage_image_t2i_2_gpus_2gpu.png",
"h100/zimage_image_t2i_2_gpus_2gpu.webp",
]
def test_threshold_metadata_merges_platform_override():
metadata = test_utils._merge_threshold_metadata(
{
@@ -0,0 +1,50 @@
from types import SimpleNamespace
import torch
from sglang.multimodal_gen.runtime.models.encoders.qwen3 import Qwen3ForCausalLM
class _CaptureLayer(torch.nn.Module):
def __init__(self):
super().__init__()
self.position_ids = None
self.attention_lengths = None
def forward(self, position_ids, hidden_states, residual, attention_lengths):
self.position_ids = position_ids
self.attention_lengths = attention_lengths
if residual is None:
residual = torch.zeros_like(hidden_states)
return hidden_states, residual
class _IdentityNorm(torch.nn.Module):
def forward(self, hidden_states, residual):
if residual is not None:
hidden_states = hidden_states + residual
return hidden_states, None
def test_default_position_ids_batch_shape():
model = Qwen3ForCausalLM.__new__(Qwen3ForCausalLM)
torch.nn.Module.__init__(model)
layer = _CaptureLayer()
model.config = SimpleNamespace(output_hidden_states=False)
model.layers = torch.nn.ModuleList([layer])
model.norm = _IdentityNorm()
def get_input_embeddings(input_ids):
return torch.zeros(input_ids.shape[0], input_ids.shape[1], 8)
model.get_input_embeddings = get_input_embeddings
input_ids = torch.zeros(2, 4, dtype=torch.long)
attention_mask = torch.ones(2, 4, dtype=torch.long)
model(input_ids=input_ids, attention_mask=attention_mask)
assert layer.position_ids.shape == input_ids.shape
assert torch.equal(layer.position_ids[0], torch.arange(4))
assert torch.equal(layer.position_ids[1], torch.arange(4))
assert layer.attention_lengths == (4, 4)
@@ -5,13 +5,73 @@ from unittest.mock import patch
import torch
from sglang.multimodal_gen.configs.pipeline_configs.zimage import ZImagePipelineConfig
from sglang.multimodal_gen.runtime.models.dits.zimage import (
ZImageRMSNorm,
ZImageTransformer2DModel,
)
class TestZImagePipelineConfig(unittest.TestCase):
def test_rmsnorm_native_formula(self) -> None:
norm = ZImageRMSNorm(4, eps=1e-5)
with torch.no_grad():
norm.weight.copy_(torch.tensor([1.0, 0.5, 1.5, 2.0]))
x = torch.tensor(
[[1.25, 0.5, -0.75, 3.0], [0.1, 2.3, -4.1, 0.7]],
dtype=torch.bfloat16,
)
output = norm(x)
expected = x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + 1e-5)
expected = expected * norm.weight.to(dtype=x.dtype)
self.assertEqual(output.dtype, x.dtype)
self.assertTrue(torch.equal(output, expected))
def test_explicit_sigmas(self) -> None:
"""Z-Image uses the native explicit flow sigmas schedule."""
config = ZImagePipelineConfig()
self.assertEqual(
config.prepare_sigmas(None, 4).tolist(),
[1.0, 0.75, 0.5, 0.25],
)
def test_autocast_disabled(self) -> None:
"""Official Z-Image runs bf16 weights without an outer autocast context."""
self.assertFalse(ZImagePipelineConfig().enable_autocast)
@patch("sglang.multimodal_gen.configs.pipeline_configs.zimage.get_sp_world_size")
def test_zimage_negative_prompt_rotary_embeddings_use_negative_prompt_len(
self, mock_get_sp_world_size
) -> None:
def test_image_rope_patch_tokens(self, mock_get_sp_world_size) -> None:
mock_get_sp_world_size.return_value = 1
config = ZImagePipelineConfig()
config.vae_config.post_init()
batch = SimpleNamespace(
prompt_embeds=[torch.ones(113, 2560)],
prompt_seq_lens=[[113]],
negative_prompt_embeds=None,
height=480,
width=640,
)
def rotary_emb(pos_ids):
return pos_ids
_, image_pos_ids = config.prepare_pos_cond_kwargs(
batch=batch,
device=torch.device("cpu"),
rotary_emb=rotary_emb,
dtype=torch.float32,
)["freqs_cis"]
self.assertEqual(image_pos_ids.shape, (1216, 3))
self.assertEqual(image_pos_ids[0].tolist(), [129, 0, 0])
self.assertEqual(image_pos_ids[1199].tolist(), [129, 29, 39])
self.assertEqual(image_pos_ids[-1].tolist(), [0, 0, 0])
@patch("sglang.multimodal_gen.configs.pipeline_configs.zimage.get_sp_world_size")
def test_negative_rope_len(self, mock_get_sp_world_size) -> None:
"""Negative CFG branch should build RoPE positions from negative prompt embeds."""
mock_get_sp_world_size.return_value = 1
@@ -42,6 +102,36 @@ class TestZImagePipelineConfig(unittest.TestCase):
self.assertEqual(cap_pos_ids.shape, (neg_cap_padded_len, 3))
self.assertEqual(image_pos_ids[0].tolist(), [neg_cap_padded_len + 1, 0, 0])
def test_batched_rope_offsets(self) -> None:
model = ZImageTransformer2DModel.__new__(ZImageTransformer2DModel)
def rotary_emb(pos_ids):
return (
pos_ids.to(torch.float32),
(pos_ids + 1000).to(torch.float32),
)
model.rotary_emb = rotary_emb
images = [torch.zeros(16, 1, 60, 80), torch.zeros(16, 1, 60, 80)]
cap_feats = [torch.zeros(113, 2560), torch.zeros(177, 2560)]
cap_freqs, image_freqs = model._build_batched_freqs_cis(
images,
cap_feats,
patch_size=2,
f_patch_size=1,
image_target_len=1216,
cap_target_len=192,
)
self.assertEqual(cap_freqs[0].shape, (2, 192, 3))
self.assertEqual(image_freqs[0].shape, (2, 1216, 3))
self.assertEqual(image_freqs[0][0, 0].tolist(), [129.0, 0.0, 0.0])
self.assertEqual(image_freqs[0][1, 0].tolist(), [193.0, 0.0, 0.0])
self.assertEqual(cap_freqs[0][0, 127].tolist(), [128.0, 0.0, 0.0])
self.assertEqual(cap_freqs[0][0, 128].tolist(), [0.0, 0.0, 0.0])
if __name__ == "__main__":
unittest.main()