返回 JoyAI-Echo
samplers.py
1 import logging
2 from dataclasses import replace
3 from functools import partial
4 from typing import Callable
5
6 import torch
7 from tqdm import tqdm
8
9 from ltx_core.components.diffusion_steps import Res2sDiffusionStep
10 from ltx_core.components.protocols import DiffusionStepProtocol
11 from ltx_core.utils import to_denoised, to_velocity
12 from ltx_pipelines.utils.helpers import post_process_latent, timesteps_from_mask
13 from ltx_pipelines.utils.res2s import get_res2s_coefficients
14 from ltx_pipelines.utils.types import DenoisingFunc, LatentState
15
16 logger = logging.getLogger(__name__)
17
18
19 def euler_denoising_loop(
20 sigmas: torch.Tensor,
21 video_state: LatentState,
22 audio_state: LatentState,
23 stepper: DiffusionStepProtocol,
24 denoise_fn: DenoisingFunc,
25 ) -> tuple[LatentState, LatentState]:
26 """
27 Perform the joint audio-video denoising loop over a diffusion schedule.
28 This function iterates over all but the final value in ``sigmas`` and, at
29 each diffusion step, calls ``denoise_fn`` to obtain denoised video and
30 audio latents. The denoised latents are post-processed with their
31 respective denoise masks and clean latents, then passed to ``stepper`` to
32 advance the noisy latents one step along the diffusion schedule.
33 ### Parameters
34 sigmas:
35 A 1D tensor of noise levels (diffusion sigmas) defining the sampling
36 schedule. All steps except the last element are iterated over.
37 video_state:
38 The current video :class:`LatentState`, containing the noisy latent,
39 its clean reference latent, and the denoising mask.
40 audio_state:
41 The current audio :class:`LatentState`, analogous to ``video_state``
42 but for the audio modality.
43 stepper:
44 An implementation of :class:`DiffusionStepProtocol` that updates a
45 latent given the current latent, its denoised estimate, the full
46 ``sigmas`` schedule, and the current step index.
47 denoise_fn:
48 A callable implementing :class:`DenoisingFunc`. It is invoked as
49 ``denoise_fn(video_state, audio_state, sigmas, step_index)`` and must
50 return a tuple ``(denoised_video, denoised_audio)``, where each element
51 is a tensor with the same shape as the corresponding latent.
52 ### Returns
53 tuple[LatentState, LatentState]
54 A pair ``(video_state, audio_state)`` containing the final video and
55 audio latent states after completing the denoising loop.
56 """
57 for step_idx, _ in enumerate(tqdm(sigmas[:-1])):
58 denoised_video, denoised_audio = denoise_fn(video_state, audio_state, sigmas, step_idx)
59
60 denoised_video = post_process_latent(denoised_video, video_state.denoise_mask, video_state.clean_latent)
61 denoised_audio = post_process_latent(denoised_audio, audio_state.denoise_mask, audio_state.clean_latent)
62
63 video_state = replace(video_state, latent=stepper.step(video_state.latent, denoised_video, sigmas, step_idx))
64 audio_state = replace(audio_state, latent=stepper.step(audio_state.latent, denoised_audio, sigmas, step_idx))
65
66 return (video_state, audio_state)
67
68
69 def gradient_estimating_euler_denoising_loop(
70 sigmas: torch.Tensor,
71 video_state: LatentState,
72 audio_state: LatentState,
73 stepper: DiffusionStepProtocol,
74 denoise_fn: DenoisingFunc,
75 ge_gamma: float = 2.0,
76 ) -> tuple[LatentState, LatentState]:
77 """
78 Perform the joint audio-video denoising loop using gradient-estimation sampling.
79 This function is similar to :func:`euler_denoising_loop`, but applies
80 gradient estimation to improve the denoised estimates by tracking velocity
81 changes across steps. See the referenced function for detailed parameter
82 documentation.
83 ### Parameters
84 ge_gamma:
85 Gradient estimation coefficient controlling the velocity correction term.
86 Default is 2.0. Paper: https://openreview.net/pdf?id=o2ND9v0CeK
87 sigmas, video_state, audio_state, stepper, denoise_fn:
88 See :func:`euler_denoising_loop` for parameter descriptions.
89 ### Returns
90 tuple[LatentState, LatentState]
91 See :func:`euler_denoising_loop` for return value description.
92 """
93
94 previous_audio_velocity = None
95 previous_video_velocity = None
96
97 def update_velocity_and_sample(
98 noisy_sample: torch.Tensor, denoised_sample: torch.Tensor, sigma: float, previous_velocity: torch.Tensor | None
99 ) -> tuple[torch.Tensor, torch.Tensor]:
100 current_velocity = to_velocity(noisy_sample, sigma, denoised_sample)
101 if previous_velocity is not None:
102 delta_v = current_velocity - previous_velocity
103 total_velocity = ge_gamma * delta_v + previous_velocity
104 denoised_sample = to_denoised(noisy_sample, total_velocity, sigma)
105 return current_velocity, denoised_sample
106
107 for step_idx, _ in enumerate(tqdm(sigmas[:-1])):
108 denoised_video, denoised_audio = denoise_fn(video_state, audio_state, sigmas, step_idx)
109
110 denoised_video = post_process_latent(denoised_video, video_state.denoise_mask, video_state.clean_latent)
111 denoised_audio = post_process_latent(denoised_audio, audio_state.denoise_mask, audio_state.clean_latent)
112
113 if sigmas[step_idx + 1] == 0:
114 return replace(video_state, latent=denoised_video), replace(audio_state, latent=denoised_audio)
115
116 previous_video_velocity, denoised_video = update_velocity_and_sample(
117 video_state.latent, denoised_video, sigmas[step_idx], previous_video_velocity
118 )
119 previous_audio_velocity, denoised_audio = update_velocity_and_sample(
120 audio_state.latent, denoised_audio, sigmas[step_idx], previous_audio_velocity
121 )
122
123 video_state = replace(video_state, latent=stepper.step(video_state.latent, denoised_video, sigmas, step_idx))
124 audio_state = replace(audio_state, latent=stepper.step(audio_state.latent, denoised_audio, sigmas, step_idx))
125
126 return (video_state, audio_state)
127
128
129 def _channelwise_normalize(x: torch.Tensor) -> torch.Tensor:
130 return x.sub_(x.mean(dim=(-2, -1), keepdim=True)).div_(x.std(dim=(-2, -1), keepdim=True))
131
132
133 def _get_new_noise(x: torch.Tensor, generator: torch.Generator) -> torch.Tensor:
134 noise = torch.randn(x.shape, generator=generator, dtype=torch.float64, device=generator.device)
135 noise = (noise - noise.mean()) / noise.std()
136 return _channelwise_normalize(noise)
137
138
139 def _inject_sde_noise(
140 state: LatentState,
141 sample: torch.Tensor,
142 denoised_sample: torch.Tensor,
143 step_noise_generator: torch.Generator,
144 new_noise_fn: Callable[[torch.Tensor, torch.Generator], torch.Tensor],
145 stepper: DiffusionStepProtocol,
146 sigmas: torch.Tensor,
147 step_idx: int,
148 legacy_mode: bool = False,
149 ) -> torch.Tensor:
150 sigmas_copy = sigmas.clone()
151 new_noise = new_noise_fn(state.latent, step_noise_generator)
152 if not legacy_mode:
153 timesteps = timesteps_from_mask(state.denoise_mask.double(), sigmas_copy[step_idx].double())
154 next_timesteps = timesteps_from_mask(state.denoise_mask.double(), sigmas_copy[step_idx + 1].double())
155 sigmas = torch.stack([timesteps, next_timesteps])
156 step_idx = 0
157 x_next = stepper.step(
158 sample=sample,
159 denoised_sample=denoised_sample,
160 sigmas=sigmas,
161 step_index=step_idx,
162 noise=new_noise,
163 )
164
165 if legacy_mode:
166 x_next = post_process_latent(x_next, state.denoise_mask, state.clean_latent)
167
168 return x_next
169
170
171 def res2s_audio_video_denoising_loop( # noqa: PLR0913,PLR0915
172 sigmas: torch.Tensor,
173 video_state: LatentState,
174 audio_state: LatentState,
175 stepper: DiffusionStepProtocol,
176 denoise_fn: DenoisingFunc,
177 noise_seed: int = -1,
178 noise_seed_substep: int | None = None,
179 bongmath: bool = True,
180 bongmath_max_iter: int = 100,
181 new_noise_fn: Callable[[torch.Tensor, torch.Generator], torch.Tensor] = _get_new_noise,
182 model_dtype: torch.dtype = torch.bfloat16,
183 legacy_mode: bool = True,
184 ) -> tuple[LatentState, LatentState]:
185 """
186 Joint audio-video denoising loop using the res_2s second-order sampler.
187 Iterates over the diffusion schedule with a two-stage Runge-Kutta step:
188 evaluates the denoiser at the current point and at a midpoint (with SDE
189 noise), then combines both with RK coefficients. Supports anchor-point
190 refinement (bong iteration) and optional SDE noise injection. Requires
191 :class:`Res2sDiffusionStep` as ``stepper``.
192 ### Parameters
193 sigmas:
194 A 1D tensor of noise levels defining the sampling schedule.
195 video_state:
196 Current video :class:`LatentState` (noisy latent, clean reference, mask).
197 audio_state:
198 Current audio :class:`LatentState`, same structure as ``video_state``.
199 stepper:
200 Must be an instance of :class:`Res2sDiffusionStep`; performs SDE step
201 with noise injection.
202 denoise_fn:
203 Callable ``(video_state, audio_state, sigmas, step_index)`` returning
204 ``(denoised_video, denoised_audio)``.
205 noise_seed:
206 Seed for step-level SDE noise; substep seed defaults to ``noise_seed + 10000``.
207 noise_seed_substep:
208 Optional seed for substep SDE noise; if None, derived from ``noise_seed``.
209 bongmath:
210 Whether to run iterative anchor refinement (bong iteration) when step size is small.
211 bongmath_max_iter:
212 Max iterations for bong refinement when enabled.
213 new_noise_fn:
214 Callable ``(latent, generator) -> noise`` for SDE injection; default
215 uses normalized channel-wise Gaussian noise.
216 model_dtype:
217 Dtype for latent state updates (e.g. bfloat16).
218 ### Returns
219 tuple[LatentState, LatentState]
220 Final ``(video_state, audio_state)`` after the denoising loop.
221 """
222 # Initialize noise generators with different seeds
223 if noise_seed_substep is None:
224 noise_seed_substep = noise_seed + 10000 # Offset to ensure different seeds
225 step_noise_generator = torch.Generator(device=video_state.latent.device).manual_seed(noise_seed)
226 substep_noise_generator = torch.Generator(device=video_state.latent.device).manual_seed(noise_seed_substep)
227 sde_noise_injecting_fn = partial(
228 _inject_sde_noise, stepper=stepper, new_noise_fn=new_noise_fn, legacy_mode=legacy_mode
229 )
230 step_noise_injecting_fn = partial(sde_noise_injecting_fn, step_noise_generator=step_noise_generator)
231 substep_noise_injecting_fn = partial(sde_noise_injecting_fn, step_noise_generator=substep_noise_generator)
232
233 if not isinstance(stepper, Res2sDiffusionStep):
234 raise ValueError("stepper must be an instance of Res2sDiffusionStep")
235
236 n_full_steps = len(sigmas) - 1
237 # inject minimal sigma value to avoid division by zero
238 if sigmas[-1] == 0:
239 sigmas = torch.cat([sigmas[:-1], torch.tensor([0.0011, 0.0], device=sigmas.device)], dim=0)
240 # Compute step sizes in hyperbolic space
241 hs = -torch.log(sigmas[1:].double().cpu() / (sigmas[:-1].double().cpu()))
242
243 # Initialize phi cache for reuse across loop iterations
244 # Cache key: (j, neg_h) where j is phi order and neg_h is negative step value
245 phi_cache = {}
246 c2 = 0.5 # Midpoint for res_2s
247
248 # Progress bar shows only full two-stage steps; final (sigma_next==0) step is done silently
249
250 for step_idx in tqdm(range(n_full_steps)):
251 sigma = sigmas[step_idx].double()
252 sigma_next = sigmas[step_idx + 1].double()
253
254 # Initialize anchor point
255 x_anchor_video = video_state.latent.clone().double()
256 x_anchor_audio = audio_state.latent.clone().double()
257
258 # ====================================================================
259 # STAGE 1: Evaluate at current point
260 # ====================================================================
261 denoised_video_1, denoised_audio_1 = denoise_fn(video_state, audio_state, sigmas, step_idx)
262 denoised_video_1 = post_process_latent(denoised_video_1, video_state.denoise_mask, video_state.clean_latent)
263 denoised_audio_1 = post_process_latent(denoised_audio_1, audio_state.denoise_mask, audio_state.clean_latent)
264
265 h = hs[step_idx].item()
266
267 # Compute RK coefficients (pass phi_cache for caching)
268 a21, b1, b2 = get_res2s_coefficients(h, phi_cache, c2)
269
270 # Compute substep sigma, sqrt is a hardcode for c2 = 0.5
271 sub_sigma = torch.sqrt(sigma * sigma_next)
272
273 # ====================================================================
274 # Compute substep x using RK coefficient a21
275 # ====================================================================
276 eps_1_video = denoised_video_1.double() - x_anchor_video
277 eps_1_audio = denoised_audio_1.double() - x_anchor_audio
278
279 x_mid_video = x_anchor_video.double() + h * a21 * eps_1_video
280 x_mid_audio = x_anchor_audio.double() + h * a21 * eps_1_audio
281
282 # ====================================================================
283 # SDE noise injection at substep
284 # ====================================================================
285 x_mid_video = substep_noise_injecting_fn(
286 state=video_state,
287 sample=x_anchor_video,
288 denoised_sample=x_mid_video,
289 sigmas=torch.stack([sigma, sub_sigma]),
290 step_idx=0,
291 )
292 x_mid_audio = substep_noise_injecting_fn(
293 state=audio_state,
294 sample=x_anchor_audio,
295 denoised_sample=x_mid_audio,
296 sigmas=torch.stack([sigma, sub_sigma]),
297 step_idx=0,
298 )
299 # ====================================================================
300 # ITERATIVE REFINEMENT (Bong Iteration) - Stabilize anchor point
301 # ====================================================================
302 if bongmath and h < 0.5 and sigma > 0.03:
303 for _ in range(bongmath_max_iter):
304 x_anchor_video = x_mid_video - h * a21 * eps_1_video
305 eps_1_video = denoised_video_1.double() - x_anchor_video
306 x_anchor_audio = x_mid_audio - h * a21 * eps_1_audio
307 eps_1_audio = denoised_audio_1.double() - x_anchor_audio
308
309 # ====================================================================
310 # STAGE 2: Evaluate at substep point (WITH NOISE)
311 # ====================================================================
312 mid_video_state = replace(video_state, latent=x_mid_video.to(model_dtype))
313 mid_audio_state = replace(audio_state, latent=x_mid_audio.to(model_dtype))
314
315 denoised_video_2, denoised_audio_2 = denoise_fn(
316 video_state=mid_video_state,
317 audio_state=mid_audio_state,
318 sigmas=torch.stack([sub_sigma]).to(sigmas.device),
319 step_index=0,
320 )
321 denoised_video_2 = post_process_latent(denoised_video_2, video_state.denoise_mask, video_state.clean_latent)
322 denoised_audio_2 = post_process_latent(denoised_audio_2, audio_state.denoise_mask, audio_state.clean_latent)
323
324 # ====================================================================
325 # FINAL COMBINATION: Compute x_next using RK coefficients
326 # ====================================================================
327 eps_2_video = denoised_video_2.double() - x_anchor_video
328 eps_2_audio = denoised_audio_2.double() - x_anchor_audio
329
330 x_next_video = x_anchor_video + h * (b1 * eps_1_video + b2 * eps_2_video)
331 x_next_audio = x_anchor_audio + h * (b1 * eps_1_audio + b2 * eps_2_audio)
332
333 # ====================================================================
334 # SDE NOISE INJECTION AT STEP LEVEL
335 # ====================================================================
336 x_next_video = step_noise_injecting_fn(
337 state=video_state,
338 sample=x_anchor_video,
339 denoised_sample=x_next_video,
340 sigmas=sigmas,
341 step_idx=step_idx,
342 )
343 x_next_audio = step_noise_injecting_fn(
344 state=audio_state,
345 sample=x_anchor_audio,
346 denoised_sample=x_next_audio,
347 sigmas=sigmas,
348 step_idx=step_idx,
349 )
350
351 # Update states
352 video_state = replace(video_state, latent=x_next_video.to(model_dtype))
353 audio_state = replace(audio_state, latent=x_next_audio.to(model_dtype))
354
355 # Final step if we need to fully remove the noise
356 if sigmas[-1] == 0:
357 denoised_video_1, denoised_audio_1 = denoise_fn(video_state, audio_state, sigmas, n_full_steps)
358 denoised_video_1 = post_process_latent(denoised_video_1, video_state.denoise_mask, video_state.clean_latent)
359 denoised_audio_1 = post_process_latent(denoised_audio_1, audio_state.denoise_mask, audio_state.clean_latent)
360 video_state = replace(video_state, latent=denoised_video_1.to(model_dtype))
361 audio_state = replace(audio_state, latent=denoised_audio_1.to(model_dtype))
362
363 return video_state, audio_state
364
364 lines PYTHON