import logging from dataclasses import replace from functools import partial from typing import Callable import torch from tqdm import tqdm from ltx_core.components.diffusion_steps import EulerCfgPpDiffusionStep, Res2sDiffusionStep from ltx_core.components.protocols import DiffusionStepProtocol from ltx_core.devices import highest_precision_float from ltx_core.model.transformer import X0Model from ltx_core.utils import to_denoised, to_velocity from ltx_pipelines.utils.helpers import post_process_latent, timesteps_from_mask from ltx_pipelines.utils.res2s import get_res2s_coefficients from ltx_pipelines.utils.types import Denoiser, LatentState logger = logging.getLogger(__name__) def _step_state( state: LatentState | None, denoised: torch.Tensor | None, stepper: DiffusionStepProtocol, sigmas: torch.Tensor, step_idx: int, ) -> LatentState | None: """Advance one diffusion step for a single modality, or return ``None`` if absent.""" if state is None or denoised is None: return state denoised = post_process_latent(denoised, state.denoise_mask, state.clean_latent) return replace(state, latent=stepper.step(state.latent, denoised, sigmas, step_idx)) def euler_denoising_loop( sigmas: torch.Tensor, video_state: LatentState | None, audio_state: LatentState | None, stepper: DiffusionStepProtocol, transformer: X0Model, denoiser: Denoiser, ) -> tuple[LatentState | None, LatentState | None]: """ Perform the joint audio-video denoising loop over a diffusion schedule. Either ``video_state`` or ``audio_state`` may be ``None`` for absent modalities; the absent modality is passed through unchanged. ### Parameters sigmas: A 1D tensor of noise levels (diffusion sigmas) defining the sampling schedule. All steps except the last element are iterated over. video_state: The current video :class:`LatentState`, or ``None`` if video is absent. audio_state: The current audio :class:`LatentState`, or ``None`` if audio is absent. stepper: An implementation of :class:`DiffusionStepProtocol` that updates a latent given the current latent, its denoised estimate, the full ``sigmas`` schedule, and the current step index. transformer: The diffusion model passed to the denoiser at each step. denoiser: A callable implementing :class:`Denoiser`. It is invoked as ``denoiser(transformer, video_state, audio_state, sigmas, step_index)`` and must return a :class:`~ltx_pipelines.utils.types.DenoisedLatentResult`. ### Returns tuple[LatentState | None, LatentState | None] Final ``(video_state, audio_state)`` after the denoising loop. """ for step_idx, _ in enumerate(tqdm(sigmas[:-1])): video_result, audio_result = denoiser(transformer, video_state, audio_state, sigmas, step_idx) denoised_video = video_result.denoised if video_result is not None else None denoised_audio = audio_result.denoised if audio_result is not None else None video_state = _step_state(video_state, denoised_video, stepper, sigmas, step_idx) audio_state = _step_state(audio_state, denoised_audio, stepper, sigmas, step_idx) return (video_state, audio_state) def gradient_estimating_euler_denoising_loop( sigmas: torch.Tensor, video_state: LatentState | None, audio_state: LatentState | None, stepper: DiffusionStepProtocol, transformer: X0Model, denoiser: Denoiser, ge_gamma: float = 2.0, ) -> tuple[LatentState | None, LatentState | None]: """ Perform the joint audio-video denoising loop using gradient-estimation sampling. Same interface as :func:`euler_denoising_loop` with an additional ``ge_gamma`` parameter for velocity correction. ### Parameters ge_gamma: Gradient estimation coefficient controlling the velocity correction term. Default is 2.0. Paper: https://openreview.net/pdf?id=o2ND9v0CeK ### Returns tuple[LatentState | None, LatentState | None] See :func:`euler_denoising_loop` for return value description. """ previous_audio_velocity = None previous_video_velocity = None def update_velocity_and_sample( noisy_sample: torch.Tensor, denoised_sample: torch.Tensor, sigma: float, previous_velocity: torch.Tensor | None ) -> tuple[torch.Tensor, torch.Tensor]: current_velocity = to_velocity(noisy_sample, sigma, denoised_sample) if previous_velocity is not None: delta_v = current_velocity - previous_velocity total_velocity = ge_gamma * delta_v + previous_velocity denoised_sample = to_denoised(noisy_sample, total_velocity, sigma) return current_velocity, denoised_sample for step_idx, _ in enumerate(tqdm(sigmas[:-1])): video_result, audio_result = denoiser(transformer, video_state, audio_state, sigmas, step_idx) denoised_video = video_result.denoised if video_result is not None else None denoised_audio = audio_result.denoised if audio_result is not None else None if video_state is not None and denoised_video is not None: denoised_video = post_process_latent(denoised_video, video_state.denoise_mask, video_state.clean_latent) if audio_state is not None and denoised_audio is not None: denoised_audio = post_process_latent(denoised_audio, audio_state.denoise_mask, audio_state.clean_latent) if sigmas[step_idx + 1] == 0: if video_state is not None and denoised_video is not None: video_state = replace(video_state, latent=denoised_video) if audio_state is not None and denoised_audio is not None: audio_state = replace(audio_state, latent=denoised_audio) return video_state, audio_state if video_state is not None and denoised_video is not None: previous_video_velocity, denoised_video = update_velocity_and_sample( video_state.latent, denoised_video, sigmas[step_idx], previous_video_velocity ) video_state = replace( video_state, latent=stepper.step(video_state.latent, denoised_video, sigmas, step_idx) ) if audio_state is not None and denoised_audio is not None: previous_audio_velocity, denoised_audio = update_velocity_and_sample( audio_state.latent, denoised_audio, sigmas[step_idx], previous_audio_velocity ) audio_state = replace( audio_state, latent=stepper.step(audio_state.latent, denoised_audio, sigmas, step_idx) ) return (video_state, audio_state) def _get_plain_noise(x: torch.Tensor, generator: torch.Generator) -> torch.Tensor: """Draw standard Gaussian noise matching the shape, dtype, and device of ``x``.""" return torch.randn(x.shape, generator=generator, dtype=x.dtype, device=x.device) def _channelwise_normalize(x: torch.Tensor) -> torch.Tensor: return x.sub_(x.mean(dim=(-2, -1), keepdim=True)).div_(x.std(dim=(-2, -1), keepdim=True)) def _get_new_noise(x: torch.Tensor, generator: torch.Generator) -> torch.Tensor: # float64 on CUDA/CPU for numerical stability; MPS has no float64, so degrade to float32. noise = torch.randn( x.shape, generator=generator, dtype=highest_precision_float(generator.device), device=generator.device ) noise = (noise - noise.mean()) / noise.std() return _channelwise_normalize(noise) def _inject_sde_noise( state: LatentState, sample: torch.Tensor, denoised_sample: torch.Tensor, step_noise_generator: torch.Generator, new_noise_fn: Callable[[torch.Tensor, torch.Generator], torch.Tensor], stepper: DiffusionStepProtocol, sigmas: torch.Tensor, step_idx: int, legacy_mode: bool = False, eta: float = 0.5, ) -> torch.Tensor: sigmas_copy = sigmas.clone() hp = highest_precision_float(state.denoise_mask.device) new_noise = new_noise_fn(state.latent, step_noise_generator) if not legacy_mode: timesteps = timesteps_from_mask(state.denoise_mask.to(hp), sigmas_copy[step_idx].to(hp)) next_timesteps = timesteps_from_mask(state.denoise_mask.to(hp), sigmas_copy[step_idx + 1].to(hp)) sigmas = torch.stack([timesteps, next_timesteps]) step_idx = 0 x_next = stepper.step( sample=sample, denoised_sample=denoised_sample, sigmas=sigmas, step_index=step_idx, noise=new_noise, eta=eta, ) if legacy_mode: x_next = post_process_latent(x_next, state.denoise_mask, state.clean_latent) return x_next def res2s_audio_video_denoising_loop( # noqa: PLR0913,PLR0915,PLR0912 sigmas: torch.Tensor, video_state: LatentState | None, audio_state: LatentState | None, stepper: DiffusionStepProtocol, transformer: X0Model, denoiser: Denoiser, noise_seed: int = -1, noise_seed_substep: int | None = None, eta: float = 0.5, bongmath: bool = True, bongmath_max_iter: int = 100, new_noise_fn: Callable[[torch.Tensor, torch.Generator], torch.Tensor] = _get_new_noise, model_dtype: torch.dtype = torch.bfloat16, legacy_mode: bool = True, ) -> tuple[LatentState | None, LatentState | None]: """ Joint audio-video denoising loop using the res_2s second-order sampler. Iterates over the diffusion schedule with a two-stage Runge-Kutta step: evaluates the denoiser at the current point and at a midpoint (with SDE noise), then combines both with RK coefficients. Supports anchor-point refinement (bong iteration) and optional SDE noise injection. Requires :class:`Res2sDiffusionStep` as ``stepper``. Either modality may be ``None`` (absent). ### Parameters transformer: The diffusion model passed to the denoiser at each step. denoiser: Callable implementing :class:`Denoiser`. noise_seed: Seed for step-level SDE noise; substep seed defaults to ``noise_seed + 10000``. noise_seed_substep: Optional seed for substep SDE noise; if None, derived from ``noise_seed``. eta: Controls stochastic noise injection strength (0=deterministic, 1=maximum). Applies to main diffusion steps; substeps always use 0.5. Default 0.5. bongmath: Whether to run iterative anchor refinement (bong iteration) when step size is small. bongmath_max_iter: Max iterations for bong refinement when enabled. new_noise_fn: Callable ``(latent, generator) -> noise`` for SDE injection. model_dtype: Dtype for latent state updates (e.g. bfloat16). ### Returns tuple[LatentState | None, LatentState | None] Final ``(video_state, audio_state)`` after the denoising loop. """ # Determine device from whichever state is present present_state = video_state or audio_state if present_state is None: raise ValueError("At least one of video_state or audio_state must be provided") state_device = present_state.latent.device # float64 on CUDA/CPU for ODE numerical stability; MPS has no float64, so degrade to float32. hp = highest_precision_float(state_device) # Initialize noise generators with different seeds if noise_seed_substep is None: noise_seed_substep = noise_seed + 10000 # Offset to ensure different seeds step_noise_generator = torch.Generator(device=state_device).manual_seed(noise_seed) substep_noise_generator = torch.Generator(device=state_device).manual_seed(noise_seed_substep) sde_noise_injecting_fn = partial( _inject_sde_noise, stepper=stepper, new_noise_fn=new_noise_fn, legacy_mode=legacy_mode ) step_noise_injecting_fn = partial(sde_noise_injecting_fn, step_noise_generator=step_noise_generator, eta=eta) # substep eta is always default 0.5 for compatibility with original implementation. substep_noise_injecting_fn = partial(sde_noise_injecting_fn, step_noise_generator=substep_noise_generator, eta=0.5) if not isinstance(stepper, Res2sDiffusionStep): raise ValueError("stepper must be an instance of Res2sDiffusionStep") n_full_steps = len(sigmas) - 1 # inject minimal sigma value to avoid division by zero if sigmas[-1] == 0: sigmas = torch.cat([sigmas[:-1], torch.tensor([0.0011, 0.0], device=sigmas.device)], dim=0) # Compute step sizes in hyperbolic space hs = -torch.log(sigmas[1:].to(hp).cpu() / (sigmas[:-1].to(hp).cpu())) # Initialize phi cache for reuse across loop iterations phi_cache = {} c2 = 0.5 # Midpoint for res_2s for step_idx in tqdm(range(n_full_steps)): sigma = sigmas[step_idx].to(hp) sigma_next = sigmas[step_idx + 1].to(hp) # Initialize anchor point x_anchor_video = video_state.latent.clone().to(hp) if video_state is not None else None x_anchor_audio = audio_state.latent.clone().to(hp) if audio_state is not None else None # ==================================================================== # STAGE 1: Evaluate at current point # ==================================================================== video_result, audio_result = denoiser(transformer, video_state, audio_state, sigmas, step_idx) denoised_video_1 = video_result.denoised if video_result is not None else None denoised_audio_1 = audio_result.denoised if audio_result is not None else None if video_state is not None and denoised_video_1 is not None: denoised_video_1 = post_process_latent(denoised_video_1, video_state.denoise_mask, video_state.clean_latent) if audio_state is not None and denoised_audio_1 is not None: denoised_audio_1 = post_process_latent(denoised_audio_1, audio_state.denoise_mask, audio_state.clean_latent) h = hs[step_idx].item() # Compute RK coefficients (pass phi_cache for caching) a21, b1, b2 = get_res2s_coefficients(h, phi_cache, c2) # Compute substep sigma, sqrt is a hardcode for c2 = 0.5 sub_sigma = torch.sqrt(sigma * sigma_next) # ==================================================================== # Compute substep x using RK coefficient a21 # ==================================================================== if x_anchor_video is not None and denoised_video_1 is not None: eps_1_video = denoised_video_1.to(hp) - x_anchor_video x_mid_video = x_anchor_video.to(hp) + h * a21 * eps_1_video else: eps_1_video = None x_mid_video = None if x_anchor_audio is not None and denoised_audio_1 is not None: eps_1_audio = denoised_audio_1.to(hp) - x_anchor_audio x_mid_audio = x_anchor_audio.to(hp) + h * a21 * eps_1_audio else: eps_1_audio = None x_mid_audio = None # ==================================================================== # SDE noise injection at substep # ==================================================================== if x_mid_video is not None and video_state is not None: x_mid_video = substep_noise_injecting_fn( state=video_state, sample=x_anchor_video, denoised_sample=x_mid_video, sigmas=torch.stack([sigma, sub_sigma]), step_idx=0, ) if x_mid_audio is not None and audio_state is not None: x_mid_audio = substep_noise_injecting_fn( state=audio_state, sample=x_anchor_audio, denoised_sample=x_mid_audio, sigmas=torch.stack([sigma, sub_sigma]), step_idx=0, ) # ==================================================================== # ITERATIVE REFINEMENT (Bong Iteration) # ==================================================================== if bongmath and h < 0.5 and sigma > 0.03: for _ in range(bongmath_max_iter): if x_mid_video is not None and eps_1_video is not None: x_anchor_video = x_mid_video - h * a21 * eps_1_video eps_1_video = denoised_video_1.to(hp) - x_anchor_video if x_mid_audio is not None and eps_1_audio is not None: x_anchor_audio = x_mid_audio - h * a21 * eps_1_audio eps_1_audio = denoised_audio_1.to(hp) - x_anchor_audio # ==================================================================== # STAGE 2: Evaluate at substep point (WITH NOISE) # ==================================================================== mid_video_state = ( replace(video_state, latent=x_mid_video.to(model_dtype)) if video_state is not None and x_mid_video is not None else None ) mid_audio_state = ( replace(audio_state, latent=x_mid_audio.to(model_dtype)) if audio_state is not None and x_mid_audio is not None else None ) video_result_2, audio_result_2 = denoiser( transformer, video_state=mid_video_state, audio_state=mid_audio_state, sigmas=torch.stack([sub_sigma]).to(sigmas.device), step_index=0, ) denoised_video_2 = video_result_2.denoised if video_result_2 is not None else None denoised_audio_2 = audio_result_2.denoised if audio_result_2 is not None else None if video_state is not None and denoised_video_2 is not None: denoised_video_2 = post_process_latent(denoised_video_2, video_state.denoise_mask, video_state.clean_latent) if audio_state is not None and denoised_audio_2 is not None: denoised_audio_2 = post_process_latent(denoised_audio_2, audio_state.denoise_mask, audio_state.clean_latent) # ==================================================================== # FINAL COMBINATION: Compute x_next using RK coefficients # ==================================================================== if x_anchor_video is not None and eps_1_video is not None and denoised_video_2 is not None: eps_2_video = denoised_video_2.to(hp) - x_anchor_video x_next_video = x_anchor_video + h * (b1 * eps_1_video + b2 * eps_2_video) else: x_next_video = None if x_anchor_audio is not None and eps_1_audio is not None and denoised_audio_2 is not None: eps_2_audio = denoised_audio_2.to(hp) - x_anchor_audio x_next_audio = x_anchor_audio + h * (b1 * eps_1_audio + b2 * eps_2_audio) else: x_next_audio = None # ==================================================================== # SDE NOISE INJECTION AT STEP LEVEL # ==================================================================== if x_next_video is not None and video_state is not None: x_next_video = step_noise_injecting_fn( state=video_state, sample=x_anchor_video, denoised_sample=x_next_video, sigmas=sigmas, step_idx=step_idx, ) if x_next_audio is not None and audio_state is not None: x_next_audio = step_noise_injecting_fn( state=audio_state, sample=x_anchor_audio, denoised_sample=x_next_audio, sigmas=sigmas, step_idx=step_idx, ) # Update states if video_state is not None and x_next_video is not None: video_state = replace(video_state, latent=x_next_video.to(model_dtype)) if audio_state is not None and x_next_audio is not None: audio_state = replace(audio_state, latent=x_next_audio.to(model_dtype)) # Final step if we need to fully remove the noise if sigmas[-1] == 0: video_result_final, audio_result_final = denoiser(transformer, video_state, audio_state, sigmas, n_full_steps) denoised_video_1 = video_result_final.denoised if video_result_final is not None else None denoised_audio_1 = audio_result_final.denoised if audio_result_final is not None else None if video_state is not None and denoised_video_1 is not None: denoised_video_1 = post_process_latent(denoised_video_1, video_state.denoise_mask, video_state.clean_latent) video_state = replace(video_state, latent=denoised_video_1.to(model_dtype)) if audio_state is not None and denoised_audio_1 is not None: denoised_audio_1 = post_process_latent(denoised_audio_1, audio_state.denoise_mask, audio_state.clean_latent) audio_state = replace(audio_state, latent=denoised_audio_1.to(model_dtype)) return video_state, audio_state def euler_cfg_pp_denoising_loop( # noqa: PLR0912 sigmas: torch.Tensor, video_state: LatentState | None, audio_state: LatentState | None, stepper: EulerCfgPpDiffusionStep, transformer: X0Model, denoiser: Denoiser, noise_seed: int = -1, new_noise_fn: Callable[[torch.Tensor, torch.Generator], torch.Tensor] = _get_plain_noise, model_dtype: torch.dtype = torch.bfloat16, ) -> tuple[LatentState | None, LatentState | None]: """ Joint audio-video denoising loop using the CFG++ corrected Euler sampler. Applies the CFG++ update rule at each step: the ODE derivative is computed from the unconditioned denoised prediction rather than the standard velocity, and an ancestral DDIM noise injection is applied in the rescaled sigma space. Requires a guided denoiser whose :class:`~ltx_pipelines.utils.types.DenoisedLatentResult` carries ``uncond`` tensors (i.e. CFG must be enabled). Either ``video_state`` or ``audio_state`` may be ``None`` for absent modalities. When both are present, noise is drawn from the same seeded generator (video first, audio second) to produce a consistent random sequence. ### Parameters sigmas: 1-D tensor of noise levels defining the sampling schedule. video_state: Current video :class:`~ltx_core.types.LatentState`, or ``None``. audio_state: Current audio :class:`~ltx_core.types.LatentState`, or ``None``. stepper: :class:`~ltx_core.components.diffusion_steps.EulerCfgPpDiffusionStep` instance carrying ``eta`` and ``s_noise`` parameters. transformer: The diffusion model passed to the denoiser at each step. denoiser: Callable implementing :class:`~ltx_pipelines.utils.types.Denoiser`. noise_seed: Integer seed for the noise generator. Default ``-1``. new_noise_fn: ``(latent, generator) -> noise`` callable. Defaults to plain ``torch.randn`` (no channel-wise normalization). Pass :func:`_get_new_noise` for the normalized variant used in res2s. model_dtype: Dtype for latent state updates. Default ``bfloat16``. ### Returns tuple[LatentState | None, LatentState | None] Final ``(video_state, audio_state)`` after the denoising loop. """ if not isinstance(stepper, EulerCfgPpDiffusionStep): raise ValueError(f"stepper must be an instance of EulerCfgPpDiffusionStep, got {type(stepper).__name__}") present_state = video_state or audio_state if present_state is None: raise ValueError("At least one of video_state or audio_state must be provided") generator = torch.Generator(device=present_state.latent.device).manual_seed(noise_seed) draw_noise = stepper.eta > 0 and stepper.s_noise > 0 for step_idx, _ in enumerate(tqdm(sigmas[:-1])): video_result, audio_result = denoiser(transformer, video_state, audio_state, sigmas, step_idx) denoised_video = video_result.denoised if video_result is not None else None denoised_audio = audio_result.denoised if audio_result is not None else None uncond_video = video_result.uncond if video_result is not None else None uncond_audio = audio_result.uncond if audio_result is not None else None if video_state is not None and not isinstance(uncond_video, torch.Tensor): raise ValueError( "euler_cfg_pp_denoising_loop requires video DenoisedLatentResult.uncond to be a tensor. " "Use GuidedDenoiser or FactoryGuidedDenoiser with cfg_scale != 1 " "or force_uncond_pass=True and a negative_context." ) if audio_state is not None and not isinstance(uncond_audio, torch.Tensor): raise ValueError( "euler_cfg_pp_denoising_loop requires audio DenoisedLatentResult.uncond to be a tensor. " "Use GuidedDenoiser or FactoryGuidedDenoiser with cfg_scale != 1 " "or force_uncond_pass=True and a negative_context." ) if video_state is not None and denoised_video is not None: denoised_video = post_process_latent( denoised_video.float(), video_state.denoise_mask, video_state.clean_latent ) noisy_video = video_state.latent.float() if audio_state is not None and denoised_audio is not None: denoised_audio = post_process_latent( denoised_audio.float(), audio_state.denoise_mask, audio_state.clean_latent ) noisy_audio = audio_state.latent.float() if sigmas[step_idx + 1] == 0: if video_state is not None and denoised_video is not None: video_state = replace(video_state, latent=denoised_video.to(model_dtype)) if audio_state is not None and denoised_audio is not None: audio_state = replace(audio_state, latent=denoised_audio.to(model_dtype)) return video_state, audio_state if video_state is not None and denoised_video is not None: video_noise = new_noise_fn(video_state.latent, generator) if draw_noise else None x_next = stepper.step( sample=noisy_video, denoised_sample=denoised_video, sigmas=sigmas, step_index=step_idx, uncond_denoised=uncond_video, noise=video_noise, ) if draw_noise: x_next = post_process_latent(x_next, video_state.denoise_mask, video_state.clean_latent) video_state = replace(video_state, latent=x_next.to(model_dtype)) if audio_state is not None and denoised_audio is not None: audio_noise = new_noise_fn(audio_state.latent, generator) if draw_noise else None x_next = stepper.step( sample=noisy_audio, denoised_sample=denoised_audio, sigmas=sigmas, step_index=step_idx, uncond_denoised=uncond_audio, noise=audio_noise, ) if draw_noise: x_next = post_process_latent(x_next, audio_state.denoise_mask, audio_state.clean_latent) audio_state = replace(audio_state, latent=x_next.to(model_dtype)) return video_state, audio_state