# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from __future__ import annotations import json import math import os from collections.abc import Iterable from typing import Any, ClassVar import torch import torch.nn.functional as F import torchsde from diffusers import AutoencoderOobleck from einops import rearrange from torch import einsum, nn from torchvision import transforms from transformers import AutoConfig, CLIPVisionModelWithProjection, T5EncoderModel, T5TokenizerFast from vllm.logger import init_logger from vllm.model_executor.models.utils import AutoWeightsLoader from vllm_omni.diffusion.attention.layer import Attention from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig from vllm_omni.diffusion.distributed.utils import get_local_device from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader from vllm_omni.diffusion.models.audiox.audiox_transformer import MMDiffusionTransformer from vllm_omni.diffusion.models.interface import SupportAudioOutput from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch from vllm_omni.transformers_utils.processors import audiox as _audiox_transforms _VIDEO_ONLY_TASKS = _audiox_transforms.VIDEO_ONLY_TASKS _TEXT_VIDEO_TASKS = _audiox_transforms.TEXT_VIDEO_TASKS _VIDEO_CONDITIONED_TASKS = _audiox_transforms.VIDEO_CONDITIONED_TASKS _normalize_prompts = _audiox_transforms.normalize_prompts prepare_audio_reference = _audiox_transforms.prepare_audio_reference prepare_video_reference = _audiox_transforms.prepare_video_reference # Polyexponential sigma schedule defaults; mirror upstream AudioX gradio interface # (``audiox/interface/gradio.py`` ``generate_cond`` defaults: ``sigma_min=0.03``, ``sigma_max=1000``). _DEFAULT_UPSTREAM_SIGMA_MIN = 0.03 _DEFAULT_UPSTREAM_SIGMA_MAX = 1000.0 logger = init_logger(__name__) def _load_audiox_bundle_config(model_root: str) -> dict[str, Any]: with open(os.path.join(os.path.abspath(model_root), "config.json"), encoding="utf-8") as f: return json.load(f) def _audio_conditioning_input_samples(model_config: dict[str, Any]) -> int: configs = model_config["model"]["conditioning"]["configs"] cfg = next(c["config"] for c in configs if c["id"] == "audio_prompt") return int(cfg["latent_seq_len"]) * int(cfg["pretransform_config"]["config"]["downsampling_ratio"]) def get_audiox_post_process_func(od_config: OmniDiffusionConfig): """Convert the pipeline's float audio tensor to a CPU numpy array for serving.""" def post_process_func(audio: torch.Tensor) -> Any: if isinstance(audio, dict) and isinstance(audio.get("payload"), dict): payload = dict(audio["payload"]) audio_payload = payload.get("audio") if isinstance(audio_payload, torch.Tensor): payload["audio"] = audio_payload.detach().cpu().float().numpy() return { "payload": payload, "metadata": audio.get("metadata") or {}, } if isinstance(audio, torch.Tensor): return audio.detach().cpu().float().numpy() return audio return post_process_func class SA_PreNorm(nn.Module): def __init__(self, dim, fn): super().__init__() self.norm = nn.LayerNorm(dim) self.fn = fn def forward(self, x, **kwargs): return self.fn(self.norm(x), **kwargs) class SA_FeedForward(nn.Module): def __init__(self, dim, hidden_dim): super().__init__() # Dropout p=0 preserves upstream ``net.{2,4}`` state-dict keys so the upstream weights # load into the right slots; inference is identical to no dropout. self.net = nn.Sequential( nn.Linear(dim, hidden_dim), nn.GELU(), nn.Dropout(0.0), nn.Linear(hidden_dim, dim), nn.Dropout(0.0), ) def forward(self, x): return self.net(x) # Manual einsum+softmax only. SDPA/diffusion Attention here degrades conditioning vs upstream. class SA_Attention(nn.Module): def __init__(self, dim, heads=8, dim_head=64): super().__init__() inner_dim = dim_head * heads project_out = not (heads == 1 and dim_head == dim) self.heads = heads self.scale = dim_head**-0.5 self.to_qkv = nn.Linear(dim, inner_dim * 3, bias=False) self.to_out = ( nn.Sequential( nn.Linear(inner_dim, dim), nn.Dropout(0.0), ) if project_out else nn.Identity() ) def forward(self, x): h = self.heads qkv = self.to_qkv(x).chunk(3, dim=-1) q, k, v = (rearrange(t, "b n (h d) -> b h n d", h=h) for t in qkv) dots = einsum("b h i d, b h j d -> b h i j", q, k) * self.scale attn = dots.softmax(dim=-1) out = einsum("b h i j, b h j d -> b h i d", attn, v) out = rearrange(out, "b h n d -> b n (h d)") out = self.to_out(out) return out class SA_Transformer(nn.Module): def __init__(self, dim, depth, heads, dim_head, mlp_dim): super().__init__() self.layers = nn.ModuleList([]) self.norm = nn.LayerNorm(dim) for _ in range(depth): self.layers.append( nn.ModuleList( [ SA_PreNorm(dim, SA_Attention(dim, heads=heads, dim_head=dim_head)), SA_PreNorm(dim, SA_FeedForward(dim, mlp_dim)), ] ) ) def forward(self, x): for attn, ff in self.layers: x = attn(x) + x x = ff(x) + x return self.norm(x) _AUDIOX_OOBLECK_CONFIG = { "audio_channels": 2, "channel_multiples": [1, 2, 4, 8, 16], "decoder_channels": 128, "decoder_input_channels": 64, "downsampling_ratios": [2, 4, 4, 8, 8], "encoder_hidden_size": 128, "sampling_rate": 44100, } def _build_audiox_oobleck(scaling_factor: float = 1.0) -> AutoencoderOobleck: vae = AutoencoderOobleck(**_AUDIOX_OOBLECK_CONFIG) vae.audiox_scaling_factor = float(scaling_factor) # type: ignore[attr-defined] return vae.eval().requires_grad_(False) class AudioVaePromptAdapter(nn.Module): def __init__(self, *, cond_dim: int, latent_seq_len: int = 215): super().__init__() self.pretransform = _build_audiox_oobleck() in_ch = int(self.pretransform.config.decoder_input_channels) self.proj_features_128 = nn.Linear(latent_seq_len, 128) self.proj_out = nn.Linear(in_ch, cond_dim) if in_ch != cond_dim else nn.Identity() def forward(self, audio: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: z = self.pretransform.encode(audio, return_dict=True).latent_dist.sample() latents = z / float(self.pretransform.audiox_scaling_factor) latents = rearrange(self.proj_features_128(latents), "b c s -> b s c") latents = self.proj_out(latents) ones = torch.ones(latents.shape[0], latents.shape[2], device=latents.device) return latents, ones class _MAFCrossAttentionBlock(nn.Module): def __init__(self, dim: int, num_heads: int): super().__init__() assert dim % num_heads == 0, "dim must be divisible by num_heads" self._num_heads = num_heads self._head_dim = dim // num_heads self._scale = self._head_dim**-0.5 self.to_q = nn.Linear(dim, dim, bias=True) self.to_kv = nn.Linear(dim, dim * 2, bias=True) self.to_out = nn.Linear(dim, dim, bias=True) nn.init.zeros_(self.to_out.weight) if self.to_out.bias is not None: nn.init.zeros_(self.to_out.bias) def forward(self, experts: torch.Tensor, full_context: torch.Tensor) -> torch.Tensor: nh, hd = self._num_heads, self._head_dim q = rearrange(self.to_q(experts), "b n (h d) -> b h n d", h=nh, d=hd) k, v = (rearrange(t, "b n (h d) -> b h n d", h=nh, d=hd) for t in self.to_kv(full_context).chunk(2, dim=-1)) out = F.scaled_dot_product_attention(q, k, v, dropout_p=0.0, is_causal=False, scale=self._scale) return self.to_out(rearrange(out, "b h n d -> b n (h d)")) class _MAFFusionBlock(nn.Module): def __init__(self, dim: int, num_heads: int, mlp_ratio: float): super().__init__() assert dim % num_heads == 0, "dim must be divisible by num_heads" head_dim = dim // num_heads self._num_heads = num_heads hidden = int(dim * mlp_ratio) self.norm1 = nn.LayerNorm(dim) self.to_qkv = nn.Linear(dim, dim * 3, bias=True) self.self_attn = Attention( num_heads=num_heads, head_size=head_dim, softmax_scale=head_dim**-0.5, causal=False, ) self.out_proj = nn.Linear(dim, dim, bias=True) self.norm2 = nn.LayerNorm(dim) self.ff = nn.Sequential( nn.Linear(dim, hidden), nn.GELU(), nn.Linear(hidden, dim), ) nn.init.zeros_(self.out_proj.weight) if self.out_proj.bias is not None: nn.init.zeros_(self.out_proj.bias) def forward(self, x: torch.Tensor) -> torch.Tensor: h = self.norm1(x) q, k, v = self.to_qkv(h).chunk(3, dim=-1) nh = self._num_heads q_bsn = rearrange(q, "b n (h d) -> b n h d", h=nh).contiguous() k_bsn = rearrange(k, "b n (h d) -> b n h d", h=nh).contiguous() v_bsn = rearrange(v, "b n (h d) -> b n h d", h=nh).contiguous() out = self.self_attn(q_bsn, k_bsn, v_bsn, attn_metadata=None) out = rearrange(out, "b n h d -> b n (h d)") out = self.out_proj(out) x = x + out x = x + self.ff(self.norm2(x)) return x class MAF_Block(nn.Module): DIM = 768 MLP_RATIO = 4.0 def __init__( self, *, dim: int = 768, num_experts_per_modality: int = 64, num_heads: int = 24, num_fusion_layers: int = 8, mlp_ratio: float = 4.0, ): super().__init__() total_experts = num_experts_per_modality * 3 self.gating_network = nn.Sequential( nn.Linear(dim * 3, dim), nn.GELU(), nn.Linear(dim, 3), nn.Sigmoid(), ) self.unified_experts = nn.Parameter(torch.randn(total_experts, dim)) self.cross_block = _MAFCrossAttentionBlock(dim, num_heads) self.norm1 = nn.LayerNorm(dim) self.fusion_blocks = nn.ModuleList( [_MAFFusionBlock(dim, num_heads, mlp_ratio) for _ in range(num_fusion_layers)] ) self.norm_v2 = nn.LayerNorm(dim) self.norm_t2 = nn.LayerNorm(dim) self.norm_a2 = nn.LayerNorm(dim) self.bypass_gate_v = nn.Parameter(torch.tensor(-10.0)) self.bypass_gate_t = nn.Parameter(torch.tensor(-10.0)) self.bypass_gate_a = nn.Parameter(torch.tensor(-10.0)) def forward( self, video_tokens: torch.Tensor, text_tokens: torch.Tensor, audio_tokens: torch.Tensor, ) -> dict[str, torch.Tensor]: batch_size = video_tokens.shape[0] v_global = video_tokens.mean(dim=1) t_global = text_tokens.mean(dim=1) a_global = audio_tokens.mean(dim=1) all_global = torch.cat([v_global, t_global, a_global], dim=1) gates = self.gating_network(all_global) w_v, w_t, w_a = gates.chunk(3, dim=-1) gated_v = video_tokens * w_v.unsqueeze(-1) gated_t = text_tokens * w_t.unsqueeze(-1) gated_a = audio_tokens * w_a.unsqueeze(-1) full_context = torch.cat([gated_v, gated_t, gated_a], dim=1) experts = self.unified_experts.unsqueeze(0).expand(batch_size, -1, -1) cross_out = self.cross_block(experts, full_context) updated_experts = self.norm1(experts + cross_out) for blk in self.fusion_blocks: updated_experts = blk(updated_experts) fused_v_experts, fused_t_experts, fused_a_experts = updated_experts.chunk(3, dim=1) refinement_v = fused_v_experts.mean(dim=1) refinement_t = fused_t_experts.mean(dim=1) refinement_a = fused_a_experts.mean(dim=1) alpha_v = torch.sigmoid(self.bypass_gate_v) alpha_t = torch.sigmoid(self.bypass_gate_t) alpha_a = torch.sigmoid(self.bypass_gate_a) final_v = video_tokens + alpha_v * self.norm_v2(refinement_v).unsqueeze(1) final_t = text_tokens + alpha_t * self.norm_t2(refinement_t).unsqueeze(1) final_a = audio_tokens + alpha_a * self.norm_a2(refinement_a).unsqueeze(1) return { "video": final_v, "text": final_t, "audio": final_a, } class _BrownianTreeNoiseSampler: """Brownian-tree noise sampler for DPM-Solver++ SDE, ported from k-diffusion. Returns a scaled Brownian increment between two sigma levels; the tree is indexed by transformed ``sigma`` (here linear, same as k-diffusion's default), so re-querying identical (sigma, sigma_next) pairs returns the same noise. Entropy must come from the user's seed — otherwise the unseeded global RNG would make sampling non-deterministic. """ def __init__(self, x: torch.Tensor, sigma_min: torch.Tensor, sigma_max: torch.Tensor, entropy: int): t0 = torch.as_tensor(sigma_min) t1 = torch.as_tensor(sigma_max) self._tree = torchsde.BrownianTree(t0, torch.zeros_like(x), t1, entropy=entropy) def __call__(self, sigma: torch.Tensor, sigma_next: torch.Tensor) -> torch.Tensor: # AudioX denoises from sigma_max → 0, so sigma > sigma_next throughout the loop. t0 = torch.as_tensor(sigma_next) t1 = torch.as_tensor(sigma) return -self._tree(t0, t1) / (t1 - t0).sqrt() class AudioXPipeline(nn.Module, SupportAudioOutput, DiffusionPipelineProfilerMixin): supports_request_batch = False support_audio_output: ClassVar[bool] = True audio_sample_rate: ClassVar[int] = 44100 audio_channels: ClassVar[int] = 2 _PROFILER_TARGETS: ClassVar[list[str]] = ["diffuse"] _CLIP_SYNC_DURATION_SEC: ClassVar[float] = 10.0 _VIDEO_SYNC_FRAME_COUNT: ClassVar[int] = 240 def __init__( self, *, od_config: OmniDiffusionConfig, prefix: str = "", ): super().__init__() self.od_config = od_config self.device = get_local_device() if od_config.model is None: raise ValueError( "AudioXPipeline requires od_config.model (directory with unified safetensors; " "see https://huggingface.co/zhangj1an/AudioX)." ) if os.path.exists(od_config.model): self._model_root = os.path.abspath(od_config.model) else: from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific self._model_root = download_weights_from_hf_specific(od_config.model, None, ["*"]) self._model_config = _load_audiox_bundle_config(self._model_root) model_config = self._model_config["model"] diffusion_config = model_config["diffusion"] self.model = MMDiffusionTransformer(**dict(diffusion_config["config"])) cond_configs = {c["id"]: c["config"] for c in model_config["conditioning"]["configs"]} self.audio_vae_adapter = AudioVaePromptAdapter( cond_dim=int(model_config["conditioning"]["cond_dim"]), latent_seq_len=int(cond_configs["audio_prompt"]["latent_seq_len"]), ) t5_name = cond_configs["text_prompt"]["t5_model_name"] self._t5_max_length = int(cond_configs["text_prompt"]["max_length"]) self.tokenizer = T5TokenizerFast.from_pretrained(t5_name) t5_config = AutoConfig.from_pretrained(t5_name) self.text_encoder = T5EncoderModel(t5_config).train(False).requires_grad_(False).to(torch.float16) clip_name = cond_configs["video_prompt"]["clip_model_name"] clip_config = AutoConfig.from_pretrained(clip_name) self.clip_encoder = CLIPVisionModelWithProjection(clip_config.vision_config) _CLIP_PATCH_TOKENS, _VIDEO_FPS, _DURATION_SEC, _DIM = 50, 5, 10, 768 _in_features = _CLIP_PATCH_TOKENS * _VIDEO_FPS * _DURATION_SEC self._clip_in_features = _in_features self._clip_out_features = 128 self.clip_proj = nn.Linear(_in_features, self._clip_out_features) self.clip_proj_sync = nn.Linear(240, self._clip_out_features) self.clip_sync_weight = nn.Parameter(torch.tensor(0.0)) self.clip_temp_transformer = SA_Transformer(_DIM, depth=4, heads=16, dim_head=64, mlp_dim=_DIM * 4) self.clip_temp_pos_embedding = nn.Parameter(torch.randn(1, _VIDEO_FPS * _DURATION_SEC, _DIM)) self.clip_empty_visual_feat = nn.Parameter(torch.zeros(1, self._clip_out_features, _DIM), requires_grad=False) _CLIP_MEAN = (0.48145466, 0.4578275, 0.40821073) _CLIP_STD = (0.26862954, 0.26130258, 0.27577711) self._clip_normalize = transforms.Compose([transforms.Normalize(mean=list(_CLIP_MEAN), std=list(_CLIP_STD))]) self.pretransform = _build_audiox_oobleck( scaling_factor=float(model_config["pretransform"].get("scale", 1.0)), ) self.io_channels = model_config["io_channels"] self.diffusion_objective = "v" gate_type_config = diffusion_config["gate_type_config"] self.maf_block = MAF_Block( dim=768, num_experts_per_modality=int(gate_type_config["num_experts_per_modality"]), num_heads=int(gate_type_config["num_heads"]), num_fusion_layers=int(gate_type_config["num_fusion_layers"]), ) logger.debug("AudioX model built from %s", self._model_root) self.weights_sources = [ DiffusersPipelineLoader.ComponentSource( model_or_path=self._model_root, subfolder="transformer", revision=getattr(od_config, "revision", None), prefix="", ), ] sample_rate = int(self._model_config.get("sample_rate", 48000)) self._sample_rate = sample_rate self._sample_size = int(self._model_config.get("sample_size", sample_rate * 10)) self._target_fps = int(self._model_config.get("video_fps", 5)) self._audio_conditioning_samples = _audio_conditioning_input_samples(self._model_config) self.setup_diffusion_pipeline_profiler( profiler_targets=list(self._PROFILER_TARGETS), enable_diffusion_pipeline_profiler=od_config.enable_diffusion_pipeline_profiler, ) def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: _legacy_prefix = "conditioner.conditioners.audio_prompt." # DiT self-attn QKV: bundle stores weights with last dim (h, d, qkv) interleaved # (i.e. Q/K/V rows for the same head sit next to each other), whereas # QKVParallelLinear expects stacked [Q|K|V] blocks along the output axis. # Reshape once at load time so the parallel loader can consume them. nheads = int(self._model_config["model"]["diffusion"]["config"]["num_heads"]) embed_dim = int(self._model_config["model"]["diffusion"]["config"]["embed_dim"]) total_heads = embed_dim // nheads head_dim = embed_dim // total_heads qkv_mid = ".attn.qkv." to_kv_mid = ".cross_attn.to_kv." def _restack_interleaved(tensor: torch.Tensor, n_slots: int) -> torch.Tensor: """Turn bundle's (h, d, slot) interleaved-last-dim layout into stacked [slot|slot|...].""" if tensor.dim() == 2: # weight: (out, in) out, inp = tensor.shape assert out == n_slots * total_heads * head_dim, (out, n_slots, total_heads, head_dim) return ( tensor.view(total_heads, head_dim, n_slots, inp).permute(2, 0, 1, 3).reshape(out, inp).contiguous() ) out = tensor.shape[0] assert out == n_slots * total_heads * head_dim return tensor.view(total_heads, head_dim, n_slots).permute(2, 0, 1).reshape(out).contiguous() def _remap(items): for name, tensor in items: if name.startswith(_legacy_prefix): name = "audio_vae_adapter." + name[len(_legacy_prefix) :] if qkv_mid in name and (name.endswith(".weight") or name.endswith(".bias")): tensor = _restack_interleaved(tensor, 3) elif to_kv_mid in name and (name.endswith(".weight") or name.endswith(".bias")): tensor = _restack_interleaved(tensor, 2) yield name, tensor loaded = AutoWeightsLoader(self).load_weights(_remap(weights)) self.to(torch.float32) self.eval().requires_grad_(False) return loaded def _conditioning_dtype(self) -> torch.dtype: p = next(self.model.parameters()) return p.dtype if p.dtype.is_floating_point else torch.float32 @staticmethod def _normalize_task(task: str | None) -> str | None: if task is None: return None t = str(task).strip().lower() return t or None @staticmethod def _text_for_task(task_norm: str | None, prompt: str) -> str: if task_norm in _VIDEO_ONLY_TASKS: return "" return prompt @staticmethod def _ensure_text_video_prompts(task_norm: str | None, prompts: list[str]) -> None: if task_norm not in _TEXT_VIDEO_TASKS: return for i, p in enumerate(prompts): if not str(p).strip(): raise ValueError( f"audiox_task={task_norm!r} requires a non-empty text prompt for item {i}; " "use v2a/v2m for video-only generation." ) def _audio_prompt_tensors( self, *, raw_prompts: list[Any], extra: dict[str, Any], device: torch.device, cond_dtype: torch.dtype = torch.float32, ) -> list[torch.Tensor]: target_len = self._audio_conditioning_samples sample_rate = self._sample_rate seconds_start = float(extra.get("seconds_start", 0.0)) seconds_total = float(target_len) / float(sample_rate) out: list[torch.Tensor] = [] for raw in raw_prompts: src = extra.get("audio_path") if src is None: out.append(torch.zeros(2, target_len, device=device, dtype=cond_dtype)) continue wav = prepare_audio_reference( src, model_sample_rate=sample_rate, seconds_start=seconds_start, seconds_total=seconds_total, device=device, ) out.append(wav.to(dtype=cond_dtype)) return out def _video_feature_tensors( self, *, task_norm: str | None, raw_prompts: list[Any], extra: dict[str, Any], seconds_start: float, target_fps: int, device: torch.device, cond_dtype: torch.dtype = torch.float32, ) -> list[torch.Tensor]: clip_frames = int(round(self._CLIP_SYNC_DURATION_SEC * target_fps)) if task_norm not in _VIDEO_CONDITIONED_TASKS: empty = torch.zeros(clip_frames, 3, 224, 224, device=device, dtype=cond_dtype) return [empty for _ in raw_prompts] tensors: list[torch.Tensor] = [] for _ in raw_prompts: src = extra.get("video_path") if src is None: raise ValueError(f"audiox_task={task_norm!r} requires video input: set extra_args['video_path'].") vt = prepare_video_reference( src, duration=float(self._CLIP_SYNC_DURATION_SEC), target_fps=target_fps, seek_time=seconds_start, ) tensors.append(vt.to(device=device, dtype=cond_dtype)) return tensors def get_conditioning_inputs(self, conditioning_tensors: dict[str, Any], negative: bool = False) -> dict[str, Any]: video_feature, video_mask = conditioning_tensors["video_prompt"] text_feature, text_mask = conditioning_tensors["text_prompt"] audio_feature, audio_mask = conditioning_tensors["audio_prompt"] refined = self.maf_block(text_feature, video_feature, audio_feature) fused = torch.cat(list(refined.values()), dim=1) masks = torch.cat([video_mask, text_mask, audio_mask], dim=1) if negative: return {"negative_cross_attn_cond": fused, "negative_cross_attn_mask": masks} return {"cross_attn_cond": fused} def diffuse( self, *, steps: int, guidance_scale: float, conditioning_tensors: dict[str, Any], negative_conditioning_tensors: dict[str, Any] | None, batch_size: int, sigma_min: float, sigma_max: float, generator: torch.Generator, cfg_rescale: float, ) -> torch.Tensor: device = self.device model_dtype = next(self.model.parameters()).dtype # Match upstream AudioX: disable TF32 matmul + fp16 reduced precision + cudnn benchmark # for numerical parity with audiox/inference/generation.py:152-156. torch.backends.cuda.matmul.allow_tf32 = False torch.backends.cudnn.allow_tf32 = False torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction = False torch.backends.cudnn.benchmark = False latent_len = self._sample_size // int(self.pretransform.hop_length) noise = torch.randn( [batch_size, self.io_channels, latent_len], device=device, generator=generator, dtype=model_dtype ) def _cast(d: dict[str, Any]) -> dict[str, Any]: return {k: (v.type(model_dtype) if isinstance(v, torch.Tensor) else v) for k, v in d.items()} cond = _cast(self.get_conditioning_inputs(conditioning_tensors)) neg = ( _cast(self.get_conditioning_inputs(negative_conditioning_tensors, negative=True)) if negative_conditioning_tensors is not None else {} ) # Inlined k-diffusion VDenoiser + sample_dpmpp_3m_sde, matching upstream AudioX exactly. # diffusers' EDMDPMSolverMultistepScheduler uses different v-prediction preconditioning # and a different stochastic update rule, which here produces a fixed ~861 Hz resonance # in the decoded audio regardless of conditioning. def denoise(x_in: torch.Tensor, sigma: torch.Tensor) -> torch.Tensor: s2 = sigma * sigma c_skip = 1.0 / (s2 + 1.0) c_out = -sigma / (s2 + 1.0).sqrt() c_in = 1.0 / (s2 + 1.0).sqrt() t_cond = sigma.atan() * (2.0 / math.pi) v = self.model( x_in * c_in, t_cond, cross_attn_cond=cond["cross_attn_cond"], negative_cross_attn_cond=neg.get("negative_cross_attn_cond"), negative_cross_attn_mask=neg.get("negative_cross_attn_mask"), cfg_scale=guidance_scale, scale_phi=cfg_rescale, ) return v * c_out + x_in * c_skip ramp = torch.linspace(1.0, 0.0, steps, device=device) sigmas = torch.cat( [ torch.exp(ramp * (math.log(sigma_max) - math.log(sigma_min)) + math.log(sigma_min)), torch.zeros(1, device=device), ] ) x = noise * sigmas[0] # Match upstream AudioX: sampler runs under fp16 autocast (see audiox/inference/sampling.py:184). with torch.cuda.amp.autocast(): if steps <= 1: s_in = x.new_ones([x.shape[0]]) sampled = denoise(x, sigmas[0] * s_in) else: # DPM-Solver++(3M) SDE loop (k-diffusion sample_dpmpp_3m_sde), eta=1.0, s_noise=1.0. noise_sampler = _BrownianTreeNoiseSampler( x, sigmas[sigmas > 0].min(), sigmas.max(), entropy=generator.initial_seed(), ) s_in = x.new_ones([x.shape[0]]) denoised_1 = denoised_2 = None h_1 = h_2 = None eta = 1.0 for i in range(len(sigmas) - 1): denoised = denoise(x, sigmas[i] * s_in) if sigmas[i + 1] == 0: x = denoised else: t_, s_ = -sigmas[i].log(), -sigmas[i + 1].log() h = s_ - t_ h_eta = h * (eta + 1) x = torch.exp(-h_eta) * x + (-h_eta).expm1().neg() * denoised if h_2 is not None: r0 = h_1 / h r1 = h_2 / h d1_0 = (denoised - denoised_1) / r0 d1_1 = (denoised_1 - denoised_2) / r1 d1 = d1_0 + (d1_0 - d1_1) * r0 / (r0 + r1) d2 = (d1_0 - d1_1) / (r0 + r1) phi_2 = h_eta.neg().expm1() / h_eta + 1 phi_3 = phi_2 / h_eta - 0.5 x = x + phi_2 * d1 - phi_3 * d2 elif h_1 is not None: r = h_1 / h d = (denoised - denoised_1) / r phi_2 = h_eta.neg().expm1() / h_eta + 1 x = x + phi_2 * d x = ( x + noise_sampler(sigmas[i], sigmas[i + 1]) * sigmas[i + 1] * (-2 * h * eta).expm1().neg().sqrt() ) denoised_1, denoised_2 = denoised, denoised_1 h_1, h_2 = h, h_1 sampled = x vae = self.pretransform.to(device=sampled.device, dtype=torch.float32).eval() return vae.decode(sampled.to(torch.float32) * float(vae.audiox_scaling_factor), return_dict=True).sample def _encode_text(self, texts: list[str], device: torch.device) -> list[torch.Tensor]: self.text_encoder.to(device) encoded = self.tokenizer( texts, truncation=True, max_length=self._t5_max_length, padding="max_length", return_tensors="pt", ) input_ids = encoded["input_ids"].to(device) attention_mask = encoded["attention_mask"].to(device).to(torch.bool) self.text_encoder.eval() with torch.no_grad(): embeddings = self.text_encoder(input_ids=input_ids, attention_mask=attention_mask)["last_hidden_state"] embeddings = embeddings.float() * attention_mask.unsqueeze(-1).float() return [embeddings, attention_mask] def _encode_video(self, video_list: list[dict], device: torch.device) -> list[torch.Tensor]: self.clip_encoder.to(device).eval() video_tensors = [item["video_tensors"] for item in video_list] video_sync_frames = torch.cat([item["video_sync_frames"] for item in video_list], dim=0).to(device) original_videos = torch.cat(video_tensors, dim=0).to(device) batch_size, time_length, _, _, _ = original_videos.size() is_zero = torch.all(original_videos == 0, dim=(1, 2, 3, 4)) frames = original_videos.flatten(0, 1) pixel_values = self._clip_normalize(frames).to(device) with torch.no_grad(): outputs = self.clip_encoder(pixel_values=pixel_values) hidden = outputs.last_hidden_state hidden = rearrange(hidden, "(b t) p d -> (b p) t d", b=batch_size, t=time_length) hidden = hidden + self.clip_temp_pos_embedding hidden = self.clip_temp_transformer(hidden) hidden = rearrange(hidden, "(b p) t d -> b (t p) d", b=batch_size) hidden = self.clip_proj(hidden.view(-1, self._clip_in_features)) hidden = hidden.view(batch_size, self._clip_out_features, -1) sync = self.clip_proj_sync(video_sync_frames.view(-1, 240)) sync = sync.view(batch_size, self._clip_out_features, -1) hidden = hidden + self.clip_sync_weight * sync empty = self.clip_empty_visual_feat.expand(batch_size, -1, -1) hidden = torch.where(is_zero.view(batch_size, 1, 1), empty, hidden) return [hidden, torch.ones(batch_size, 1, device=device)] def _encode_conditioning_tensors(self, batch_metadata: list[dict[str, Any]]) -> dict[str, Any]: device = self.device audio = torch.cat([item["audio_prompt"] for item in batch_metadata], dim=0).to(device) return { "audio_prompt": list(self.audio_vae_adapter(audio)), "text_prompt": self._encode_text([item["text_prompt"] for item in batch_metadata], device), "video_prompt": self._encode_video([item["video_prompt"] for item in batch_metadata], device), } def _build_conditioning_batch( self, *, texts: list[str], video_tensors_list: list[torch.Tensor], audio_prompt_list: list[torch.Tensor], sync_features: torch.Tensor, seconds_start: float, seconds_model: float, num_outputs_per_prompt: int, task_norm: str | None, ) -> list[dict[str, Any]]: batch: list[dict[str, Any]] = [] for i, text in enumerate(texts): for _ in range(num_outputs_per_prompt): batch.append( { "video_prompt": { "video_tensors": video_tensors_list[i].unsqueeze(0), "video_sync_frames": sync_features, }, "text_prompt": self._text_for_task(task_norm, text), "audio_prompt": audio_prompt_list[i].unsqueeze(0), "seconds_start": seconds_start, "seconds_total": seconds_model, } ) return batch def forward(self, req: DiffusionRequestBatch) -> DiffusionOutput: if req.prompts is None or len(req.prompts) == 0: raise ValueError("AudioXPipeline requires at least one prompt.") normalized_prompts = _normalize_prompts(list(req.prompts)) prompts = [p["prompt"] for p in normalized_prompts] sampling_params = req.sampling_params if sampling_params.num_inference_steps is None: raise ValueError("AudioXPipeline requires sampling_params.num_inference_steps.") num_inference_steps = int(sampling_params.num_inference_steps) extra_args = sampling_params.extra_args or {} task_norm = self._normalize_task(extra_args.get("audiox_task")) self._ensure_text_video_prompts(task_norm, prompts) neg: list[str] | None = None if not all(p.get("negative_prompt") is None for p in normalized_prompts): neg = [str(p.get("negative_prompt") or "") for p in normalized_prompts] guidance_scale = float(sampling_params.guidance_scale) if sampling_params.num_outputs_per_prompt <= 0: raise ValueError("AudioXPipeline requires sampling_params.num_outputs_per_prompt > 0.") num_outputs_per_prompt = int(sampling_params.num_outputs_per_prompt) batch_size = len(prompts) * num_outputs_per_prompt seconds_start = float(extra_args.get("seconds_start", 0.0)) seconds_model = self._sample_size / self._sample_rate seconds_total = float(extra_args.get("seconds_total", seconds_model)) sigma_min = float(extra_args.get("sigma_min", _DEFAULT_UPSTREAM_SIGMA_MIN)) sigma_max = float(extra_args.get("sigma_max", _DEFAULT_UPSTREAM_SIGMA_MAX)) cfg_rescale = float(extra_args.get("cfg_rescale", 0.0)) device = self.device generator = sampling_params.generator if generator is None: raise ValueError("AudioXPipeline requires sampling_params.generator.") target_fps = self._target_fps cond_dtype = self._conditioning_dtype() sync_features = torch.zeros(1, self._VIDEO_SYNC_FRAME_COUNT, 768, device=device, dtype=cond_dtype) audio_prompt_list = self._audio_prompt_tensors( raw_prompts=normalized_prompts, extra=extra_args, device=device, cond_dtype=cond_dtype, ) video_tensors_list = self._video_feature_tensors( task_norm=task_norm, raw_prompts=normalized_prompts, extra=extra_args, seconds_start=seconds_start, target_fps=target_fps, device=device, cond_dtype=cond_dtype, ) conditioning_batch = self._build_conditioning_batch( texts=prompts, video_tensors_list=video_tensors_list, audio_prompt_list=audio_prompt_list, sync_features=sync_features, seconds_start=seconds_start, seconds_model=seconds_model, num_outputs_per_prompt=num_outputs_per_prompt, task_norm=task_norm, ) negative_conditioning_batch: list[dict[str, Any]] | None = None if neg is not None and guidance_scale > 1.0: negative_conditioning_batch = self._build_conditioning_batch( texts=neg, video_tensors_list=video_tensors_list, audio_prompt_list=audio_prompt_list, sync_features=sync_features, seconds_start=seconds_start, seconds_model=seconds_model, num_outputs_per_prompt=num_outputs_per_prompt, task_norm=task_norm, ) conditioning_tensors = self._encode_conditioning_tensors(conditioning_batch) negative_conditioning_tensors: dict[str, Any] | None = None if negative_conditioning_batch is not None: negative_conditioning_tensors = self._encode_conditioning_tensors(negative_conditioning_batch) audio = self.diffuse( steps=num_inference_steps, guidance_scale=guidance_scale, conditioning_tensors=conditioning_tensors, negative_conditioning_tensors=negative_conditioning_tensors, batch_size=batch_size, sigma_min=sigma_min, sigma_max=sigma_max, generator=generator, cfg_rescale=cfg_rescale, ) # Trim decoded audio to the requested duration (matches upstream AudioX sample script). if 0.0 < seconds_total < seconds_model: target_samples = int(seconds_total * self._sample_rate) audio = audio[..., :target_samples] stage_durations = self.stage_durations if getattr(self, "enable_diffusion_pipeline_profiler", False) else None return DiffusionOutput( output={ "payload": {"audio": audio}, "metadata": {"audio": {"audiox_task": task_norm}}, }, stage_durations=stage_durations, )