[Perf] Support request-level batching for Wan2.2 pipelines (#5676)

Signed-off-by: nagisa-kun <1434936049@qq.com>
Signed-off-by: nagisa <1434936049@qq.com>
This commit is contained in:
hurukawa authored and GitHub committed 2026-08-15 23:58:36 +08:00
1 parent d1e230c95b
commit 2a315e1a2d
18 files changed
+1871 -374

No files matched your search

@@ -37,7 +37,7 @@ from vllm_omni.diffusion.models.wan2_2.wan2_2_transformer import WanTransformer3
from vllm_omni.diffusion.postprocess import interpolate_video_tensor
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch, split_diffusion_output_by_request
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
from vllm_omni.platforms import current_omni_platform
@@ -204,6 +204,8 @@ def get_wan22_post_process_func(
output_type: str = "np",
sampling_params=None,
):
if sampling_params is not None and sampling_params.output_type is not None:
output_type = sampling_params.output_type
if output_type == "latent":
return video
video_metadata = {}
@@ -233,6 +235,8 @@ def get_wan22_pre_process_func(
prompt = request.prompt
multi_modal_data = prompt.get("multi_modal_data", {}) if not isinstance(prompt, str) else None
raw_image = multi_modal_data.get("image", None) if multi_modal_data is not None else None
has_image = raw_image is not None and (not isinstance(raw_image, list) or bool(raw_image))
request.batch_compatibility_key = ("wan22_image_condition", has_image)
if isinstance(prompt, str):
prompt = OmniTextPrompt(prompt=prompt)
if "additional_information" not in prompt:
@@ -288,6 +292,7 @@ class Wan22Pipeline(
SupportsComponentDiscovery,
WanLoraLoaderMixin,
):
supports_request_batch = True
_dit_modules: ClassVar[list[str]] = ["transformer", "transformer_2"]
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
_vae_modules: ClassVar[list[str]] = ["vae"]
@@ -561,27 +566,33 @@ class Wan22Pipeline(
return latents
def forward(self, req: DiffusionRequestBatch) -> DiffusionOutput:
prompt: str | None = None
negative_prompt: str | None = None
prompt_embeds: torch.Tensor | None = None
negative_prompt_embeds: torch.Tensor | None = None
if len(req.prompts) > 1:
raise ValueError(
"""This model only supports a single prompt, not a batched request.""",
"""Please pass in a single prompt object or string, or a single-item list.""",
)
if len(req.prompts) == 1:
first_prompt = req.prompts[0]
prompt = first_prompt if isinstance(first_prompt, str) else (first_prompt.get("prompt") or "")
negative_prompt = None if isinstance(first_prompt, str) else first_prompt.get("negative_prompt")
def forward(self, req: DiffusionRequestBatch) -> list[DiffusionOutput]:
sampling_params_list = req.sampling_params_list
common = sampling_params_list[0]
prompt_texts = [prompt if isinstance(prompt, str) else (prompt.get("prompt") or "") for prompt in req.prompts]
negative_prompts = [
None if isinstance(prompt, str) else prompt.get("negative_prompt") for prompt in req.prompts
]
prompt_fields = DiffusionRequestBatch.collate_prompt_field_map(
req.prompts,
{
"prompt_embeds": None,
"negative_prompt_embeds": None,
},
)
prompt_embeds = prompt_fields["prompt_embeds"]
negative_prompt_embeds = prompt_fields["negative_prompt_embeds"]
prompt: list[str] | None = prompt_texts if prompt_embeds is None else None
negative_prompt: list[str] | None = None
if negative_prompt_embeds is None and any(value is not None for value in negative_prompts):
negative_prompt = [value or "" for value in negative_prompts]
if not prompt:
raise ValueError("Prompt is required for Wan2.2 generation.")
if prompt is not None and not all(prompt):
raise ValueError("Prompt is required for Wan2.2 generation when prompt_embeds are not provided.")
height = req.sampling_params.height or 480
width = req.sampling_params.width or 832
num_frames = req.sampling_params.num_frames or 81
height = common.height or 480
width = common.width or 832
num_frames = common.num_frames or 81
# Ensure dimensions are compatible with VAE and patch size
# For expand_timesteps mode, we need latent dims to be even (divisible by patch_size)
@@ -589,19 +600,20 @@ class Wan22Pipeline(
mod_value = self.vae_scale_factor_spatial * patch_size[1] # 16*2=32 for TI2V, 8*2=16 for I2V
height = (height // mod_value) * mod_value
width = (width // mod_value) * mod_value
num_steps = 40 if req.sampling_params.num_inference_steps is None else req.sampling_params.num_inference_steps
num_steps = 40 if common.num_inference_steps is None else common.num_inference_steps
output_type = req.sampling_params.output_type or "np"
output_type = common.output_type or "np"
num_outputs_per_prompt = common.num_outputs_per_prompt or 1
attention_kwargs: dict | None = None
guidance_low, guidance_high = resolve_wan_guidance_scales(req.sampling_params, default_guidance_scale=4.0)
guidance_low, guidance_high = resolve_wan_guidance_scales(common, default_guidance_scale=4.0)
# record guidance for properties
self._guidance_scale = guidance_low
self._guidance_scale_2 = guidance_high
# Prefer engine-configured boundary_ratio, but allow per-request fallback.
boundary_ratio = self.boundary_ratio if self.boundary_ratio is not None else req.sampling_params.boundary_ratio
boundary_ratio = self.boundary_ratio if self.boundary_ratio is not None else common.boundary_ratio
if boundary_ratio is None:
boundary_ratio = 0.875
@@ -633,32 +645,49 @@ class Wan22Pipeline(
# Fallback to text_encoder dtype if no transformer loaded
dtype = self.text_encoder.dtype
# Seed / generator
generator = req.sampling_params.generator
if generator is None and req.sampling_params.seed is not None:
generator = torch.Generator(device=device).manual_seed(req.sampling_params.seed)
generator = req.collate_request_generators(num_outputs_per_prompt, None)
request_latents = req.collate_request_tensors("latents", None)
if DEBUG_PERF:
# Sync GPU before timing to ensure accurate measurements
current_omni_platform.synchronize()
_t_pipeline_start = time.perf_counter()
_t_text_enc_start = _t_pipeline_start
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
prompt=prompt,
negative_prompt=negative_prompt,
do_classifier_free_guidance=guidance_low > 1.0 or guidance_high > 1.0,
num_videos_per_prompt=req.sampling_params.num_outputs_per_prompt or 1,
max_sequence_length=req.sampling_params.max_sequence_length or 512,
device=device,
dtype=dtype,
)
do_classifier_free_guidance = guidance_low > 1.0 or guidance_high > 1.0
if prompt_embeds is None:
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
prompt=prompt,
negative_prompt=negative_prompt,
do_classifier_free_guidance=do_classifier_free_guidance,
num_videos_per_prompt=num_outputs_per_prompt,
max_sequence_length=common.max_sequence_length or 512,
device=device,
dtype=dtype,
)
else:
prompt_embeds = prompt_embeds.to(device=device, dtype=dtype)
prompt_embeds = prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
if negative_prompt_embeds is not None:
negative_prompt_embeds = negative_prompt_embeds.to(device=device, dtype=dtype)
negative_prompt_embeds = negative_prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
elif do_classifier_free_guidance:
_, negative_prompt_embeds = self.encode_prompt(
prompt=[""] * req.num_reqs,
negative_prompt=negative_prompt,
do_classifier_free_guidance=True,
num_videos_per_prompt=num_outputs_per_prompt,
max_sequence_length=common.max_sequence_length or 512,
device=device,
dtype=dtype,
)
if DEBUG_PERF:
current_omni_platform.synchronize()
_t_text_enc_ms = (time.perf_counter() - _t_text_enc_start) * 1000
sample_solver = resolve_wan_sample_solver(req, default=self._sample_solver)
flow_shift = resolve_wan_flow_shift(req, self.od_config)
first_request = req.requests[0]
sample_solver = resolve_wan_sample_solver(first_request, default=self._sample_solver)
flow_shift = resolve_wan_flow_shift(first_request, self.od_config)
if sample_solver != self._sample_solver or abs(flow_shift - self._flow_shift) > 1e-6:
self.scheduler = build_wan_scheduler(sample_solver, flow_shift)
self._sample_solver = sample_solver
@@ -674,37 +703,41 @@ class Wan22Pipeline(
if DEBUG_PERF:
_t_latent_prep_start = time.perf_counter()
multi_modal_data = req.prompts[0].get("multi_modal_data", {}) if not isinstance(req.prompts[0], str) else None
raw_image = multi_modal_data.get("image", None) if multi_modal_data is not None else None
if isinstance(raw_image, list):
if len(raw_image) > 1:
logger.warning(
"""Received a list of image. Only a single image is supported by this model."""
"""Taking only the first image for now."""
)
raw_image = raw_image[0]
if raw_image is None:
image = None
elif isinstance(raw_image, str):
image = PIL.Image.open(raw_image)
else:
image = cast(PIL.Image.Image | torch.Tensor, raw_image)
images: list[PIL.Image.Image | torch.Tensor | None] = []
for request_prompt in req.prompts:
multi_modal_data = request_prompt.get("multi_modal_data", {}) if not isinstance(request_prompt, str) else {}
raw_image = multi_modal_data.get("image")
if isinstance(raw_image, list):
if len(raw_image) > 1:
logger.warning("Received multiple images for one Wan request; using only the first image.")
raw_image = raw_image[0] if raw_image else None
if isinstance(raw_image, str):
raw_image = PIL.Image.open(raw_image)
images.append(cast(PIL.Image.Image | torch.Tensor | None, raw_image))
latent_condition = None
first_frame_mask = None
if self.expand_timesteps and image is not None:
if self.expand_timesteps and any(image is not None for image in images):
if not all(image is not None for image in images):
raise ValueError("Cannot batch Wan requests with a mix of provided and missing image conditions.")
# I2V mode: encode image and prepare condition
from diffusers.video_processor import VideoProcessor
video_processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)
# Preprocess image
if isinstance(image, PIL.Image.Image):
image = image.resize((width, height), PIL.Image.Resampling.LANCZOS)
image_tensor = video_processor.preprocess(image, height=height, width=width)
else:
image_tensor = image
image_tensors = []
for image in images:
assert image is not None
if isinstance(image, PIL.Image.Image):
image = image.resize((width, height), PIL.Image.Resampling.LANCZOS)
image_tensor = video_processor.preprocess(image, height=height, width=width)
else:
image_tensor = image.unsqueeze(0) if image.ndim == 3 else image
image_tensors.append(image_tensor)
image_tensor = DiffusionRequestBatch.collate_tensors(image_tensors, "image condition", None)
assert image_tensor is not None
image_tensor = image_tensor.repeat_interleave(num_outputs_per_prompt, dim=0)
# Use out_channels for noise latents (not in_channels which includes condition)
num_channels_latents = self.transformer_config.out_channels
@@ -720,7 +753,7 @@ class Wan22Pipeline(
dtype=torch.float32,
device=device,
generator=generator,
latents=req.sampling_params.latents,
latents=request_latents,
)
# Encode image condition
@@ -731,7 +764,6 @@ class Wan22Pipeline(
image_tensor = image_tensor.unsqueeze(2) # [B, C, 1, H, W]
image_tensor = image_tensor.to(device=device, dtype=self.vae.dtype)
latent_condition = retrieve_latents(self.vae.encode(image_tensor), sample_mode="argmax")
latent_condition = latent_condition.repeat(batch_size, 1, 1, 1, 1)
# Normalize condition latents
latents_mean = (
@@ -747,7 +779,7 @@ class Wan22Pipeline(
# Create mask: 0 for first frame (condition), 1 for rest (to denoise)
first_frame_mask = torch.ones(
1, 1, num_latent_frames, latent_height, latent_width, dtype=torch.float32, device=device
batch_size, 1, num_latent_frames, latent_height, latent_width, dtype=torch.float32, device=device
)
first_frame_mask[:, :, 0] = 0
else:
@@ -762,7 +794,7 @@ class Wan22Pipeline(
dtype=torch.float32,
device=device,
generator=generator,
latents=req.sampling_params.latents,
latents=request_latents,
)
if DEBUG_PERF:
current_omni_platform.synchronize()
@@ -839,8 +871,13 @@ class Wan22Pipeline(
_t_pipeline_wall_ms - _t_stages_sum,
)
return DiffusionOutput(
output=output, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None
return split_diffusion_output_by_request(
DiffusionOutput(
output=output,
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
),
req,
num_outputs_per_prompt=num_outputs_per_prompt,
)
def predict_noise(