# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """AuK audio-editing pipeline for vLLM-Omni. Turns a layer-fused text condition (produced by the encoder stage) plus an optional source clip into a 24 kHz waveform: the source clip is encoded to VAE latents, a rectified-flow DiT integrates the target latents conditioned on both, and the BigVGAN-flow decoder renders the waveform. """ from __future__ import annotations import json import os from collections.abc import Iterable from typing import Any, ClassVar import numpy as np import torch import torchaudio from safetensors import safe_open from torch import nn from vllm.logger import init_logger from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig from vllm_omni.diffusion.distributed.utils import get_local_device from vllm_omni.diffusion.models.auk.auk_transformer import AuKTransformer, dit_state_dict, sample_latents from vllm_omni.diffusion.models.auk.auk_vae import AuKVAE from vllm_omni.diffusion.models.interface import ( SupportAudioInput, SupportAudioOutput, SupportsComponentDiscovery, ) from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch from vllm_omni.model_extras.auk import resolve_gen_frames logger = init_logger(__name__) # Longest ref + target latent sequence the released checkpoints accept. MAX_LATENT_FRAMES = 65536 # Weight-file layout: the DiT lives under this prefix; the layer-fusion # parameters sit next to it but belong to the encoder stage. _DIT_PREFIX = "transformer." _FUSION_KEYS = frozenset({"layer_weights", "layer_scale"}) # The distilled variant only reproduces its training recipe at these settings. _FLASH_NFE = 4 _FLASH_CFG = 0.0 # Decorrelates the VAE posterior draw from the initial latent for the same request seed. _VAE_SEED_OFFSET = 0x5EED_0A0C def get_auk_post_process_func(od_config: OmniDiffusionConfig): """Create the post-processing function for AuK audio output. Tensor output types pass through; anything else becomes a numpy waveform. The sample rate is not attached here: the output formatter reads ``AuKPipeline.audio_sample_rate`` for audio-output pipelines. """ del od_config # The conversion does not depend on the config. def post_process_func( audio: torch.Tensor, output_type: str = "np", ): if output_type in ("pt", "latent"): return audio return audio.cpu().float().numpy() return post_process_func def _prompt_mapping(prompt: Any) -> dict[str, Any]: """Return the prompt dict, or an empty mapping for a bare text prompt.""" if isinstance(prompt, dict): return prompt return {} def _unwrap_single(value: Any) -> Any: """Unwrap the one-element lists the serving layer sometimes produces.""" if isinstance(value, list) and len(value) == 1: return value[0] return value def _split_audio(audio: Any, default_sample_rate: int) -> tuple[Any, int]: """Split a multimodal audio item into raw samples and a sample rate.""" if isinstance(audio, (tuple, list)) and len(audio) == 2 and isinstance(audio[1], (int, float)): return audio[0], int(audio[1]) return audio, default_sample_rate class AuKPipeline(nn.Module, SupportAudioInput, SupportAudioOutput, SupportsComponentDiscovery): """Instruction-driven audio generation and editing with AuK. One request per forward: the rectified-flow ODE runs over the whole target span and the batch dimension carries the CFG branches, so there is nothing to share between requests yet. Args: od_config: OmniDiffusion configuration. ``od_config.model`` must be an assembled AuK directory (``config.json``, ``auk.safetensors``, ``vae.safetensors``), as produced by ``tools/prepare_auk_checkpoint.py``. prefix: Unused; kept for the pipeline construction contract. """ supports_request_batch = False # Picked up by ``supports_audio_output`` in the diffusion engine so the # default stage metadata reports ``final_output_type="audio"`` and the # ``multimodal_output`` payload includes the sample rate. support_audio_output: ClassVar[bool] = True support_audio_input: ClassVar[bool] = True audio_sample_rate: ClassVar[int] = 24000 # The DiT cannot run without an encoder-stage text condition, which the # engine's synthetic warmup request has no way to produce. dummy_run_num_frames: ClassVar[int] = 0 _dit_modules: ClassVar[list[str]] = ["dit"] _encoder_modules: ClassVar[list[str]] = [] _vae_modules: ClassVar[list[str]] = ["vae"] def __init__( self, *, od_config: OmniDiffusionConfig, prefix: str = "", ) -> None: super().__init__() del prefix # Weights are not namespaced: one checkpoint, one pipeline. self.od_config = od_config self.device = get_local_device() self.dtype = getattr(od_config, "dtype", None) or torch.bfloat16 model_dir = od_config.model if not model_dir or not os.path.isdir(model_dir): raise ValueError( f"AuK needs an assembled local checkpoint directory, got {model_dir!r}. " "Build one with tools/prepare_auk_checkpoint.py." ) config = _read_config(model_dir) vae_config = dict(config["vae"]) self.latent_dim = int(vae_config["latent_dim"]) self.hop_size = int(vae_config["downsample_rate"]) self.sample_rate = int(vae_config["target_sample_rate"]) self.variant = str(config.get("variant", "base")) self.is_flash = self.variant == "flash" self.flash_t_grid = [float(t) for t in config.get("flash_t_grid") or ()] if self.is_flash and len(self.flash_t_grid) != _FLASH_NFE + 1: raise ValueError(f"The flash variant needs a {_FLASH_NFE + 1}-point flash_t_grid, got {self.flash_t_grid}.") defaults = dict(config.get("defaults") or {}) self.default_nfe = int(defaults.get("nfe", 32)) self.default_cfg = float(defaults.get("cfg", 2.0)) self.default_sway = defaults.get("sway", -1.0) self._flash_lock_logged = False # The codec is small and numerically sensitive: it stays in fp32. It # must sit on the inference device BEFORE load_weights folds weight # norm: a CPU fold lands one ULP off the accelerator's and the encoder # amplifies that into a 1e-2 latent drift. self.vae = AuKVAE.from_config(vae_config["model_init_kwargs"]).to(device=self.device, dtype=torch.float32) self.vae.load_weights(os.path.join(model_dir, "vae.safetensors")) self.vae = self.vae.eval() self.vae.requires_grad_(False) self._check_vae_geometry() # The transformer owns every key the checkpoint tool writes into `dit`, # `attn_mask_enabled` included, so the section passes straight through. self.dit = AuKTransformer(latent_dim=self.latent_dim, **config["dit"]) self.dit = self.dit.to(dtype=self.dtype) self.dit.load_state_dict(_read_dit_weights(model_dir, self.dtype), strict=True) self.dit = self.dit.to(device=self.device).eval() self.dit.requires_grad_(False) logger.info( "AuK pipeline ready: variant=%s dtype=%s latent_dim=%d hop=%d sample_rate=%d", self.variant, self.dtype, self.latent_dim, self.hop_size, self.sample_rate, ) # The assembled checkpoint is not a diffusers layout: __init__ reads # auk.safetensors and vae.safetensors directly, so the loader has no # component sources to stream and load_weights only reports what is loaded. weights_sources: ClassVar[tuple] = () def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: stray = [name for name, _ in weights] if stray: logger.warning("AuKPipeline ignores %d loader-provided weights (e.g. %s)", len(stray), stray[:3]) return {name for name, _ in self.named_parameters()} def _check_vae_geometry(self) -> None: """Fail fast when config.json and the VAE disagree on latent geometry.""" declared = { "latent_dim": (self.latent_dim, int(self.vae.latent_dim)), "downsample_rate": (self.hop_size, int(self.vae.hop_size)), "target_sample_rate": (self.sample_rate, int(self.vae.sample_rate)), } bad = {name: pair for name, pair in declared.items() if pair[0] != pair[1]} if bad: raise ValueError(f"config.json and the AuK VAE disagree (config, vae): {bad}") if self.sample_rate != self.audio_sample_rate: raise ValueError( f"AuK advertises {self.audio_sample_rate} Hz output but the checkpoint is {self.sample_rate} Hz." ) def _resolve_generator(self, sampling_params: Any) -> torch.Generator | None: """Per-request generator; the process-global RNG is never seeded.""" generator = sampling_params.generator if isinstance(generator, list): if len(generator) > 1: logger.warning( "AuKPipeline runs one request per forward; using the first of %d generators", len(generator) ) generator = generator[0] if generator else None if generator is None and sampling_params.seed is not None: generator = torch.Generator(device=self.device).manual_seed(int(sampling_params.seed)) return generator def _prepare_waveform(self, audio: Any) -> torch.Tensor: """Return mono float32 samples ``[1, T]`` at the VAE sample rate. ``T`` is truncated to a whole number of latent frames. The encoder is causal, so dropping a trailing partial frame leaves every emitted frame unchanged. """ data, sample_rate = _split_audio(audio, self.sample_rate) if isinstance(data, np.ndarray): wav = torch.from_numpy(np.ascontiguousarray(data)) else: wav = torch.as_tensor(data) wav = wav.detach().to(device="cpu", dtype=torch.float32) if wav.ndim == 1: wav = wav.unsqueeze(0) elif wav.ndim == 2: # Accept both [channels, samples] and soundfile's [samples, channels]; # the channel axis is the short one (at most 8 wide). if wav.shape[0] > wav.shape[1] and wav.shape[1] <= 8: wav = wav.transpose(0, 1) if wav.shape[0] > 1: wav = wav.mean(dim=0, keepdim=True) else: raise ValueError(f"AuK audio input must be [samples] or [channels, samples], got {tuple(wav.shape)}.") if wav.shape[-1] == 0: raise ValueError("AuK audio input is empty.") if not torch.isfinite(wav).all(): raise ValueError("AuK audio input contains NaN or Inf.") if sample_rate != self.sample_rate: wav = torchaudio.functional.resample(wav, sample_rate, self.sample_rate) frames = wav.shape[-1] // self.hop_size if frames < 1: raise ValueError( f"AuK audio input is shorter than one latent frame ({self.hop_size} samples at {self.sample_rate} Hz)." ) return wav[..., : frames * self.hop_size] def _encode_source( self, audio: Any, *, sample: bool, generator: torch.Generator | None, ) -> torch.Tensor: """Encode the source clip to normalized latents ``[1, np, latent_dim]``.""" if audio is None: return torch.zeros(1, 0, self.latent_dim, device=self.device, dtype=torch.float32) wav = self._prepare_waveform(_unwrap_single(audio)).to(self.device) return self.vae.encode(wav, sample=sample, generator=generator) def _target_frames(self, gen_seconds: Any, ref_frames: int) -> int: """Target latent length, capped by the remaining context.""" frames = resolve_gen_frames( None if gen_seconds is None else float(gen_seconds), ref_frames, sample_rate=self.sample_rate, hop=self.hop_size, ) budget = MAX_LATENT_FRAMES - ref_frames if budget < 1: raise ValueError( f"The source clip already fills the {MAX_LATENT_FRAMES}-frame context ({ref_frames} frames)." ) if frames > budget: logger.warning("AuK target of %d frames exceeds the remaining context; clamping to %d.", frames, budget) frames = budget return frames def _resolve_schedule( self, sampling_params: Any, knobs: dict[str, Any], ) -> tuple[int, float, float | None, list[float] | None]: """Resolve (nfe, cfg, sway, t_grid), locking the distilled recipe on Flash. A knob present but ``None`` counts as unspecified: the stage input processor fills every key it knows about, whether the caller set it or not. """ steps = sampling_params.num_inference_steps nfe = self.default_nfe if steps is None else int(steps) if nfe < 1: raise ValueError(f"num_inference_steps must be >= 1 for AuK; got {steps}") cfg = float(sampling_params.guidance_scale) if sampling_params.guidance_scale_provided else self.default_cfg sway = knobs.get("sway") sway = self.default_sway if sway is None else float(sway) raw_grid = knobs.get("t_grid") t_grid = [float(t) for t in raw_grid] if raw_grid else None if not self.is_flash: return nfe, cfg, sway, t_grid locked = (_FLASH_NFE, _FLASH_CFG, None, self.flash_t_grid) if (nfe, cfg, sway, t_grid) != locked and not self._flash_lock_logged: self._flash_lock_logged = True logger.warning( "AuK-Flash ignores per-request sampling knobs (asked nfe=%s cfg=%s sway=%s t_grid=%s); " "using the distilled recipe nfe=%d cfg=%s with the checkpoint time grid.", nfe, cfg, sway, t_grid, _FLASH_NFE, _FLASH_CFG, ) return locked def _dit_autocast(self): """Match the reference, which runs the DiT under bf16 autocast.""" return torch.autocast( device_type=self.device.type, dtype=self.dtype, enabled=self.dtype in (torch.bfloat16, torch.float16), ) def forward(self, req: DiffusionRequestBatch) -> list[DiffusionOutput]: """Generate one waveform. Args: req: Request batch holding exactly one request. The prompt carries ``prompt_embeds`` (the fused text condition ``[nt, 2048]``), an optional ``multi_modal_data["audio"]`` source clip, and ``additional_information["auk"]`` with ``gen_seconds``, ``sway``, ``t_grid`` and ``vae_sample``. ``num_inference_steps``, ``guidance_scale`` and ``seed``/``generator`` come from the sampling params. Returns: One ``DiffusionOutput`` whose ``output`` is a float32 mono waveform ``[T]`` at 24 kHz, or the normalized target latents ``[1, gen_frames, latent_dim]`` when ``output_type`` is ``latent``. """ assert req.num_reqs == 1, f"AuKPipeline runs one request per forward, got {req.num_reqs}." prompt = _prompt_mapping(req.prompts[0]) sampling_params = req.sampling_params knobs = dict((prompt.get("additional_information") or {}).get("auk") or {}) text = _unwrap_single(prompt.get("prompt_embeds")) if text is None: raise ValueError("AuK stage 1 needs `prompt_embeds`, the fused text condition from the encoder stage.") text = torch.as_tensor(text).to(device=self.device, dtype=self.dtype) if text.ndim == 3 and text.shape[0] == 1: text = text[0] if text.ndim != 2: raise ValueError(f"AuK `prompt_embeds` must be [nt, text_hidden_dim], got {tuple(text.shape)}.") text = text.unsqueeze(0) c_mask = torch.ones(text.shape[:2], dtype=torch.bool, device=self.device) generator = self._resolve_generator(sampling_params) audio = (prompt.get("multi_modal_data") or {}).get("audio") output_type = sampling_params.output_type or "np" with torch.inference_mode(): # The VAE posterior draw gets its own generator, seeded with a # fixed offset from the request seed: the initial latent below then # still equals the reference's fresh manual_seed draw, and the two # noise streams are not copies of each other. vae_generator = None if knobs.get("vae_sample") and generator is not None: vae_seed = (int(generator.initial_seed()) + _VAE_SEED_OFFSET) % (2**63) vae_generator = torch.Generator(device=self.device).manual_seed(vae_seed) ref = self._encode_source(audio, sample=bool(knobs.get("vae_sample")), generator=vae_generator) ref_frames = ref.shape[1] ref_mask = torch.ones(ref.shape[:2], dtype=torch.bool, device=self.device) gen_frames = self._target_frames(knobs.get("gen_seconds"), ref_frames) nfe, cfg, sway, t_grid = self._resolve_schedule(sampling_params, knobs) with self._dit_autocast(): latents = sample_latents( self.dit, text=text, c_mask=c_mask, ref=ref, ref_mask=ref_mask, gen_frames=gen_frames, nfe=nfe, cfg_strength=cfg, sway_sampling_coef=sway, t_grid=t_grid, seed=None, latent_dim=self.latent_dim, device=self.device, dtype=torch.float32, generator=generator, ) latents = latents.float() if not torch.isfinite(latents).all(): raise RuntimeError("AuK generated latents contain NaN or Inf.") if output_type == "latent": return [DiffusionOutput(output=latents.detach().cpu())] wav = self.vae.decode(latents) # One mono waveform per request; the formatter expects [T]. wav = wav.detach().to(device="cpu", dtype=torch.float32).reshape(-1) if not torch.isfinite(wav).all(): raise RuntimeError("AuK generated audio contains NaN or Inf.") return [DiffusionOutput(output=wav)] def _read_config(model_dir: str) -> dict[str, Any]: """Read the assembled AuK ``config.json``.""" path = os.path.join(model_dir, "config.json") try: with open(path) as handle: config = json.load(handle) except FileNotFoundError as exc: raise ValueError(f"AuK checkpoint is missing config.json: {path}") from exc except json.JSONDecodeError as exc: logger.error("AuK config.json is not valid JSON: %s", path) raise ValueError(f"AuK config.json is not valid JSON: {path}") from exc missing = [key for key in ("dit", "vae") if key not in config] if missing: raise ValueError(f"AuK config.json is missing required sections {missing}: {path}") return config def _read_dit_weights(model_dir: str, dtype: torch.dtype) -> dict[str, torch.Tensor]: """Read the DiT state dict from ``auk.safetensors``, cast to ``dtype``. Tensors are cast one at a time so the fp32 checkpoint is never fully resident alongside the model. """ path = os.path.join(model_dir, "auk.safetensors") if not os.path.isfile(path): raise ValueError(f"AuK checkpoint is missing auk.safetensors: {path}") with safe_open(path, framework="pt", device="cpu") as checkpoint: keys = list(checkpoint.keys()) state = dit_state_dict(((key, checkpoint.get_tensor(key).to(dtype)) for key in keys), _DIT_PREFIX) if not state: raise ValueError(f"AuK checkpoint has no '{_DIT_PREFIX}*' weights: {path}") unexpected = sorted(key for key in keys if key not in _FUSION_KEYS and not key.startswith(_DIT_PREFIX)) if unexpected: logger.warning("Ignoring %d unrecognized keys in %s, e.g. %s", len(unexpected), path, unexpected[:5]) return state