[diffusion] feat: support sp for image models (#13180)

This commit is contained in:
Mick
2025-11-23 18:11:42 +08:00
committed by GitHub
parent 53fffefd5d
commit d4593964fe
19 changed files with 624 additions and 393 deletions
@@ -9,6 +9,7 @@ from typing import Any
import torch import torch
from diffusers.image_processor import VaeImageProcessor from diffusers.image_processor import VaeImageProcessor
from einops import rearrange
from sglang.multimodal_gen.configs.models import ( from sglang.multimodal_gen.configs.models import (
DiTConfig, DiTConfig,
@@ -18,6 +19,11 @@ from sglang.multimodal_gen.configs.models import (
) )
from sglang.multimodal_gen.configs.models.encoders import BaseEncoderOutput from sglang.multimodal_gen.configs.models.encoders import BaseEncoderOutput
from sglang.multimodal_gen.configs.utils import update_config_from_args from sglang.multimodal_gen.configs.utils import update_config_from_args
from sglang.multimodal_gen.runtime.distributed import (
get_sp_parallel_rank,
get_sp_world_size,
sequence_model_parallel_all_gather,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.utils import ( from sglang.multimodal_gen.utils import (
FlexibleArgumentParser, FlexibleArgumentParser,
@@ -59,10 +65,45 @@ def postprocess_text(output: BaseEncoderOutput, _text_inputs) -> torch.tensor:
raise NotImplementedError raise NotImplementedError
def shard_rotary_emb_for_sp(emb):
"""
Shard rotary embeddings [S, D] along sequence for SP.
If S is not divisible by SP degree, pad by repeating the last row.
"""
# Sequence Parallelism: slice image RoPE to local shard if enabled
try:
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_sp_parallel_rank,
get_sp_world_size,
)
sp_world_size = get_sp_world_size()
except Exception:
sp_world_size = 1
seq_len = emb.shape[0]
if seq_len % sp_world_size != 0:
pad_len = sp_world_size - (seq_len % sp_world_size)
pad = emb[-1:].repeat(pad_len, 1)
emb = torch.cat([emb, pad], dim=0)
if sp_world_size > 1:
try:
rank = get_sp_parallel_rank()
except Exception:
rank = 0
seq_len = emb.shape[0]
local_len = seq_len // sp_world_size
start = rank * local_len
end = start + local_len
emb = emb[start:end]
return emb
else:
return emb
# config for a single pipeline # config for a single pipeline
@dataclass @dataclass
class PipelineConfig: class PipelineConfig:
"""Base configuration for all pipeline architectures.""" """The base configuration class for a generation pipeline."""
task_type: ModelTaskType task_type: ModelTaskType
@@ -163,9 +204,28 @@ class PipelineConfig:
return shape return shape
# called after latents are prepared # called after latents are prepared
def pack_latents(self, latents, batch_size, batch): def maybe_pack_latents(self, latents, batch_size, batch):
return latents return latents
def gather_latents_for_sp(self, latents):
# For video latents [B, C, T_local, H, W], gather along time dim=2
latents = sequence_model_parallel_all_gather(latents, dim=2)
return latents
def shard_latents_for_sp(self, batch, latents):
# general logic for video models
sp_world_size, rank_in_sp_group = get_sp_world_size(), get_sp_parallel_rank()
if latents.dim() != 5:
return latents, False
time_dim = latents.shape[2]
if time_dim > 0 and time_dim % sp_world_size == 0:
sharded_tensor = rearrange(
latents, "b c (n t) h w -> b c n t h w", n=sp_world_size
).contiguous()
sharded_tensor = sharded_tensor[:, :, rank_in_sp_group, :, :, :]
return sharded_tensor, True
return latents, False
def get_pos_prompt_embeds(self, batch): def get_pos_prompt_embeds(self, batch):
return batch.prompt_embeds return batch.prompt_embeds
@@ -459,6 +519,55 @@ class PipelineConfig:
self.__post_init__() self.__post_init__()
@dataclass
class ImagePipelineConfig(PipelineConfig):
"""Base config for image generation pipelines with token-like latents [B, S, D]."""
def shard_latents_for_sp(self, batch, latents):
sp_world_size, rank_in_sp_group = get_sp_world_size(), get_sp_parallel_rank()
seq_len = latents.shape[1]
# Pad to next multiple of SP degree if needed
if seq_len % sp_world_size != 0:
pad_len = sp_world_size - (seq_len % sp_world_size)
pad = torch.zeros(
(latents.shape[0], pad_len, latents.shape[2]),
dtype=latents.dtype,
device=latents.device,
)
latents = torch.cat([latents, pad], dim=1)
# Record padding length for later unpad
batch.sp_seq_pad = int(getattr(batch, "sp_seq_pad", 0)) + pad_len
sharded_tensor = rearrange(
latents, "b (n s) d -> b n s d", n=sp_world_size
).contiguous()
sharded_tensor = sharded_tensor[:, rank_in_sp_group, :, :]
return sharded_tensor, True
def gather_latents_for_sp(self, latents):
# For image latents [B, S_local, D], gather along sequence dim=1
latents = sequence_model_parallel_all_gather(latents, dim=1)
return latents
def _unpad_and_unpack_latents(self, latents, batch):
vae_scale_factor = self.vae_config.arch_config.vae_scale_factor
channels = self.dit_config.arch_config.in_channels
batch_size = latents.shape[0]
height = 2 * (int(batch.height) // (vae_scale_factor * 2))
width = 2 * (int(batch.width) // (vae_scale_factor * 2))
# If SP padding was applied, remove extra tokens before reshaping
target_tokens = (height // 2) * (width // 2)
if latents.shape[1] > target_tokens:
latents = latents[:, :target_tokens, :]
latents = latents.view(batch_size, height // 2, width // 2, channels // 4, 2, 2)
latents = latents.permute(0, 3, 1, 4, 2, 5)
return latents, batch_size, channels, height, width
@dataclass @dataclass
class SlidingTileAttnConfig(PipelineConfig): class SlidingTileAttnConfig(PipelineConfig):
"""Configuration for sliding tile attention.""" """Configuration for sliding tile attention."""
@@ -14,14 +14,16 @@ from sglang.multimodal_gen.configs.models.encoders import (
) )
from sglang.multimodal_gen.configs.models.vaes.flux import FluxVAEConfig from sglang.multimodal_gen.configs.models.vaes.flux import FluxVAEConfig
from sglang.multimodal_gen.configs.pipeline_configs.base import ( from sglang.multimodal_gen.configs.pipeline_configs.base import (
ImagePipelineConfig,
ModelTaskType, ModelTaskType,
PipelineConfig,
preprocess_text, preprocess_text,
shard_rotary_emb_for_sp,
) )
from sglang.multimodal_gen.configs.pipeline_configs.hunyuan import ( from sglang.multimodal_gen.configs.pipeline_configs.hunyuan import (
clip_postprocess_text, clip_postprocess_text,
clip_preprocess_text, clip_preprocess_text,
) )
from sglang.multimodal_gen.configs.pipeline_configs.qwen_image import _pack_latents
def t5_postprocess_text(outputs: BaseEncoderOutput, _text_inputs) -> torch.Tensor: def t5_postprocess_text(outputs: BaseEncoderOutput, _text_inputs) -> torch.Tensor:
@@ -29,8 +31,9 @@ def t5_postprocess_text(outputs: BaseEncoderOutput, _text_inputs) -> torch.Tenso
@dataclass @dataclass
class FluxPipelineConfig(PipelineConfig): class FluxPipelineConfig(ImagePipelineConfig):
# FIXME: duplicate with SamplingParams.guidance_scale? """Configuration for the FLUX pipeline."""
embedded_cfg_scale: float = 3.5 embedded_cfg_scale: float = 3.5
task_type: ModelTaskType = ModelTaskType.T2I task_type: ModelTaskType = ModelTaskType.T2I
@@ -82,21 +85,14 @@ class FluxPipelineConfig(PipelineConfig):
shape = (batch_size, num_channels_latents, height, width) shape = (batch_size, num_channels_latents, height, width)
return shape return shape
def pack_latents(self, latents, batch_size, batch): def maybe_pack_latents(self, latents, batch_size, batch):
height = 2 * ( height = 2 * (
batch.height // (self.vae_config.arch_config.vae_scale_factor * 2) batch.height // (self.vae_config.arch_config.vae_scale_factor * 2)
) )
width = 2 * (batch.width // (self.vae_config.arch_config.vae_scale_factor * 2)) width = 2 * (batch.width // (self.vae_config.arch_config.vae_scale_factor * 2))
num_channels_latents = self.dit_config.arch_config.in_channels // 4 num_channels_latents = self.dit_config.arch_config.in_channels // 4
# pack latents # pack latents
latents = latents.view( return _pack_latents(latents, batch_size, num_channels_latents, height, width)
batch_size, num_channels_latents, height // 2, 2, width // 2, 2
)
latents = latents.permute(0, 2, 4, 1, 3, 5)
latents = latents.reshape(
batch_size, (height // 2) * (width // 2), num_channels_latents * 4
)
return latents
def get_pos_prompt_embeds(self, batch): def get_pos_prompt_embeds(self, batch):
return batch.prompt_embeds[1] return batch.prompt_embeds[1]
@@ -133,23 +129,27 @@ class FluxPipelineConfig(PipelineConfig):
original_width=width, original_width=width,
device=device, device=device,
) )
ids = torch.cat([txt_ids, img_ids], dim=0).to(device=device)
# NOTE(mick): prepare it here, to avoid unnecessary computations # NOTE(mick): prepare it here, to avoid unnecessary computations
freqs_cis = rotary_emb.forward(ids) img_cos, img_sin = rotary_emb.forward(img_ids)
return freqs_cis img_cos = shard_rotary_emb_for_sp(img_cos)
img_sin = shard_rotary_emb_for_sp(img_sin)
txt_cos, txt_sin = rotary_emb.forward(txt_ids)
cos = torch.cat([txt_cos, img_cos], dim=0).to(device=device)
sin = torch.cat([txt_sin, img_sin], dim=0).to(device=device)
return cos, sin
def post_denoising_loop(self, latents, batch): def post_denoising_loop(self, latents, batch):
# unpack latents for flux # unpack latents for flux
# VAE applies 8x compression on images but we must also account for packing which requires (
# latent height and width to be divisible by 2. latents,
batch_size = latents.shape[0] batch_size,
channels = latents.shape[-1] channels,
vae_scale_factor = self.vae_config.arch_config.vae_scale_factor height,
height = 2 * (int(batch.height) // (vae_scale_factor * 2)) width,
width = 2 * (int(batch.width) // (vae_scale_factor * 2)) ) = self._unpad_and_unpack_latents(latents, batch)
latents = latents.view(batch_size, height // 2, width // 2, channels // 4, 2, 2)
latents = latents.permute(0, 3, 1, 4, 2, 5)
latents = latents.reshape(batch_size, channels // (2 * 2), height, width) latents = latents.reshape(batch_size, channels // (2 * 2), height, width)
return latents return latents
@@ -10,8 +10,9 @@ from sglang.multimodal_gen.configs.models.dits.qwenimage import QwenImageDitConf
from sglang.multimodal_gen.configs.models.encoders.qwen_image import Qwen2_5VLConfig from sglang.multimodal_gen.configs.models.encoders.qwen_image import Qwen2_5VLConfig
from sglang.multimodal_gen.configs.models.vaes.qwenimage import QwenImageVAEConfig from sglang.multimodal_gen.configs.models.vaes.qwenimage import QwenImageVAEConfig
from sglang.multimodal_gen.configs.pipeline_configs.base import ( from sglang.multimodal_gen.configs.pipeline_configs.base import (
ImagePipelineConfig,
ModelTaskType, ModelTaskType,
PipelineConfig, shard_rotary_emb_for_sp,
) )
from sglang.multimodal_gen.utils import calculate_dimensions from sglang.multimodal_gen.utils import calculate_dimensions
@@ -64,9 +65,10 @@ def _pack_latents(latents, batch_size, num_channels_latents, height, width):
@dataclass @dataclass
class QwenImagePipelineConfig(PipelineConfig): class QwenImagePipelineConfig(ImagePipelineConfig):
should_use_guidance: bool = False """Configuration for the QwenImage pipeline."""
should_use_guidance: bool = False
task_type: ModelTaskType = ModelTaskType.T2I task_type: ModelTaskType = ModelTaskType.T2I
vae_tiling: bool = False vae_tiling: bool = False
@@ -105,15 +107,14 @@ class QwenImagePipelineConfig(PipelineConfig):
return self.vae_config.arch_config.vae_scale_factor return self.vae_config.arch_config.vae_scale_factor
def prepare_latent_shape(self, batch, batch_size, num_frames): def prepare_latent_shape(self, batch, batch_size, num_frames):
height = 2 * ( vae_scale_factor = self.vae_config.arch_config.vae_scale_factor
batch.height // (self.vae_config.arch_config.vae_scale_factor * 2) height = 2 * (batch.height // (vae_scale_factor * 2))
) width = 2 * (batch.width // (vae_scale_factor * 2))
width = 2 * (batch.width // (self.vae_config.arch_config.vae_scale_factor * 2))
num_channels_latents = self.dit_config.arch_config.in_channels // 4 num_channels_latents = self.dit_config.arch_config.in_channels // 4
shape = (batch_size, num_channels_latents, height, width) shape = (batch_size, 1, num_channels_latents, height, width)
return shape return shape
def pack_latents(self, latents, batch_size, batch): def maybe_pack_latents(self, latents, batch_size, batch):
height = 2 * ( height = 2 * (
batch.height // (self.vae_config.arch_config.vae_scale_factor * 2) batch.height // (self.vae_config.arch_config.vae_scale_factor * 2)
) )
@@ -124,6 +125,7 @@ class QwenImagePipelineConfig(PipelineConfig):
@staticmethod @staticmethod
def get_freqs_cis(img_shapes, txt_seq_lens, rotary_emb, device, dtype): def get_freqs_cis(img_shapes, txt_seq_lens, rotary_emb, device, dtype):
# img_shapes: for global entire image
img_freqs, txt_freqs = rotary_emb(img_shapes, txt_seq_lens, device=device) img_freqs, txt_freqs = rotary_emb(img_shapes, txt_seq_lens, device=device)
img_cos, img_sin = ( img_cos, img_sin = (
@@ -134,139 +136,128 @@ class QwenImagePipelineConfig(PipelineConfig):
txt_freqs.real.to(dtype=dtype), txt_freqs.real.to(dtype=dtype),
txt_freqs.imag.to(dtype=dtype), txt_freqs.imag.to(dtype=dtype),
) )
return (img_cos, img_sin), (txt_cos, txt_sin) return (img_cos, img_sin), (txt_cos, txt_sin)
def prepare_pos_cond_kwargs(self, batch, device, rotary_emb, dtype): def _prepare_cond_kwargs(self, batch, prompt_embeds, rotary_emb, device, dtype):
batch_size = batch.latents.shape[0] batch_size = prompt_embeds[0].shape[0]
height = batch.height
width = batch.width
vae_scale_factor = self.vae_config.arch_config.vae_scale_factor vae_scale_factor = self.vae_config.arch_config.vae_scale_factor
img_shapes = [ img_shapes = [
[ [
( (
1, 1,
batch.height // vae_scale_factor // 2, height // vae_scale_factor // 2,
batch.width // vae_scale_factor // 2, width // vae_scale_factor // 2,
) )
] ]
] * batch_size ] * batch_size
txt_seq_lens = [batch.prompt_embeds[0].shape[1]] txt_seq_lens = [prompt_embeds[0].shape[1]]
return {
"img_shapes": img_shapes, (img_cos, img_sin), (txt_cos, txt_sin) = self.get_freqs_cis(
"txt_seq_lens": txt_seq_lens,
"freqs_cis": QwenImagePipelineConfig.get_freqs_cis(
img_shapes, txt_seq_lens, rotary_emb, device, dtype img_shapes, txt_seq_lens, rotary_emb, device, dtype
), )
img_cos = shard_rotary_emb_for_sp(img_cos)
img_sin = shard_rotary_emb_for_sp(img_sin)
return {
"txt_seq_lens": txt_seq_lens,
"freqs_cis": ((img_cos, img_sin), (txt_cos, txt_sin)),
} }
def prepare_pos_cond_kwargs(self, batch, device, rotary_emb, dtype):
return self._prepare_cond_kwargs(
batch, batch.prompt_embeds, rotary_emb, device, dtype
)
def prepare_neg_cond_kwargs(self, batch, device, rotary_emb, dtype): def prepare_neg_cond_kwargs(self, batch, device, rotary_emb, dtype):
batch_size = batch.latents.shape[0] return self._prepare_cond_kwargs(
vae_scale_factor = self.vae_config.arch_config.vae_scale_factor batch, batch.negative_prompt_embeds, rotary_emb, device, dtype
img_shapes = [
[
(
1,
batch.height // vae_scale_factor // 2,
batch.width // vae_scale_factor // 2,
) )
]
] * batch_size
txt_seq_lens = [batch.negative_prompt_embeds[0].shape[1]]
return {
"img_shapes": img_shapes,
"txt_seq_lens": txt_seq_lens,
"freqs_cis": QwenImagePipelineConfig.get_freqs_cis(
img_shapes, txt_seq_lens, rotary_emb, device, dtype
),
}
def post_denoising_loop(self, latents, batch): def post_denoising_loop(self, latents, batch):
# VAE applies 8x compression on images but we must also account for packing which requires # unpack latents for qwen-image
# latent height and width to be divisible by 2. (
batch_size = latents.shape[0] latents,
channels = latents.shape[-1] batch_size,
vae_scale_factor = self.vae_config.arch_config.vae_scale_factor channels,
height = 2 * (int(batch.height) // (vae_scale_factor * 2)) height,
width = 2 * (int(batch.width) // (vae_scale_factor * 2)) width,
) = self._unpad_and_unpack_latents(latents, batch)
latents = latents.view(batch_size, height // 2, width // 2, channels // 4, 2, 2)
latents = latents.permute(0, 3, 1, 4, 2, 5)
latents = latents.reshape(batch_size, channels // (2 * 2), 1, height, width) latents = latents.reshape(batch_size, channels // (2 * 2), 1, height, width)
return latents return latents
class QwenImageEditPipelineConfig(QwenImagePipelineConfig): class QwenImageEditPipelineConfig(QwenImagePipelineConfig):
"""Configuration for the QwenImageEdit pipeline."""
task_type: ModelTaskType = ModelTaskType.I2I task_type: ModelTaskType = ModelTaskType.I2I
def prepare_pos_cond_kwargs(self, batch, device, rotary_emb, dtype): def _prepare_edit_cond_kwargs(
# TODO: lots of duplications here self, batch, prompt_embeds, rotary_emb, device, dtype
):
batch_size = batch.latents.shape[0] batch_size = batch.latents.shape[0]
assert batch_size == 1
height = batch.height height = batch.height
width = batch.width width = batch.width
image = batch.pil_image image = batch.pil_image
image_size = image[0].size if isinstance(image, list) else image.size image_size = image[0].size if isinstance(image, list) else image.size
calculated_width, calculated_height, _ = calculate_dimensions( edit_width, edit_height, _ = calculate_dimensions(
1024 * 1024, image_size[0] / image_size[1] 1024 * 1024, image_size[0] / image_size[1]
) )
vae_scale_factor = self.get_vae_scale_factor() vae_scale_factor = self.get_vae_scale_factor()
img_shapes = [ img_shapes = [
[ [
(1, height // vae_scale_factor // 2, width // vae_scale_factor // 2),
( (
1, 1,
calculated_height // vae_scale_factor // 2, height // vae_scale_factor // 2,
calculated_width // vae_scale_factor // 2, width // vae_scale_factor // 2,
), ),
] (
1,
edit_height // vae_scale_factor // 2,
edit_width // vae_scale_factor // 2,
),
],
] * batch_size ] * batch_size
txt_seq_lens = [batch.prompt_embeds[0].shape[1]] txt_seq_lens = [prompt_embeds[0].shape[1]]
return { (img_cos, img_sin), (txt_cos, txt_sin) = QwenImagePipelineConfig.get_freqs_cis(
"img_shapes": img_shapes,
"txt_seq_lens": txt_seq_lens,
"freqs_cis": QwenImagePipelineConfig.get_freqs_cis(
img_shapes, txt_seq_lens, rotary_emb, device, dtype img_shapes, txt_seq_lens, rotary_emb, device, dtype
), )
# perform sp shard on noisy image tokens
noisy_img_seq_len = (
1 * (height // vae_scale_factor // 2) * (width // vae_scale_factor // 2)
)
noisy_img_cos = shard_rotary_emb_for_sp(img_cos[:noisy_img_seq_len, :])
noisy_img_sin = shard_rotary_emb_for_sp(img_sin[:noisy_img_seq_len, :])
# concat back the img_cos for input image (since it is not sp-shared later)
img_cos = torch.cat([noisy_img_cos, img_cos[noisy_img_seq_len:, :]], dim=0).to(
device=device
)
img_sin = torch.cat([noisy_img_sin, img_sin[noisy_img_seq_len:, :]], dim=0).to(
device=device
)
return {
"txt_seq_lens": txt_seq_lens,
"freqs_cis": ((img_cos, img_sin), (txt_cos, txt_sin)),
} }
def prepare_pos_cond_kwargs(self, batch, device, rotary_emb, dtype):
return self._prepare_edit_cond_kwargs(
batch, batch.prompt_embeds, rotary_emb, device, dtype
)
def prepare_neg_cond_kwargs(self, batch, device, rotary_emb, dtype): def prepare_neg_cond_kwargs(self, batch, device, rotary_emb, dtype):
batch_size = batch.latents.shape[0] return self._prepare_edit_cond_kwargs(
height = batch.height batch, batch.negative_prompt_embeds, rotary_emb, device, dtype
width = batch.width
image = batch.pil_image
image_size = image[0].size if isinstance(image, list) else image.size
calculated_width, calculated_height, _ = calculate_dimensions(
1024 * 1024, image_size[0] / image_size[1]
) )
vae_scale_factor = self.get_vae_scale_factor()
img_shapes = [
[
(1, height // vae_scale_factor // 2, width // vae_scale_factor // 2),
(
1,
calculated_height // vae_scale_factor // 2,
calculated_width // vae_scale_factor // 2,
),
]
] * batch_size
txt_seq_lens = [batch.negative_prompt_embeds[0].shape[1]]
return {
"img_shapes": img_shapes,
"txt_seq_lens": txt_seq_lens,
"freqs_cis": QwenImagePipelineConfig.get_freqs_cis(
img_shapes, txt_seq_lens, rotary_emb, device, dtype
),
}
def prepare_latent_shape(self, batch, batch_size, num_frames):
vae_scale_factor = self.vae_config.arch_config.vae_scale_factor
height = 2 * (batch.height // (vae_scale_factor * 2))
width = 2 * (batch.width // (vae_scale_factor * 2))
num_channels_latents = self.dit_config.arch_config.in_channels // 4
shape = (batch_size, 1, num_channels_latents, height, width)
return shape
def preprocess_image(self, image, image_processor): def preprocess_image(self, image, image_processor):
image_size = image[0].size if isinstance(image, list) else image.size image_size = image[0].size if isinstance(image, list) else image.size
@@ -290,5 +281,6 @@ class QwenImageEditPipelineConfig(QwenImagePipelineConfig):
return width, height return width, height
def slice_noise_pred(self, noise, latents): def slice_noise_pred(self, noise, latents):
# remove noise over input image
noise = noise[:, : latents.size(1)] noise = noise[:, : latents.size(1)]
return noise return noise
@@ -507,6 +507,7 @@ class SamplingParams:
if user_params is None: if user_params is None:
return return
# user is not allowed to modify any param defined in the SamplingParams subclass
subclass_defined_fields = set(type(self).__annotations__.keys()) subclass_defined_fields = set(type(self).__annotations__.keys())
# Compare against current instance to avoid constructing a default instance # Compare against current instance to avoid constructing a default instance
@@ -284,7 +284,7 @@ class DiffGenerator:
# TODO: send batch when supported # TODO: send batch when supported
for request_idx, req in enumerate(requests): for request_idx, req in enumerate(requests):
logger.info( logger.info(
"Processing prompt %d/%d: %s...", "Processing prompt: %d/%d: %s",
request_idx + 1, request_idx + 1,
len(requests), len(requests),
req.prompt[:100], req.prompt[:100],
@@ -170,7 +170,7 @@ class UlyssesAttention_VSA(UlyssesAttention):
replicated_k: torch.Tensor | None = None, replicated_k: torch.Tensor | None = None,
replicated_v: torch.Tensor | None = None, replicated_v: torch.Tensor | None = None,
gate_compress: torch.Tensor | None = None, gate_compress: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]: ) -> torch.Tensor:
"""Forward pass for distributed attention. """Forward pass for distributed attention.
Args: Args:
@@ -212,16 +212,14 @@ class UlyssesAttention_VSA(UlyssesAttention):
q, k, v, gate_compress=gate_compress, attn_metadata=ctx_attn_metadata q, k, v, gate_compress=gate_compress, attn_metadata=ctx_attn_metadata
) # type: ignore[call-arg] ) # type: ignore[call-arg]
# Redistribute back if using sequence parallelism
replicated_output = None
# Apply backend-specific postprocess_output # Apply backend-specific postprocess_output
output = self.attn_impl.postprocess_output(output, ctx_attn_metadata) output = self.attn_impl.postprocess_output(output, ctx_attn_metadata)
output = sequence_model_parallel_all_to_all_4D( output = sequence_model_parallel_all_to_all_4D(
output, scatter_dim=1, gather_dim=2 output, scatter_dim=1, gather_dim=2
) )
return output, replicated_output
return output
class LocalAttention(nn.Module): class LocalAttention(nn.Module):
@@ -309,7 +307,7 @@ class USPAttention(nn.Module):
causal: bool = False, causal: bool = False,
supported_attention_backends: set[AttentionBackendEnum] | None = None, supported_attention_backends: set[AttentionBackendEnum] | None = None,
prefix: str = "", prefix: str = "",
dropout_p: float = 0.0, dropout_rate: float = 0.0,
**extra_impl_args, **extra_impl_args,
) -> None: ) -> None:
super().__init__() super().__init__()
@@ -341,7 +339,7 @@ class USPAttention(nn.Module):
self.backend = backend_name_to_enum(attn_backend.get_name()) self.backend = backend_name_to_enum(attn_backend.get_name())
self.dtype = dtype self.dtype = dtype
self.causal = causal self.causal = causal
self.dropout_p = dropout_p self.dropout_p = dropout_rate
def forward( def forward(
self, self,
@@ -351,7 +349,7 @@ class USPAttention(nn.Module):
replicated_q: torch.Tensor | None = None, replicated_q: torch.Tensor | None = None,
replicated_k: torch.Tensor | None = None, replicated_k: torch.Tensor | None = None,
replicated_v: torch.Tensor | None = None, replicated_v: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]: ) -> torch.Tensor:
""" """
Forward pass for USPAttention. Forward pass for USPAttention.
@@ -367,7 +365,7 @@ class USPAttention(nn.Module):
if get_sequence_parallel_world_size() == 1: if get_sequence_parallel_world_size() == 1:
# No sequence parallelism, just run local attention. # No sequence parallelism, just run local attention.
out = self.attn_impl.forward(q, k, v, ctx_attn_metadata) out = self.attn_impl.forward(q, k, v, ctx_attn_metadata)
return out, None return out
# Ulysses-style All-to-All for sequence/head sharding # Ulysses-style All-to-All for sequence/head sharding
if get_ulysses_parallel_world_size() > 1: if get_ulysses_parallel_world_size() > 1:
@@ -395,4 +393,4 @@ class USPAttention(nn.Module):
# -> [B, S_local, H, D] # -> [B, S_local, H, D]
out = _usp_output_all_to_all(out, head_dim=2) out = _usp_output_all_to_all(out, head_dim=2)
return out, None return out
@@ -32,7 +32,7 @@ from diffusers.models.normalization import (
from torch.nn import LayerNorm as LayerNorm from torch.nn import LayerNorm as LayerNorm
from sglang.multimodal_gen.configs.models.dits.flux import FluxConfig from sglang.multimodal_gen.configs.models.dits.flux import FluxConfig
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention from sglang.multimodal_gen.runtime.layers.attention import USPAttention
# from sglang.multimodal_gen.runtime.layers.layernorm import LayerNorm as LayerNorm # from sglang.multimodal_gen.runtime.layers.layernorm import LayerNorm as LayerNorm
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm
@@ -149,17 +149,17 @@ class FluxAttention(torch.nn.Module, AttentionModuleMixin):
self.to_add_out = ReplicatedLinear(self.inner_dim, query_dim, bias=out_bias) self.to_add_out = ReplicatedLinear(self.inner_dim, query_dim, bias=out_bias)
# Scaled dot product attention # Scaled dot product attention
self.attn = LocalAttention( self.attn = USPAttention(
num_heads=num_heads, num_heads=num_heads,
head_size=self.head_dim, head_size=self.head_dim,
dropout_rate=0, dropout_rate=0,
softmax_scale=None, softmax_scale=None,
causal=False, causal=False,
supported_attention_backends=( supported_attention_backends={
AttentionBackendEnum.FA, AttentionBackendEnum.FA,
AttentionBackendEnum.TORCH_SDPA, AttentionBackendEnum.TORCH_SDPA,
AttentionBackendEnum.SAGE_ATTN, AttentionBackendEnum.SAGE_ATTN,
), },
) )
def forward( def forward(
@@ -14,7 +14,7 @@ from diffusers.models.modeling_outputs import Transformer2DModelOutput
from diffusers.models.normalization import AdaLayerNormContinuous 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.layers.attention import LocalAttention from sglang.multimodal_gen.runtime.layers.attention import USPAttention
from sglang.multimodal_gen.runtime.layers.layernorm import LayerNorm, RMSNorm from sglang.multimodal_gen.runtime.layers.layernorm import LayerNorm, RMSNorm
from sglang.multimodal_gen.runtime.layers.linear import ReplicatedLinear from sglang.multimodal_gen.runtime.layers.linear import ReplicatedLinear
from sglang.multimodal_gen.runtime.layers.triton_ops import ( from sglang.multimodal_gen.runtime.layers.triton_ops import (
@@ -282,7 +282,7 @@ class QwenImageCrossAttention(nn.Module):
self.norm_added_k = RMSNorm(head_dim, eps=eps) self.norm_added_k = RMSNorm(head_dim, eps=eps)
# Scaled dot product attention # Scaled dot product attention
self.attn = LocalAttention( self.attn = USPAttention(
num_heads=num_heads, num_heads=num_heads,
head_size=self.head_dim, head_size=self.head_dim,
dropout_rate=0, dropout_rate=0,
@@ -301,7 +301,7 @@ class QwenImageCrossAttention(nn.Module):
image_rotary_emb: tuple[torch.Tensor, torch.Tensor], image_rotary_emb: tuple[torch.Tensor, torch.Tensor],
**cross_attention_kwargs, **cross_attention_kwargs,
): ):
seq_txt = encoder_hidden_states.shape[1] seq_len_txt = encoder_hidden_states.shape[1]
# Compute QKV for image stream (sample projections) # Compute QKV for image stream (sample projections)
img_query, _ = self.to_q(hidden_states) img_query, _ = self.to_q(hidden_states)
@@ -366,8 +366,8 @@ class QwenImageCrossAttention(nn.Module):
joint_hidden_states = joint_hidden_states.to(joint_query.dtype) joint_hidden_states = joint_hidden_states.to(joint_query.dtype)
# Split attention outputs back # Split attention outputs back
txt_attn_output = joint_hidden_states[:, :seq_txt, :] # Text part txt_attn_output = joint_hidden_states[:, :seq_len_txt, :] # Text part
img_attn_output = joint_hidden_states[:, seq_txt:, :] # Image part img_attn_output = joint_hidden_states[:, seq_len_txt:, :] # Image part
# Apply output projections # Apply output projections
img_attn_output, _ = self.to_out[0](img_attn_output) img_attn_output, _ = self.to_out[0](img_attn_output)
@@ -568,7 +568,6 @@ class QwenImageTransformer2DModel(CachableDiT):
encoder_hidden_states: torch.Tensor = None, encoder_hidden_states: torch.Tensor = None,
encoder_hidden_states_mask: torch.Tensor = None, encoder_hidden_states_mask: torch.Tensor = None,
timestep: torch.LongTensor = None, timestep: torch.LongTensor = None,
img_shapes: Optional[List[Tuple[int, int, int]]] = None,
txt_seq_lens: Optional[List[int]] = None, txt_seq_lens: Optional[List[int]] = None,
freqs_cis: tuple[torch.Tensor, torch.Tensor] = None, freqs_cis: tuple[torch.Tensor, torch.Tensor] = None,
guidance: torch.Tensor = None, # TODO: this should probably be removed guidance: torch.Tensor = None, # TODO: this should probably be removed
@@ -252,7 +252,7 @@ class SelfAttention(nn.Module):
q = self._apply_rope(q, cos, sin) q = self._apply_rope(q, cos, sin)
k = self._apply_rope(k, cos, sin) k = self._apply_rope(k, cos, sin)
output, _ = self.attn(q, k, v) # [B,heads,S,D] output = self.attn(q, k, v) # [B,heads,S,D]
output = rearrange(output, "b s h d -> b s (h d)") output = rearrange(output, "b s h d -> b s (h d)")
output, _ = self.wo(output) output, _ = self.wo(output)
@@ -13,7 +13,6 @@ from sglang.multimodal_gen.configs.models.dits import WanVideoConfig
from sglang.multimodal_gen.configs.sample.wan import WanTeaCacheParams from sglang.multimodal_gen.configs.sample.wan import WanTeaCacheParams
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 ( from sglang.multimodal_gen.runtime.layers.attention import (
LocalAttention,
UlyssesAttention_VSA, UlyssesAttention_VSA,
USPAttention, USPAttention,
) )
@@ -138,7 +137,7 @@ class WanSelfAttention(nn.Module):
self.norm_k = RMSNorm(dim, eps=eps) if qk_norm else nn.Identity() self.norm_k = RMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
# Scaled dot product attention # Scaled dot product attention
self.attn = LocalAttention( self.attn = USPAttention(
num_heads=num_heads, num_heads=num_heads,
head_size=self.head_dim, head_size=self.head_dim,
dropout_rate=0, dropout_rate=0,
@@ -391,7 +390,7 @@ class WanTransformerBlock(nn.Module):
query, key = _apply_rotary_emb( query, key = _apply_rotary_emb(
query, cos, sin, is_neox_style=False query, cos, sin, is_neox_style=False
), _apply_rotary_emb(key, cos, sin, is_neox_style=False) ), _apply_rotary_emb(key, cos, sin, is_neox_style=False)
attn_output, _ = self.attn1(query, key, value) attn_output = self.attn1(query, key, value)
attn_output = attn_output.flatten(2) attn_output = attn_output.flatten(2)
attn_output, _ = self.to_out(attn_output) attn_output, _ = self.to_out(attn_output)
attn_output = attn_output.squeeze(1) attn_output = attn_output.squeeze(1)
@@ -560,7 +559,7 @@ class WanTransformerBlock_VSA(nn.Module):
query, cos, sin, is_neox_style=False query, cos, sin, is_neox_style=False
), _apply_rotary_emb(key, cos, sin, is_neox_style=False) ), _apply_rotary_emb(key, cos, sin, is_neox_style=False)
attn_output, _ = self.attn1(query, key, value, gate_compress=gate_compress) attn_output = self.attn1(query, key, value, gate_compress=gate_compress)
attn_output = attn_output.flatten(2) attn_output = attn_output.flatten(2)
attn_output, _ = self.to_out(attn_output) attn_output, _ = self.to_out(attn_output)
attn_output = attn_output.squeeze(1) attn_output = attn_output.squeeze(1)
@@ -335,7 +335,7 @@ class DenoisingStage(PipelineStage):
) )
# Handle sequence parallelism AFTER TI2V processing # Handle sequence parallelism AFTER TI2V processing
self._preprocess_sp_latents(batch) self._preprocess_sp_latents(batch, server_args)
latents = batch.latents latents = batch.latents
# Shard z and reserved_frames_mask for TI2V if SP is enabled # Shard z and reserved_frames_mask for TI2V if SP is enabled
@@ -524,38 +524,29 @@ class DenoisingStage(PipelineStage):
torch.mps.current_allocated_memory(), torch.mps.current_allocated_memory(),
) )
def _preprocess_sp_latents(self, batch: Req): def _preprocess_sp_latents(self, batch: Req, server_args: ServerArgs):
"""Shard latents for Sequence Parallelism if applicable.""" """Shard latents for Sequence Parallelism if applicable."""
sp_world_size, rank_in_sp_group = get_sp_world_size(), get_sp_parallel_rank()
if get_sp_world_size() <= 1: if get_sp_world_size() <= 1:
batch.did_sp_shard_latents = False
return return
def _shard_tensor( if batch.latents is not None:
tensor: torch.Tensor | None, (
) -> tuple[torch.Tensor | None, bool]: batch.latents,
if tensor is None: did_shard,
return None, False ) = server_args.pipeline_config.shard_latents_for_sp(batch, batch.latents)
if tensor.dim() == 5:
time_dim = tensor.shape[2]
if time_dim > 0 and time_dim % sp_world_size == 0:
sharded_tensor = rearrange(
tensor, "b c (n t) h w -> b c n t h w", n=sp_world_size
).contiguous()
sharded_tensor = sharded_tensor[:, :, rank_in_sp_group, :, :, :]
return sharded_tensor, True
# For 4D image tensors or unsharded 5D tensors, return as is.
return tensor, False
batch.latents, did_shard = _shard_tensor(batch.latents)
batch.did_sp_shard_latents = did_shard batch.did_sp_shard_latents = did_shard
else:
batch.did_sp_shard_latents = False
# image_latent is sharded independently, but the decision to all-gather later # For I2I tasks like QwenImageEdit, the image_latent (input image) should be
# is based on whether the main `latents` was sharded. # replicated on all SP ranks, not sharded, as it provides global context.
if batch.image_latent is not None: if (
batch.image_latent, _ = _shard_tensor(batch.image_latent) server_args.pipeline_config.task_type != ModelTaskType.I2I
and batch.image_latent is not None
):
batch.image_latent, _ = server_args.pipeline_config.shard_latents_for_sp(
batch, batch.image_latent
)
def _postprocess_sp_latents( def _postprocess_sp_latents(
self, self,
@@ -565,13 +556,20 @@ class DenoisingStage(PipelineStage):
) -> tuple[torch.Tensor, torch.Tensor | None]: ) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Gather latents after Sequence Parallelism if they were sharded.""" """Gather latents after Sequence Parallelism if they were sharded."""
if get_sp_world_size() > 1 and getattr(batch, "did_sp_shard_latents", False): if get_sp_world_size() > 1 and getattr(batch, "did_sp_shard_latents", False):
latents = sequence_model_parallel_all_gather(latents, dim=2) latents = self.server_args.pipeline_config.gather_latents_for_sp(latents)
if trajectory_tensor is not None: if trajectory_tensor is not None:
# trajectory_tensor shape: [b, num_steps, c, t_local, h, w] -> gather on dim 3 # trajectory_tensor shapes:
# - video: [b, num_steps, c, t_local, h, w] -> gather on dim=3
# - image: [b, num_steps, s_local, d] -> gather on dim=2
trajectory_tensor = trajectory_tensor.to(get_local_torch_device()) trajectory_tensor = trajectory_tensor.to(get_local_torch_device())
gather_dim = 3 if trajectory_tensor.dim() >= 5 else 2
trajectory_tensor = sequence_model_parallel_all_gather( trajectory_tensor = sequence_model_parallel_all_gather(
trajectory_tensor, dim=3 trajectory_tensor, dim=gather_dim
) )
if gather_dim == 2 and hasattr(batch, "raw_latent_shape"):
orig_s = batch.raw_latent_shape[1]
if trajectory_tensor.shape[2] > orig_s:
trajectory_tensor = trajectory_tensor[:, :, :orig_s, :]
return latents, trajectory_tensor return latents, trajectory_tensor
def start_profile(self, batch: Req): def start_profile(self, batch: Req):
@@ -104,7 +104,7 @@ class ImageEncodingStage(PipelineStage):
image = batch.pil_image image = batch.pil_image
# preprocess the imag_processor # preprocess via vae_image_processor
prompt_image = server_args.pipeline_config.preprocess_image( prompt_image = server_args.pipeline_config.preprocess_image(
image, self.vae_image_processor image, self.vae_image_processor
) )
@@ -87,7 +87,7 @@ class LatentPreparationStage(PipelineStage):
latents = randn_tensor( latents = randn_tensor(
shape, generator=generator, device=device, dtype=dtype shape, generator=generator, device=device, dtype=dtype
) )
latents = server_args.pipeline_config.pack_latents( latents = server_args.pipeline_config.maybe_pack_latents(
latents, batch_size, batch latents, batch_size, batch
) )
else: else:
@@ -793,18 +793,6 @@ class ServerArgs:
return provided_args return provided_args
def check_server_sp_args(self): def check_server_sp_args(self):
if self.pipeline_config.task_type.is_image_gen():
if (
(self.sp_degree and self.sp_degree > 1)
or (self.ulysses_degree and self.ulysses_degree > 1)
or (self.ring_degree and self.ring_degree > 1)
):
raise ValueError(
"SP is not supported for image generation models for now"
)
self.sp_degree = self.ulysses_degree = self.ring_degree = 1
if self.sp_degree == -1: if self.sp_degree == -1:
# assume we leave all remaining gpus to sp # assume we leave all remaining gpus to sp
num_gpus_per_group = self.dp_size * self.tp_size num_gpus_per_group = self.dp_size * self.tp_size
@@ -861,8 +849,11 @@ class ServerArgs:
def check_server_dp_args(self): def check_server_dp_args(self):
assert self.num_gpus % self.dp_size == 0, f"{self.num_gpus=}, {self.dp_size=}" assert self.num_gpus % self.dp_size == 0, f"{self.num_gpus=}, {self.dp_size=}"
assert self.dp_size >= 1, "--dp-size must be natural number" assert self.dp_size >= 1, "--dp-size must be natural number"
self.dp_degree = self.num_gpus // self.dp_size # NOTE: disable temporarily
# self.dp_degree = self.num_gpus // self.dp_size
logger.info(f"Setting dp_degree to: {self.dp_degree}") logger.info(f"Setting dp_degree to: {self.dp_degree}")
if self.dp_size > 1:
raise ValueError("DP is not yet supported")
def check_server_args(self) -> None: def check_server_args(self) -> None:
"""Validate inference arguments for consistency""" """Validate inference arguments for consistency"""
@@ -920,18 +911,6 @@ class ServerArgs:
self.pipeline_config.check_pipeline_config() self.pipeline_config.check_pipeline_config()
# Add preprocessing config validation if needed
if self.mode == ExecutionMode.PREPROCESS:
if self.preprocess_config is None:
raise ValueError(
"preprocess_config is not set in ServerArgs when mode is PREPROCESS"
)
if self.preprocess_config.model_path == "":
self.preprocess_config.model_path = self.model_path
if not self.pipeline_config.vae_config.load_encoder:
self.pipeline_config.vae_config.load_encoder = True
self.preprocess_config.check_preprocess_config()
# parallelism # parallelism
self.check_server_dp_args() self.check_server_dp_args()
# allocate all remaining gpus for sp-size # allocate all remaining gpus for sp-size
@@ -15,25 +15,40 @@ class TestFlux_T2V(TestGenerateBase):
extra_args = [] extra_args = []
data_type: DataType = DataType.IMAGE data_type: DataType = DataType.IMAGE
thresholds = { thresholds = {
"test_single_gpu": 6.90 * 1.05, "test_single_gpu": 6.5 * 1.05,
"test_usp": 8.3 * 1.05,
} }
def test_cfg_parallel(self):
pass
def test_mixed(self):
pass
class TestQwenImage(TestGenerateBase): class TestQwenImage(TestGenerateBase):
model_path = "Qwen/Qwen-Image" model_path = "Qwen/Qwen-Image"
extra_args = [] extra_args = []
data_type: DataType = DataType.IMAGE data_type: DataType = DataType.IMAGE
thresholds = { thresholds = {
"test_single_gpu": 11.7 * 1.05, "test_single_gpu": 10.4 * 1.05,
"test_usp": 20.2 * 1.05,
} }
def test_cfg_parallel(self):
pass
def test_mixed(self):
pass
class TestQwenImageEdit(TestGenerateBase): class TestQwenImageEdit(TestGenerateBase):
model_path = "Qwen/Qwen-Image-Edit" model_path = "Qwen/Qwen-Image-Edit"
extra_args = [] extra_args = []
data_type: DataType = DataType.IMAGE data_type: DataType = DataType.IMAGE
thresholds = { thresholds = {
"test_single_gpu": 43.5 * 1.05, "test_single_gpu": 33.4 * 1.05,
"test_usp": 26.9 * 1.05,
} }
prompt: str | None = ( prompt: str | None = (
@@ -57,13 +72,11 @@ class TestQwenImageEdit(TestGenerateBase):
f"--output-path={self.output_path}", f"--output-path={self.output_path}",
] + [f"--image-path={img_path}"] ] + [f"--image-path={img_path}"]
def test_single_gpu(self): def test_cfg_parallel(self):
self._run_test( pass
name=f"{self.model_name()}_single_gpu",
args=None, def test_mixed(self):
model_path=self.model_path, pass
test_key="test_single_gpu",
)
if __name__ == "__main__": if __name__ == "__main__":
@@ -9,7 +9,7 @@
"denoise_stage": 0.05, "denoise_stage": 0.05,
"non_denoise_stage": 0.4, "non_denoise_stage": 0.4,
"denoise_step": 0.2, "denoise_step": 0.2,
"denoise_agg": 0.08 "denoise_agg": 0.1
}, },
"improvement_reporting": { "improvement_reporting": {
"threshold": 0.2 "threshold": 0.2
@@ -96,6 +96,72 @@
"49": 410.42 "49": 410.42
} }
}, },
"qwen_image_t2i_2_gpus": {
"stages_ms": {
"InputValidationStage": 0.04,
"TextEncodingStage": 693.2,
"ConditioningStage": 0.02,
"TimestepPreparationStage": 2.84,
"LatentPreparationStage": 9.13,
"DenoisingStage": 24529.77,
"DecodingStage": 612.79
},
"denoise_step_ms": {
"0": 405.94,
"1": 420.06,
"2": 414.79,
"3": 392.4,
"4": 408.14,
"5": 605.0,
"6": 469.39,
"7": 574.04,
"8": 539.61,
"9": 452.93,
"10": 279.36,
"11": 271.8,
"12": 438.26,
"13": 552.65,
"14": 576.1,
"15": 679.84,
"16": 543.0,
"17": 512.81,
"18": 522.27,
"19": 545.06,
"20": 545.85,
"21": 523.83,
"22": 519.36,
"23": 513.78,
"24": 532.54,
"25": 524.94,
"26": 542.59,
"27": 570.91,
"28": 568.73,
"29": 564.52,
"30": 564.57,
"31": 544.94,
"32": 496.81,
"33": 488.98,
"34": 457.18,
"35": 441.42,
"36": 437.44,
"37": 477.6,
"38": 429.17,
"39": 465.55,
"40": 448.25,
"41": 511.83,
"42": 450.6,
"43": 375.78,
"44": 504.4,
"45": 524.44,
"46": 535.22,
"47": 514.52,
"48": 431.58,
"49": 410.68
},
"expected_e2e_ms": 25850.45,
"expected_avg_denoise_ms": 490.43,
"expected_median_denoise_ms": 512.32
},
"flux_image_t2i": { "flux_image_t2i": {
"stages_ms": { "stages_ms": {
"InputValidationStage": 0.03, "InputValidationStage": 0.03,
@@ -162,6 +228,72 @@
"expected_avg_denoise_ms": 165.83, "expected_avg_denoise_ms": 165.83,
"expected_median_denoise_ms": 169.33 "expected_median_denoise_ms": 169.33
}, },
"flux_image_t2i_2_gpus": {
"stages_ms": {
"InputValidationStage": 0.03,
"TextEncodingStage": 74.47,
"ConditioningStage": 0.01,
"TimestepPreparationStage": 2.23,
"LatentPreparationStage": 6.17,
"DenoisingStage": 8400.49,
"DecodingStage": 381.56
},
"denoise_step_ms": {
"0": 166.27,
"1": 59.6,
"2": 167.31,
"3": 168.7,
"4": 168.83,
"5": 171.05,
"6": 174.64,
"7": 170.92,
"8": 169.69,
"9": 169.21,
"10": 167.71,
"11": 177.62,
"12": 166.44,
"13": 174.61,
"14": 170.43,
"15": 169.47,
"16": 167.24,
"17": 169.15,
"18": 169.51,
"19": 172.3,
"20": 172.19,
"21": 172.36,
"22": 168.39,
"23": 168.47,
"24": 170.55,
"25": 170.96,
"26": 168.43,
"27": 169.01,
"28": 169.62,
"29": 170.95,
"30": 171.83,
"31": 171.92,
"32": 170.1,
"33": 170.46,
"34": 169.91,
"35": 168.91,
"36": 170.27,
"37": 170.23,
"38": 169.62,
"39": 169.66,
"40": 169.57,
"41": 169.42,
"42": 168.59,
"43": 171.12,
"44": 169.6,
"45": 169.93,
"46": 171.23,
"47": 171.03,
"48": 170.14,
"49": 169.4
},
"expected_e2e_ms": 9006.3,
"expected_avg_denoise_ms": 167.89,
"expected_median_denoise_ms": 169.67
},
"qwen_image_edit_ti2i": { "qwen_image_edit_ti2i": {
"notes": "single uploaded reference image, Qwen/Qwen-Image-Edit", "notes": "single uploaded reference image, Qwen/Qwen-Image-Edit",
"expected_e2e_ms": 138500.0, "expected_e2e_ms": 138500.0,
@@ -465,197 +597,195 @@
}, },
"wan2_1_i2v_14b_480P_2gpu": { "wan2_1_i2v_14b_480P_2gpu": {
"stages_ms": { "stages_ms": {
"InputValidationStage": 32.94, "InputValidationStage": 33.57,
"TextEncodingStage": 2316.5, "TextEncodingStage": 2424.73,
"ImageEncodingStage": 3026.2, "ImageEncodingStage": 3462.55,
"ConditioningStage": 0.01, "ConditioningStage": 0.01,
"TimestepPreparationStage": 2.69, "TimestepPreparationStage": 2.69,
"LatentPreparationStage": 9.73, "LatentPreparationStage": 9.73,
"ImageVAEEncodingStage": 2290.98, "ImageVAEEncodingStage": 2290.98,
"DenoisingStage": 385080.09, "DenoisingStage": 414428.85,
"DecodingStage": 2984.69, "DecodingStage": 3016.1
"per_frame_generation": null
}, },
"denoise_step_ms": { "denoise_step_ms": {
"0": 8785.36, "0": 9304.67,
"1": 7644.16, "1": 8218.78,
"2": 7687.27, "2": 8269.27,
"3": 7703.9, "3": 8291.59,
"4": 7710.61, "4": 8308.29,
"5": 7716.32, "5": 8300.75,
"6": 7714.26, "6": 8302.76,
"7": 7711.27, "7": 8297.95,
"8": 7711.08, "8": 8295.26,
"9": 7706.57, "9": 8296.45,
"10": 7700.78, "10": 8287.48,
"11": 7696.03, "11": 8275.98,
"12": 7704.73, "12": 8281.9,
"13": 7699.99, "13": 8283.39,
"14": 7705.33, "14": 8264.96,
"15": 7701.11, "15": 8275.66,
"16": 7704.04, "16": 8271.89,
"17": 7695.31, "17": 8273.77,
"18": 7693.63, "18": 8279.34,
"19": 7686.34, "19": 8271.89,
"20": 7683.27, "20": 8265.83,
"21": 7689.82, "21": 8259.99,
"22": 7688.74, "22": 8260.36,
"23": 7686.01, "23": 8270.06,
"24": 7675.43, "24": 8271.58,
"25": 7679.86, "25": 8272.39,
"26": 7676.75, "26": 8267.87,
"27": 7671.65, "27": 8277.09,
"28": 7667.0, "28": 8264.49,
"29": 7669.83, "29": 8266.14,
"30": 7660.5, "30": 8263.67,
"31": 7666.82, "31": 8273.82,
"32": 7660.89, "32": 8260.5,
"33": 7668.75, "33": 8268.44,
"34": 7662.27, "34": 8253.2,
"35": 7659.71, "35": 8244.32,
"36": 7661.36, "36": 8258.15,
"37": 7664.87, "37": 8256.65,
"38": 7666.93, "38": 8255.48,
"39": 7661.05, "39": 8260.09,
"40": 7661.88, "40": 8250.99,
"41": 7657.96, "41": 8253.52,
"42": 7660.6, "42": 8247.39,
"43": 7669.82, "43": 8252.7,
"44": 7655.78, "44": 8243.67,
"45": 7654.25, "45": 8251.94,
"46": 7656.56, "46": 8258.73,
"47": 7652.37, "47": 8240.57,
"48": 7657.61, "48": 8249.64,
"49": 7644.6 "49": 8248.14
}, },
"expected_e2e_ms": 395758.23, "expected_e2e_ms": 425569.98,
"expected_avg_denoise_ms": 7701.42, "expected_avg_denoise_ms": 8288.39,
"expected_median_denoise_ms": 7676.09 "expected_median_denoise_ms": 8267.01
}, },
"wan2_1_i2v_14b_720P_2gpu": { "wan2_1_i2v_14b_720P_2gpu": {
"stages_ms": { "stages_ms": {
"InputValidationStage": 53.67, "InputValidationStage": 53.67,
"TextEncodingStage": 2838, "TextEncodingStage": 2838,
"ImageEncodingStage": 3123.99, "ImageEncodingStage": 3123.99,
"ConditioningStage": 0.02, "ConditioningStage": 0.01,
"TimestepPreparationStage": 3.39, "TimestepPreparationStage": 3.39,
"LatentPreparationStage": 6.68, "LatentPreparationStage": 8.41,
"ImageVAEEncodingStage": 2261.05, "ImageVAEEncodingStage": 2261.05,
"DenoisingStage": 386761.14, "DenoisingStage": 417418.12,
"DecodingStage": 2968.35, "DecodingStage": 2968.35
"per_frame_generation": null
}, },
"denoise_step_ms": { "denoise_step_ms": {
"0": 10021.98, "0": 11848.08,
"1": 7633.62, "1": 8220.3,
"2": 7676.46, "2": 8274.3,
"3": 7704.68, "3": 8298.9,
"4": 7725.09, "4": 8303.34,
"5": 7732.86, "5": 8322.44,
"6": 7735.42, "6": 8314.37,
"7": 7739.05, "7": 8318.54,
"8": 7740.89, "8": 8304.94,
"9": 7724.35, "9": 8303.04,
"10": 7730.2, "10": 8305.22,
"11": 7713.23, "11": 8296.22,
"12": 7715.93, "12": 8289.2,
"13": 7710.93, "13": 8294.19,
"14": 7699.95, "14": 8294.87,
"15": 7704.72, "15": 8285.96,
"16": 7704.03, "16": 8284.98,
"17": 7700.47, "17": 8281.61,
"18": 7702.0, "18": 8277.35,
"19": 7705.92, "19": 8287.46,
"20": 7704.35, "20": 8280.3,
"21": 7705.11, "21": 8279.18,
"22": 7693.85, "22": 8279.37,
"23": 7696.91, "23": 8280.16,
"24": 7689.6, "24": 8282.67,
"25": 7681.2, "25": 8272.14,
"26": 7675.63, "26": 8279.37,
"27": 7678.95, "27": 8271.66,
"28": 7683.82, "28": 8274.6,
"29": 7681.07, "29": 8272.88,
"30": 7671.07, "30": 8273.76,
"31": 7674.65, "31": 8266.17,
"32": 7679.56, "32": 8267.77,
"33": 7674.59, "33": 8266.88,
"34": 7672.16, "34": 8263.14,
"35": 7679.68, "35": 8265.97,
"36": 7670.81, "36": 8267.76,
"37": 7661.84, "37": 8268.03,
"38": 7668.58, "38": 8262.24,
"39": 7667.1, "39": 8261.4,
"40": 7670.22, "40": 8263.65,
"41": 7664.97, "41": 8272.46,
"42": 7667.3, "42": 8254.9,
"43": 7668.87, "43": 8261.03,
"44": 7663.43, "44": 8252.92,
"45": 7656.34, "45": 8262.49,
"46": 7662.81, "46": 8253.67,
"47": 7662.05, "47": 8254.92,
"48": 7654.13, "48": 8257.08,
"49": 7648.62 "49": 8236.56
}, },
"expected_e2e_ms": 397541.45, "expected_e2e_ms": 427536.9,
"expected_avg_denoise_ms": 7735.02, "expected_avg_denoise_ms": 8348.21,
"expected_median_denoise_ms": 7681.14 "expected_median_denoise_ms": 8274.45
}, },
"wan2_2_t2v_a14b_2gpu": { "wan2_2_t2v_a14b_2gpu": {
"stages_ms": { "stages_ms": {
"InputValidationStage": 0.09, "InputValidationStage": 0.07,
"TextEncodingStage": 2322.57, "TextEncodingStage": 2507.83,
"ConditioningStage": 0.03, "ConditioningStage": 0.02,
"TimestepPreparationStage": 2.29, "TimestepPreparationStage": 3.22,
"LatentPreparationStage": 3.08, "LatentPreparationStage": 2.99,
"DenoisingStage": 79913.08, "DenoisingStage": 103136.69,
"DecodingStage": 1339.58 "DecodingStage": 1431.71
}, },
"denoise_step_ms": { "denoise_step_ms": {
"0": 19269.37, "0": 24471.86,
"1": 691.64, "1": 757.31,
"2": 699.28, "2": 760.07,
"3": 696.55, "3": 758.74,
"4": 698.6, "4": 762.4,
"5": 704.56, "5": 755.83,
"6": 699.26, "6": 760.06,
"7": 700.84, "7": 756.38,
"8": 700.27, "8": 755.38,
"9": 704.15, "9": 754.25,
"10": 699.04, "10": 754.51,
"11": 704.79, "11": 753.46,
"12": 701.48, "12": 753.67,
"13": 707.24, "13": 753.08,
"14": 697.54, "14": 754.83,
"15": 698.89, "15": 753.04,
"16": 697.97, "16": 754.28,
"17": 699.34, "17": 754.45,
"18": 697.68, "18": 758.19,
"19": 697.42, "19": 756.23,
"20": 697.14, "20": 755.14,
"21": 700.14, "21": 755.92,
"22": 696.75, "22": 759.52,
"23": 702.36, "23": 762.09,
"24": 697.3, "24": 756.8,
"25": 703.97, "25": 758.86,
"26": 33676.93, "26": 48787.27,
"27": 700.4, "27": 758.5,
"28": 703.68, "28": 757.57,
"29": 691.86, "29": 757.16,
"30": 706.1, "30": 758.43,
"31": 704.18, "31": 763.31,
"32": 700.34, "32": 753.69,
"33": 698.62, "33": 754.91,
"34": 698.66, "34": 752.03,
"35": 699.77, "35": 763.65,
"36": 700.96, "36": 760.96,
"37": 701.02, "37": 754.31,
"38": 703.98, "38": 753.64,
"39": 702.18 "39": 756.95
}, },
"expected_e2e_ms": 83595.94, "expected_e2e_ms": 106895.63,
"expected_avg_denoise_ms": 1988.81, "expected_avg_denoise_ms": 2550.47,
"expected_median_denoise_ms": 700.2 "expected_median_denoise_ms": 756.59
}, },
"wan2_1_t2v_14b_2gpu": { "wan2_1_t2v_14b_2gpu": {
"stages_ms": { "stages_ms": {
@@ -132,7 +132,7 @@ class ServerManager:
env["SGLANG_PERF_LOG_DIR"] = log_dir.as_posix() env["SGLANG_PERF_LOG_DIR"] = log_dir.as_posix()
# TODO: unify with run_command # TODO: unify with run_command
print(f"Running command: {shlex.join(command)}") logger.info(f"Running command: {shlex.join(command)}")
process = subprocess.Popen( process = subprocess.Popen(
command, command,
@@ -369,6 +369,25 @@ TWO_GPU_CASES = [
custom_validator="video", custom_validator="video",
num_gpus=2, num_gpus=2,
), ),
DiffusionTestCase(
id="qwen_image_t2i_2_gpus",
model_path="Qwen/Qwen-Image",
modality="image",
prompt="A futuristic cityscape at sunset with flying cars",
output_size="1024x1024",
warmup_text=1,
warmup_edit=0,
num_gpus=2,
),
DiffusionTestCase(
id="flux_image_t2i_2_gpus",
model_path="black-forest-labs/FLUX.1-dev",
modality="image",
prompt="A futuristic cityscape at sunset with flying cars",
output_size="1024x1024",
warmup_text=1,
warmup_edit=0,
),
] ]
# Load global configuration # Load global configuration
@@ -385,8 +385,6 @@ class TestGenerateBase(TestCLIBase):
def test_cfg_parallel(self): def test_cfg_parallel(self):
"""cfg parallel""" """cfg parallel"""
if self.data_type == DataType.IMAGE:
return
self._run_test( self._run_test(
name=f"{self.model_name()}_cfg_parallel", name=f"{self.model_name()}_cfg_parallel",
args="--num-gpus 2 --enable-cfg-parallel", args="--num-gpus 2 --enable-cfg-parallel",
@@ -396,8 +394,6 @@ class TestGenerateBase(TestCLIBase):
def test_usp(self): def test_usp(self):
"""usp""" """usp"""
if self.data_type == DataType.IMAGE:
return
self._run_test( self._run_test(
name=f"{self.model_name()}_usp", name=f"{self.model_name()}_usp",
args="--num-gpus 4 --ulysses-degree=2 --ring-degree=2", args="--num-gpus 4 --ulysses-degree=2 --ring-degree=2",
@@ -407,8 +403,6 @@ class TestGenerateBase(TestCLIBase):
def test_mixed(self): def test_mixed(self):
"""mixed""" """mixed"""
if self.data_type == DataType.IMAGE:
return
self._run_test( self._run_test(
name=f"{self.model_name()}_mixed", name=f"{self.model_name()}_mixed",
args="--num-gpus 4 --ulysses-degree=2 --ring-degree=1 --enable-cfg-parallel", args="--num-gpus 4 --ulysses-degree=2 --ring-degree=1 --enable-cfg-parallel",