Files
vllm-omni/vllm_omni/diffusion/models/anima/pipeline_anima.py
T
2026-09-19 16:16:38 +00:00

796 lines
34 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import inspect
import json
import logging
import os
from collections.abc import Callable, Iterable
from pathlib import Path
from typing import Any, cast
import numpy as np
import torch
from diffusers.image_processor import VaeImageProcessor
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
from diffusers.utils.torch_utils import randn_tensor
from safetensors.torch import load_file
from torch import nn
from transformers import AutoTokenizer, Qwen3Model, T5TokenizerFast
from vllm_omni.diffusion.config import set_current_diffusion_config
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_qwenimage import DistributedAutoencoderKLQwenImage
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.models.anima.anima_text_conditioner import (
ANIMA_TEXT_CONDITIONER_CONFIG,
AnimaTextConditioner,
)
from vllm_omni.diffusion.models.anima.anima_transformer import ANIMA_TRANSFORMER_CONFIG, AnimaTransformer3DModel
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
from vllm_omni.diffusion.offloader.config import OffloadStrategy, resolve_offload_strategy
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
from vllm_omni.diffusion.utils.size_utils import normalize_min_aligned_size
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
logger = logging.getLogger(__name__)
ANIMA_VAE_SCALE_FACTOR = 8
def retrieve_timesteps(
scheduler: Any,
num_inference_steps: int | None = None,
device: str | torch.device | None = None,
timesteps: list[int] | None = None,
sigmas: list[float] | None = None,
**kwargs: Any,
) -> tuple[torch.Tensor, int]:
if timesteps is not None and sigmas is not None:
raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
if timesteps is not None:
if "timesteps" not in inspect.signature(scheduler.set_timesteps).parameters:
raise ValueError(
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
f" timestep schedules. Please check whether you are using the correct scheduler."
)
scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
scheduler_timesteps = scheduler.timesteps
num_inference_steps = len(scheduler_timesteps)
elif sigmas is not None:
if "sigmas" not in inspect.signature(scheduler.set_timesteps).parameters:
raise ValueError(
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
f" sigmas schedules. Please check whether you are using the correct scheduler."
)
scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
scheduler_timesteps = scheduler.timesteps
num_inference_steps = len(scheduler_timesteps)
else:
scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
scheduler_timesteps = scheduler.timesteps
if num_inference_steps is None:
num_inference_steps = len(scheduler_timesteps)
return cast(torch.Tensor, scheduler_timesteps), num_inference_steps
_ANIMA_ORIGINAL_TRANSFORMER_RENAMES = {
"t_embedder.1": "time_embed.t_embedder",
"t_embedding_norm": "time_embed.norm",
"blocks": "transformer_blocks",
"adaln_modulation_self_attn.1": "norm1.linear_1",
"adaln_modulation_self_attn.2": "norm1.linear_2",
"adaln_modulation_cross_attn.1": "norm2.linear_1",
"adaln_modulation_cross_attn.2": "norm2.linear_2",
"adaln_modulation_mlp.1": "norm3.linear_1",
"adaln_modulation_mlp.2": "norm3.linear_2",
"self_attn": "attn1",
"cross_attn": "attn2",
"q_proj": "to_q",
"k_proj": "to_k",
"v_proj": "to_v",
"output_proj": "to_out.0",
"q_norm": "norm_q",
"k_norm": "norm_k",
"mlp.layer1": "ff.net.0.proj",
"mlp.layer2": "ff.net.2",
"x_embedder.proj.1": "patch_embed.proj",
"final_layer.adaln_modulation.1": "norm_out.linear_1",
"final_layer.adaln_modulation.2": "norm_out.linear_2",
"final_layer.linear": "proj_out",
}
_ANIMA_ORIGINAL_TRANSFORMER_DROP_KEYS = (
"accum_video_sample_counter",
"accum_image_sample_counter",
"accum_iteration",
"accum_train_in_hours",
"pos_embedder.seq",
"pos_embedder.dim_spatial_range",
"pos_embedder.dim_temporal_range",
"_extra_state",
)
def _anima_vae_scale_factor_from_vae(vae: Any) -> int:
vae_config = getattr(vae, "config", None)
spatial_compression_ratio = getattr(vae_config, "spatial_compression_ratio", None)
if spatial_compression_ratio is not None:
return int(spatial_compression_ratio)
temporal_downsample = getattr(vae, "temperal_downsample", None)
if temporal_downsample is not None:
return int(2 ** len(temporal_downsample))
return ANIMA_VAE_SCALE_FACTOR
def get_anima_post_process_func(od_config: OmniDiffusionConfig) -> Callable[[torch.Tensor], Any]:
image_processor = VaeImageProcessor(vae_scale_factor=ANIMA_VAE_SCALE_FACTOR)
def post_process_func(images: torch.Tensor) -> Any:
return image_processor.postprocess(images)
return post_process_func
class AnimaPipeline(nn.Module, DiffusionPipelineProfilerMixin, ProgressBarMixin):
"""Native Anima model path.
The Anima transformer checkpoint is distributed as a single safetensors
file. This loader builds the native pipeline components and runs the denoise
transformer/text-conditioner directly.
"""
supports_step_execution = False
def __init__(self, *, od_config: OmniDiffusionConfig, device: torch.device | str | None = None) -> None:
super().__init__()
self.od_config = od_config
self.parallel_config = od_config.parallel_config
self.device = device or get_local_device()
self.weights_sources: list[str] = []
self._raise_unsupported_features()
self._interrupt = False
self._guidance_scale = 1.0
self._num_timesteps = 0
self._current_timestep = None
self._profiler_initialized = False
def _raise_unsupported_features(self) -> None:
pc = self.od_config.parallel_config
if pc.cfg_parallel_size > 1:
raise NotImplementedError("AnimaPipeline does not yet support CFG parallel. Validate native CFG first.")
if pc.sequence_parallel_size is not None and pc.sequence_parallel_size > 1:
raise NotImplementedError("AnimaPipeline does not yet support sequence parallelism. Add an _sp_plan first.")
if pc.tensor_parallel_size > 1:
raise NotImplementedError("AnimaPipeline does not yet support tensor parallelism. Port TP linears first.")
if pc.use_hsdp:
raise NotImplementedError("AnimaPipeline does not yet support HSDP. Validate sharding after native load.")
if self.od_config.cache_backend not in ("none", None):
raise NotImplementedError("AnimaPipeline does not yet support TeaCache or Cache-DiT.")
if self.od_config.quantization_config is not None:
raise NotImplementedError("AnimaPipeline does not yet support quantized checkpoints.")
if resolve_offload_strategy(self.od_config) is not OffloadStrategy.NONE:
raise NotImplementedError("AnimaPipeline does not yet support CPU offload.")
@staticmethod
def _sub_state_dict(state_dict: dict[str, torch.Tensor], prefix: str) -> dict[str, torch.Tensor]:
return {key.removeprefix(prefix): value for key, value in state_dict.items() if key.startswith(prefix)}
@staticmethod
def _convert_original_transformer_state_dict(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
if "patch_embed.proj.weight" in state_dict:
return state_dict
# The raw Anima checkpoint uses the original training names for a
# Cosmos-style denoiser. Convert those keys to the local
# AnimaTransformer3DModel module names before strict loading.
converted_state_dict = {}
for key, value in state_dict.items():
new_key = key.removeprefix("net.")
if any(drop_key in new_key for drop_key in _ANIMA_ORIGINAL_TRANSFORMER_DROP_KEYS):
continue
for old_key, new_name in _ANIMA_ORIGINAL_TRANSFORMER_RENAMES.items():
new_key = new_key.replace(old_key, new_name)
if new_key in converted_state_dict:
raise ValueError(f"Duplicate converted Anima transformer key: {new_key}")
converted_state_dict[new_key] = value
return converted_state_dict
@staticmethod
def _split_single_file_state_dict(
state_dict: dict[str, torch.Tensor],
) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]:
transformer_state_dict = AnimaPipeline._sub_state_dict(state_dict, "transformer.")
text_conditioner_state_dict = AnimaPipeline._sub_state_dict(state_dict, "text_conditioner.")
if transformer_state_dict and text_conditioner_state_dict:
return transformer_state_dict, text_conditioner_state_dict
text_conditioner_state_dict = AnimaPipeline._sub_state_dict(state_dict, "net.llm_adapter.")
if text_conditioner_state_dict:
transformer_state_dict = {
key: value for key, value in state_dict.items() if not key.startswith("net.llm_adapter.")
}
return transformer_state_dict, text_conditioner_state_dict
raise ValueError(
"Anima native loader could not find text-conditioner weights. Expected either "
"`text_conditioner.*` Diffusers keys or `net.llm_adapter.*` original Anima keys."
)
def _load_native_denoiser_components(
self,
state_dict: dict[str, torch.Tensor] | None = None,
) -> tuple[AnimaTransformer3DModel, AnimaTextConditioner]:
if state_dict is None:
model_path = self.od_config.model
if not isinstance(model_path, str) or not os.path.isfile(model_path):
raise ValueError(f"AnimaPipeline currently requires a local file path, got: {model_path}")
state_dict = load_file(model_path, device="cpu")
transformer_state_dict, text_conditioner_state_dict = self._split_single_file_state_dict(state_dict)
del state_dict
transformer_state_dict = self._convert_original_transformer_state_dict(transformer_state_dict)
config_path = Path(self.od_config.model).with_suffix(".json")
config = json.loads(config_path.read_text()) if config_path.is_file() else {}
text_conditioner_config = {**ANIMA_TEXT_CONDITIONER_CONFIG, **config.get("text_conditioner", {})}
transformer_config = {**ANIMA_TRANSFORMER_CONFIG, **config.get("transformer", {})}
native_text_conditioner = AnimaTextConditioner(**cast(dict[str, Any], text_conditioner_config))
native_text_conditioner.load_state_dict(text_conditioner_state_dict, strict=True)
native_text_conditioner.to(device=self.device, dtype=self.od_config.dtype)
del text_conditioner_state_dict
native_transformer = AnimaTransformer3DModel(**cast(dict[str, Any], transformer_config))
native_transformer.load_state_dict(transformer_state_dict, strict=True)
native_transformer.to(device=self.device, dtype=self.od_config.dtype)
del transformer_state_dict
return native_transformer, native_text_conditioner
def _component_path(self, name: str, default_model: str) -> str:
custom_args = self.od_config.custom_pipeline_args or {}
return str(custom_args.get(f"{name}_model", custom_args.get(f"{name}_path", default_model)))
@staticmethod
def _from_pretrained_kwargs(model: str, subfolder: str | None = None) -> dict[str, Any]:
kwargs: dict[str, Any] = {"local_files_only": os.path.exists(model)}
if subfolder is not None and os.path.isdir(os.path.join(model, subfolder)):
kwargs["subfolder"] = subfolder
return kwargs
def _load_outer_components(self) -> tuple[Any, Any, Any, Any, Any]:
custom_args = self.od_config.custom_pipeline_args or {}
components_model_value = custom_args.get("components_model", custom_args.get("components_path"))
if components_model_value is None:
if not isinstance(self.od_config.model, str):
raise ValueError("AnimaPipeline requires `model` or `components_model` to load outer components.")
checkpoint_dir = os.path.dirname(os.path.abspath(self.od_config.model))
components_model = checkpoint_dir if os.path.isdir(checkpoint_dir) else self.od_config.model
else:
components_model = str(components_model_value)
text_encoder_model = self._component_path("text_encoder", components_model)
vae_model = self._component_path("vae", components_model)
tokenizer_model = self._component_path("tokenizer", components_model)
t5_tokenizer_model = self._component_path("t5_tokenizer", components_model)
text_encoder = Qwen3Model.from_pretrained(
text_encoder_model,
torch_dtype=self.od_config.dtype,
**self._from_pretrained_kwargs(text_encoder_model, "text_encoder"),
).to(self.device)
vae = DistributedAutoencoderKLQwenImage.from_pretrained(
vae_model,
torch_dtype=self.od_config.dtype,
**self._from_pretrained_kwargs(vae_model, "vae"),
).to(self.device)
tokenizer = AutoTokenizer.from_pretrained(
tokenizer_model,
**self._from_pretrained_kwargs(tokenizer_model, "tokenizer"),
)
t5_tokenizer = T5TokenizerFast.from_pretrained(
t5_tokenizer_model,
**self._from_pretrained_kwargs(t5_tokenizer_model, "t5_tokenizer"),
)
scheduler_model = self._component_path("scheduler", components_model)
has_scheduler_config = os.path.isfile(os.path.join(scheduler_model, "scheduler_config.json")) or os.path.isdir(
os.path.join(scheduler_model, "scheduler")
)
if has_scheduler_config:
scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
scheduler_model,
**self._from_pretrained_kwargs(scheduler_model, "scheduler"),
)
else:
scheduler = FlowMatchEulerDiscreteScheduler(shift=3.0)
return text_encoder, tokenizer, t5_tokenizer, vae, scheduler
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]] | None = None) -> set[str]:
logger.info("Loading Anima transformer/text-conditioner directly from single-file checkpoint.")
state_dict = dict(weights) if weights is not None else None
loaded = set(state_dict.keys()) if state_dict is not None else set()
if not state_dict:
state_dict = None
with set_current_diffusion_config(self.od_config):
native_transformer, native_text_conditioner = self._load_native_denoiser_components(state_dict)
text_encoder, tokenizer, t5_tokenizer, vae, scheduler = self._load_outer_components()
self.transformer = native_transformer
self.text_conditioner = native_text_conditioner
self.text_encoder = text_encoder
self.tokenizer = tokenizer
self.t5_tokenizer = t5_tokenizer
self.vae = vae
self.scheduler = scheduler
self.vae_scale_factor = _anima_vae_scale_factor_from_vae(self.vae)
self._setup_profiler()
return loaded
def _setup_profiler(self) -> None:
if self._profiler_initialized:
return
self.setup_diffusion_pipeline_profiler(
profiler_targets=["vae.decode", "diffuse", "text_encoder.forward"],
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler,
)
self._profiler_initialized = True
def _extract_prompts(self, prompts: list[Any]) -> tuple[str | list[str] | None, str | list[str] | None]:
"""Extract prompt and negative_prompt from OmniPromptType list."""
prompt = [p if isinstance(p, str) else (p.get("prompt") or "") for p in prompts] or None
if all(isinstance(p, str) or p.get("negative_prompt") is None for p in prompts):
negative_prompt = None
elif prompts:
negative_prompt = ["" if isinstance(p, str) else (p.get("negative_prompt") or "") for p in prompts]
else:
negative_prompt = None
return prompt, negative_prompt
def _get_qwen_prompt_embeds(
self,
prompt: str | list[str],
max_sequence_length: int,
device: torch.device,
dtype: torch.dtype,
) -> tuple[torch.Tensor, torch.Tensor]:
prompt = [prompt] if isinstance(prompt, str) else prompt
text_inputs = self.tokenizer(
prompt,
padding="longest",
max_length=max_sequence_length,
truncation=True,
return_tensors="pt",
)
text_input_ids = text_inputs.input_ids.to(device)
prompt_attention_mask = text_inputs.attention_mask.to(device)
if text_input_ids.shape[-1] == 0:
text_input_ids = text_input_ids.new_zeros((text_input_ids.shape[0], 1))
prompt_attention_mask = prompt_attention_mask.new_zeros((prompt_attention_mask.shape[0], 1))
prompt_embeds = self.text_encoder(
input_ids=text_input_ids,
attention_mask=prompt_attention_mask,
output_hidden_states=False,
).last_hidden_state
prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
prompt_embeds = prompt_embeds * prompt_attention_mask.to(prompt_embeds).unsqueeze(-1)
return prompt_embeds, prompt_attention_mask
def _get_t5_prompt_ids(
self,
prompt: str | list[str],
max_sequence_length: int,
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor]:
prompt = [prompt] if isinstance(prompt, str) else prompt
text_inputs = self.t5_tokenizer(
prompt,
padding="longest",
max_length=max_sequence_length,
truncation=True,
return_tensors="pt",
)
return text_inputs.input_ids.to(device), text_inputs.attention_mask.to(device)
def encode_prompt(
self,
prompt: str | list[str],
negative_prompt: str | list[str] | None = None,
prepare_unconditional_embeds: bool = True,
max_sequence_length: int = 512,
device: torch.device | None = None,
dtype: torch.dtype | None = None,
) -> dict[str, torch.Tensor | None]:
device = device or self.device
dtype = dtype or self.text_encoder.dtype
prompt = [prompt] if isinstance(prompt, str) else prompt
batch_size = len(prompt)
prompt_embeds, prompt_attention_mask = self._get_qwen_prompt_embeds(
prompt=prompt,
max_sequence_length=max_sequence_length,
device=device,
dtype=dtype,
)
t5_input_ids, t5_attention_mask = self._get_t5_prompt_ids(
prompt=prompt,
max_sequence_length=max_sequence_length,
device=device,
)
negative_prompt_embeds = None
negative_prompt_attention_mask = None
negative_t5_input_ids = None
negative_t5_attention_mask = None
if prepare_unconditional_embeds:
if negative_prompt is None:
negative_prompt = ""
if isinstance(negative_prompt, str):
negative_prompt = [negative_prompt] * batch_size
elif len(negative_prompt) != batch_size:
raise ValueError(
f"`negative_prompt` length ({len(negative_prompt)}) must match prompt batch size ({batch_size})."
)
negative_prompt_embeds, negative_prompt_attention_mask = self._get_qwen_prompt_embeds(
prompt=negative_prompt,
max_sequence_length=max_sequence_length,
device=device,
dtype=dtype,
)
negative_t5_input_ids, negative_t5_attention_mask = self._get_t5_prompt_ids(
prompt=negative_prompt,
max_sequence_length=max_sequence_length,
device=device,
)
return {
"qwen_prompt_embeds": prompt_embeds,
"qwen_attention_mask": prompt_attention_mask,
"t5_input_ids": t5_input_ids,
"t5_attention_mask": t5_attention_mask,
"negative_qwen_prompt_embeds": negative_prompt_embeds,
"negative_qwen_attention_mask": negative_prompt_attention_mask,
"negative_t5_input_ids": negative_t5_input_ids,
"negative_t5_attention_mask": negative_t5_attention_mask,
}
def condition_prompt_embeds(
self,
qwen_prompt_embeds: torch.Tensor,
qwen_attention_mask: torch.Tensor,
t5_input_ids: torch.Tensor,
t5_attention_mask: torch.Tensor,
device: torch.device | None = None,
conditioning_dtype: torch.dtype | None = None,
output_dtype: torch.dtype | None = None,
) -> torch.Tensor:
device = device or self.device
conditioning_dtype = conditioning_dtype or self.text_conditioner.dtype
output_dtype = output_dtype or self.transformer.dtype
prompt_embeds = self.text_conditioner(
source_hidden_states=qwen_prompt_embeds.to(device=device, dtype=conditioning_dtype),
target_input_ids=t5_input_ids.to(device),
target_attention_mask=t5_attention_mask.to(device),
source_attention_mask=qwen_attention_mask.to(device),
)
return prompt_embeds.to(dtype=output_dtype, device=device)
def prepare_latents(
self,
batch_size: int,
num_channels_latents: int,
height: int,
width: int,
dtype: torch.dtype | None = None,
device: torch.device | None = None,
generator: torch.Generator | list[torch.Generator] | None = None,
latents: torch.Tensor | None = None,
) -> torch.Tensor:
device = device or self.device
dtype = dtype or self.transformer.dtype
if latents is not None:
return latents.to(device=device, dtype=dtype)
latent_height = height // self.vae_scale_factor
latent_width = width // self.vae_scale_factor
shape = (batch_size, num_channels_latents, 1, latent_height, latent_width)
if isinstance(generator, list) and len(generator) != batch_size:
raise ValueError(
f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
f" size of {batch_size}. Make sure the batch size matches the length of the generators."
)
# Draw latents in float32 then cast
latents = randn_tensor(shape, generator=generator, device=device, dtype=torch.float32)
sigma_max = (
getattr(self.scheduler.config, "sigma_max", 1.0)
if hasattr(self, "scheduler") and hasattr(self.scheduler, "config")
else 1.0
)
return latents.to(dtype=dtype) * sigma_max
def prepare_timesteps(
self,
num_inference_steps: int,
sigmas: list[float] | None = None,
device: torch.device | None = None,
) -> tuple[torch.Tensor, int]:
device = device or self.device
sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
return retrieve_timesteps(
self.scheduler,
device=device,
sigmas=sigmas,
)
def diffuse(
self,
prompt_embeds: torch.Tensor,
negative_prompt_embeds: torch.Tensor | None,
latents: torch.Tensor,
padding_mask: torch.Tensor,
timesteps: torch.Tensor,
do_true_cfg: bool,
true_cfg_scale: float,
) -> torch.Tensor:
self.scheduler.set_begin_index(0)
with self.progress_bar(total=len(timesteps)) as pbar:
for i, t in enumerate(timesteps):
if self.interrupt:
continue
self._current_timestep = t
# Broadcast timestep to match batch size
timestep = t.expand(latents.shape[0]).to(dtype=self.transformer.dtype, device=latents.device)
timestep = timestep / 1000.0 # normalize to 0..1 range
# Predict noise
latent_model_input = latents.to(self.transformer.dtype)
# Positive pass
noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep,
encoder_hidden_states=prompt_embeds,
padding_mask=padding_mask,
return_dict=False,
)[0]
# Negative / uncond pass
if do_true_cfg and negative_prompt_embeds is not None:
noise_pred_uncond = self.transformer(
hidden_states=latent_model_input,
timestep=timestep,
encoder_hidden_states=negative_prompt_embeds,
padding_mask=padding_mask,
return_dict=False,
)[0]
noise_pred = noise_pred_uncond + true_cfg_scale * (noise_pred - noise_pred_uncond)
# Scheduler step
latents_dtype = latents.dtype
latents = self.scheduler.step(noise_pred.to(latents_dtype), t, latents, return_dict=False)[0]
if latents.dtype != latents_dtype:
latents = latents.to(latents_dtype)
pbar.update()
return latents
def decode_latents(
self,
latents: torch.Tensor,
output_type: str = "pil",
) -> DiffusionOutput:
"""Decode final latents."""
if output_type == "latent":
return DiffusionOutput(
output=latents,
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
latents = latents.to(self.vae.dtype)
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents_std = (
torch.tensor(self.vae.config.latents_std)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents = latents * latents_std + latents_mean
image = self.vae.decode(latents, return_dict=False)[0][:, :, 0]
return DiffusionOutput(
output=image,
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
def forward(
self,
req: DiffusionRequestBatch,
prompt: str | list[str] | None = None,
negative_prompt: str | list[str] | None = None,
true_cfg_scale: float = 4.0,
height: int | None = None,
width: int | None = None,
num_inference_steps: int = 50,
sigmas: list[float] | None = None,
guidance_scale: float = 4.0,
num_images_per_prompt: int = 1,
generator: torch.Generator | list[torch.Generator] | None = None,
latents: torch.Tensor | None = None,
output_type: str | None = "pil",
max_sequence_length: int = 512,
) -> list[DiffusionOutput]:
if req.num_reqs != 1:
raise ValueError("AnimaPipeline supports one request per forward.")
extracted_prompt, negative_prompt = self._extract_prompts(req.prompts)
prompt = extracted_prompt or prompt
if prompt is None:
prompt = ""
height = req.sampling_params.height or height or 1024
width = req.sampling_params.width or width or 1024
height, width = normalize_min_aligned_size(height, width, self.vae_scale_factor * 2)
if height is None or width is None:
raise ValueError("AnimaPipeline requires resolved height and width.")
height = int(height)
width = int(width)
if req.sampling_params.output_type is not None:
output_type = req.sampling_params.output_type
output_type = output_type or "pil"
num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps
sigmas = req.sampling_params.sigmas or sigmas
max_sequence_length = req.sampling_params.max_sequence_length or max_sequence_length
generator = req.sampling_params.generator or generator
if req.sampling_params.guidance_scale_provided:
guidance_scale = req.sampling_params.guidance_scale
if req.sampling_params.true_cfg_scale is None:
true_cfg_scale = guidance_scale
if req.sampling_params.true_cfg_scale is not None:
true_cfg_scale = req.sampling_params.true_cfg_scale
num_images_per_prompt = (
req.sampling_params.num_outputs_per_prompt
if req.sampling_params.num_outputs_per_prompt > 0
else num_images_per_prompt
)
self._guidance_scale = guidance_scale
self._interrupt = False
if isinstance(prompt, str):
batch_size = 1
else:
batch_size = len(prompt)
do_true_cfg = guidance_scale > 1.0
# Encode prompts
enc_out = self.encode_prompt(
prompt=prompt,
negative_prompt=negative_prompt,
prepare_unconditional_embeds=do_true_cfg,
max_sequence_length=max_sequence_length,
device=self.device,
dtype=self.text_encoder.dtype,
)
qwen_prompt_embeds = enc_out["qwen_prompt_embeds"]
qwen_attention_mask = enc_out["qwen_attention_mask"]
t5_input_ids = enc_out["t5_input_ids"]
t5_attention_mask = enc_out["t5_attention_mask"]
if (
qwen_prompt_embeds is None
or qwen_attention_mask is None
or t5_input_ids is None
or t5_attention_mask is None
):
raise ValueError("Anima prompt encoding did not produce all required positive embeddings.")
# Condition prompt embeds
prompt_embeds = self.condition_prompt_embeds(
qwen_prompt_embeds=qwen_prompt_embeds,
qwen_attention_mask=qwen_attention_mask,
t5_input_ids=t5_input_ids,
t5_attention_mask=t5_attention_mask,
device=self.device,
)
negative_prompt_embeds = None
negative_qwen_prompt_embeds = enc_out["negative_qwen_prompt_embeds"]
if do_true_cfg and negative_qwen_prompt_embeds is not None:
negative_qwen_attention_mask = enc_out["negative_qwen_attention_mask"]
negative_t5_input_ids = enc_out["negative_t5_input_ids"]
negative_t5_attention_mask = enc_out["negative_t5_attention_mask"]
if (
negative_qwen_attention_mask is None
or negative_t5_input_ids is None
or negative_t5_attention_mask is None
):
raise ValueError("Anima prompt encoding did not produce all required negative embeddings.")
negative_prompt_embeds = self.condition_prompt_embeds(
qwen_prompt_embeds=negative_qwen_prompt_embeds,
qwen_attention_mask=negative_qwen_attention_mask,
t5_input_ids=negative_t5_input_ids,
t5_attention_mask=negative_t5_attention_mask,
device=self.device,
)
# Repeat for num_images_per_prompt
_, seq_len, _ = prompt_embeds.shape
prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
if negative_prompt_embeds is not None:
_, seq_len, _ = negative_prompt_embeds.shape
negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)
negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
# Prepare latents
latents = self.prepare_latents(
batch_size=batch_size * num_images_per_prompt,
num_channels_latents=16, # Cosmos/Anima has 16 channels
height=height,
width=width,
dtype=torch.float32,
device=self.device,
generator=generator,
latents=latents,
)
# Prepare padding mask
padding_mask = latents.new_zeros(1, 1, height, width, dtype=self.transformer.dtype)
# Prepare timesteps
timesteps, num_inference_steps = self.prepare_timesteps(
num_inference_steps=num_inference_steps,
sigmas=sigmas,
device=self.device,
)
self._num_timesteps = len(timesteps)
# Denoise loop
latents = self.diffuse(
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
latents=latents,
padding_mask=padding_mask,
timesteps=timesteps,
do_true_cfg=do_true_cfg,
true_cfg_scale=true_cfg_scale,
)
self._current_timestep = None
return [self.decode_latents(latents, output_type=output_type)]
@property
def guidance_scale(self) -> float:
return self._guidance_scale
@property
def num_timesteps(self) -> int:
return self._num_timesteps
@property
def current_timestep(self) -> torch.Tensor | None:
return self._current_timestep
@property
def interrupt(self) -> bool:
return self._interrupt