[bugfix][CI] Diffusers backend update (#3096)

Signed-off-by: Huang, Zeyu <11222265+fhfuih@users.noreply.github.com>
Signed-off-by: Zeyu Huang | 黃澤宇 <11222265+fhfuih@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Zeyu Huang | 黃澤宇andCopilot Autofix powered by AI authored and GitHub committed 2026-05-05 20:54:39 +08:00
1 parent d0b531520d
commit f19891e4f1
18 files changed
+1366 -451

No files matched your search

@@ -13,18 +13,21 @@ It does NOT support:
- Step-wise execution (continuous batching)
"""
import inspect
import logging
import os
from typing import Any
import re
from typing import Any, cast
import torch
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
from torch import nn
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.models.diffusers_adapter.pipeline_utils import BasePipelineUtils, get_pipeline_utils
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.inputs.data import OmniPromptType
from vllm_omni.inputs.data import OmniPromptType, OmniTextPrompt
from vllm_omni.platforms import current_omni_platform
logger = logging.getLogger(__name__)
@@ -49,9 +52,11 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
def __init__(self, *, od_config: OmniDiffusionConfig, device: torch.device | None = None):
super().__init__()
self._pipeline: DiffusionPipeline
self._accept_call_kwargs: set[str] | None = None # None to accept all kwargs
self.od_config = od_config
self.device = device
self._capabilities: dict[str, Any] = {}
self._pipeline_utils: BasePipelineUtils = BasePipelineUtils()
self._raise_unsupported_features()
self.setup_diffusion_pipeline_profiler(
@@ -77,10 +82,18 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
}
logger.debug(f"Loading diffusers pipeline with kwargs: {load_kwargs}")
self._pipeline = DiffusionPipeline.from_pretrained(
model_id,
**load_kwargs,
).to(self.device)
pipeline_class = self.od_config.diffusers_pipeline_cls
pipeline_class_name = pipeline_class.__name__ if pipeline_class is not None else None
self._pipeline_utils = get_pipeline_utils(pipeline_class_name)
self._pipeline_utils.update_load_kwargs(self.od_config, load_kwargs)
self._pipeline = DiffusionPipeline.from_pretrained(model_id, **load_kwargs)
self._pipeline_utils.apply_post_load_updates(self._pipeline, self.od_config)
self._pipeline.to(self.device)
# Cache __call__kwargs signature introspection for later input validation
self._accept_call_kwargs = set(inspect.signature(self._pipeline.__call__).parameters.keys())
# CPU offloading
if self.od_config.enable_layerwise_offload:
@@ -270,30 +283,50 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
def _build_call_kwargs(self, req: OmniDiffusionRequest) -> dict[str, Any]:
"""Translate ``OmniDiffusionRequest`` into diffusers ``__call__`` kwargs."""
sampling = req.sampling_params
prompt, neg_prompt = self._extract_prompt(req.prompts)
input_kwargs = self._extract_input(req.prompts)
self._pipeline_utils.validate_runtime_sampling_params(sampling)
# Merge user-provided call kwargs from stage/CLI defaults.
# Request-time parameters take precedence over stage-config defaults
call_kwargs = self.od_config.diffusers_call_kwargs
kwargs: dict[str, Any] = {
**call_kwargs,
"prompt": prompt,
"num_inference_steps": sampling.num_inference_steps,
"guidance_scale": sampling.guidance_scale,
"output_type": sampling.output_type or self.od_config.output_type,
}
# Load time defaults -> input kwargs (prompts, neg prompts, images...) -> request-time sampling params
kwargs: dict[str, Any] = {}
if sampling.height is not None:
kwargs["height"] = sampling.height
if sampling.width is not None:
kwargs["width"] = sampling.width
if sampling.num_frames is not None and sampling.num_frames > 1:
kwargs["num_frames"] = sampling.num_frames
if sampling.num_outputs_per_prompt is not None and sampling.num_outputs_per_prompt > 1:
kwargs["num_images_per_prompt"] = sampling.num_outputs_per_prompt
# Load time defaults
for key, value in self.od_config.diffusers_call_kwargs.items():
if self._accept_call_kwargs is None or key in self._accept_call_kwargs:
kwargs[key] = value
else:
logger.warning(
f"Skipping unsupported diffusers pipeline __call__ argument `{key}` from "
f"diffusers_call_kwargs. Check out the documentation of {self._pipeline.__class__.__name__}."
)
if neg_prompt is not None:
kwargs["negative_prompt"] = neg_prompt
# Input kwargs
for key, value in input_kwargs.items():
if self._accept_call_kwargs is None or key in self._accept_call_kwargs:
kwargs[key] = value
else:
logger.warning(
f"Skipping unsupported diffusers pipeline __call__ argument `{key}` from prompt input."
f"Check out the documentation of {self._pipeline.__class__.__name__}."
)
# Request-time sampling params
for key, value in sampling.__dict__.items():
if value is None:
continue
if self._accept_call_kwargs is None or key in self._accept_call_kwargs:
kwargs[key] = value
# Special format fields in sampling params
if output_type := sampling.output_type or self.od_config.output_type:
kwargs["output_type"] = output_type
if (num_outputs_per_prompt := sampling.num_outputs_per_prompt) > 0:
# In diffusers, they are num_images_per_prompt, num_videos_per_prompt, etc.
for key in self._accept_call_kwargs or ():
if re.match(r"num_[a-z]+_per_prompt", key):
kwargs[key] = num_outputs_per_prompt
if sampling.generator is not None:
kwargs["generator"] = sampling.generator
@@ -302,38 +335,100 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
else:
kwargs["generator"] = torch.Generator(device=sampling.generator_device)
if sampling.latents is not None:
kwargs["latents"] = sampling.latents
logger.info(
"Calling diffusers pipeline with kwargs: %s", DiffusersAdapterPipeline._summarize_call_kwargs_value(kwargs)
)
return kwargs
@staticmethod
def _extract_prompt(prompt_obj: list[OmniPromptType]) -> tuple[str | list[str], str | list[str] | None]:
def _extract_input(self, prompt_obj: list[OmniPromptType]) -> dict[str, Any]:
"""Extract the text prompts and negative prompts from a list of prompt objects."""
if len(prompt_obj) == 1:
if isinstance(prompt_obj[0], str):
return prompt_obj[0], None
return {"prompt": prompt_obj[0]}
else:
return prompt_obj[0].get("prompt", ""), prompt_obj[0].get("negative_prompt", None)
obj = cast(OmniTextPrompt, prompt_obj[0])
negative_prompt = obj.get("negative_prompt")
multi_modal_data = obj.get("multi_modal_data") or {}
kwargs = {
"prompt": obj.get("prompt", ""),
**multi_modal_data,
}
if negative_prompt is not None:
kwargs["negative_prompt"] = negative_prompt
return kwargs
# Check the first element for the presence of multimodal data.
# The following elements should have the same multimodal data fields, or none has multimodal data.
multi_modal_data_fields: list[str] = []
if isinstance(prompt_obj[0], dict) and (multi_modal_data := prompt_obj[0].get("multi_modal_data")):
multi_modal_data_fields = list(multi_modal_data.keys())
if multi_modal_data_fields:
for i, prompt in enumerate(prompt_obj):
assert isinstance(prompt, dict) and (multi_modal_data := prompt.get("multi_modal_data")) is not None, (
"When there are multiple prompts and the first prompt has multimodal data, "
f"each prompt should also contain the same multimodal data fields, but prompt {i} does not."
)
for key in multi_modal_data_fields:
assert key in multi_modal_data, (
"When there are multiple prompts and the first prompt has multimodal data, each prompt should "
f"also contain the same multimodal data fields, but prompt {i} does not contain {key}."
)
assert not isinstance(multi_modal_data.get(key), list), (
f"When there are multiple prompts and each prompt has multiple {key} data, this input pattern "
"is ambiguous as diffusers accepts flattened lists of text prompts and multimodal data. "
f"To use multiple {key} data, please use one single prompt instead."
)
input_kwargs = {"prompt": [], **{key: [] for key in multi_modal_data_fields}}
# Negative prompt rule:
# - If any OmniTextPrompt has a negative prompt, or diffusers_call_kwargs has `negative_prompt`,
# enforce a negative prompt input (list[str]) -> `kwargs_should_contain_negative_prompt=true`
# (Because the negative prompt must be str, list[str], or None. It cannot be list[str|None])
# - Further in this case, try:
# - 1. negative prompt in this OmniTextPrompt (typed dict)
# - 2. fallback negative prompt from diffusers_call_kwargs (single str or the i-th item in list[str])
# - 3. empty string ""
# - Otherwise, `kwargs_should_contain_negative_prompt=False`. Do not add "negative_prompt" key to input_kwargs.
has_negative_prompt = any(isinstance(p, dict) and p.get("negative_prompt") is not None for p in prompt_obj)
fallback_negative_prompt = self.od_config.diffusers_call_kwargs.get("negative_prompt")
kwargs_should_contain_negative_prompt = has_negative_prompt or fallback_negative_prompt is not None
if kwargs_should_contain_negative_prompt:
input_kwargs["negative_prompt"] = []
for i, prompt in enumerate(prompt_obj):
this_fallback_negative_prompt = ""
if isinstance(fallback_negative_prompt, str):
this_fallback_negative_prompt = fallback_negative_prompt
elif isinstance(fallback_negative_prompt, list):
try:
this_fallback_negative_prompt = fallback_negative_prompt[i]
except IndexError:
raise ValueError(
"The fallback negative_prompt in diffusers_call_kwargs is a list, but its length "
f"({len(fallback_negative_prompt)}) is less than the number of prompts ({len(prompt_obj)}). "
"Please provide a list with the same length as the number of prompts."
)
prompts = []
negative_prompts: list[str] | None = []
for prompt in prompt_obj:
if isinstance(prompt, str):
prompts.append(prompt)
input_kwargs["prompt"].append(prompt)
if kwargs_should_contain_negative_prompt:
input_kwargs["negative_prompt"].append(this_fallback_negative_prompt)
for key in multi_modal_data_fields:
input_kwargs[key].append(None)
else:
prompts.append(prompt.get("prompt", ""))
negative_prompts.append(prompt.get("negative_prompt", ""))
if all(not np for np in negative_prompts):
negative_prompts = None
return prompts, negative_prompts
obj = cast(OmniTextPrompt, prompt)
input_kwargs["prompt"].append(obj.get("prompt", ""))
@staticmethod
def _extract_negative_prompt(prompt_obj: Any) -> str | None:
"""Extract the negative prompt from a prompt object, if present."""
if isinstance(prompt_obj, dict):
return prompt_obj.get("negative_prompt")
return getattr(prompt_obj, "negative_prompt", None)
if kwargs_should_contain_negative_prompt:
negative_prompt: str = obj.get("negative_prompt", this_fallback_negative_prompt)
input_kwargs["negative_prompt"].append(negative_prompt)
multi_modal_data = obj.get("multi_modal_data") or {}
for key in multi_modal_data_fields:
input_kwargs[key].append(multi_modal_data.get(key))
return input_kwargs
def _wrap_output(self, output: Any) -> DiffusionOutput:
"""Convert diffusers pipeline output to ``DiffusionOutput``.
@@ -356,3 +451,30 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
return DiffusionOutput(output=output.audios)
return DiffusionOutput(output=output)
@staticmethod
def _summarize_call_kwargs_value(value: Any) -> Any:
"""Return a sanitized summary of diffusers call kwargs for logging."""
if value is None or isinstance(value, (bool, int, float)):
return value
if isinstance(value, str):
if len(value) < 20:
return value
return f"{value[:10]}...{value[-10:]}"
if isinstance(value, torch.Tensor):
return f"Tensor with shape {tuple(value.shape)}, dtype {value.dtype}, device {value.device}"
if isinstance(value, torch.Generator):
return f"Generator with seed {value.initial_seed()} on device {value.device}"
if isinstance(value, dict):
return {k: DiffusersAdapterPipeline._summarize_call_kwargs_value(v) for k, v in value.items()}
if isinstance(value, (list, tuple, set)):
if len(value) > 10:
return f"{value.__class__.__name__} of length {len(value)}"
return value.__class__([DiffusersAdapterPipeline._summarize_call_kwargs_value(v) for v in value])
shape = getattr(value, "shape", None)
size = getattr(value, "size", None)
if shape is not None:
return {"type": type(value).__name__, "shape": tuple(shape)}
if size is not None and not callable(size):
return {"type": type(value).__name__, "size": size}
return f"<{type(value).__name__}>"