Source code for nemo.collections.audio.parts.submodules.diffusion

# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES.  All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from abc import ABC, abstractmethod
from typing import Optional, Tuple, Type

import numpy as np
import torch

from nemo.collections.common.parts.utils import mask_sequence_tensor
from nemo.core.classes import NeuralModule, typecheck
from nemo.core.neural_types import FloatType, LengthsType, NeuralType, SpectrogramType, VoidType
from nemo.utils import logging


[docs] class StochasticDifferentialEquation(NeuralModule, ABC): """Base class for stochastic differential equations.""" def __init__(self, time_min: float, time_max: float, num_steps: int): super().__init__() # min and max time if time_min <= 0: raise ValueError(f'time_min should be positive, current value {time_min}') if time_max <= time_min: raise ValueError(f'time_max should be larger than time_min, current max {time_max} and min {time_min}') self.time_min = time_min self.time_max = time_max # number of steps if num_steps <= 0: raise ValueError(f'num_steps needs to be positive: current value {num_steps}') self.num_steps = num_steps @property def dt(self) -> float: """Time step for this SDE. This denotes the step size between `0` and `self.time_max` when using `self.num_steps`. """ return self.time_max / self.num_steps @property def time_delta(self) -> float: """Time range for this SDE.""" return self.time_max - self.time_min
[docs] def generate_time(self, size: int, device: torch.device) -> torch.Tensor: """Generate random time steps in the valid range. Time steps are generated between `self.time_min` and `self.time_max`. Args: size: number of samples device: device to use Returns: A tensor of floats with shape (size,) """ time = torch.rand(size, device=device) * self.time_delta + self.time_min return time
[docs] @abstractmethod def coefficients(self, state: torch.Tensor, time: torch.Tensor, **kwargs) -> Tuple[torch.Tensor, torch.Tensor]: """ Args: state: tensor of shape (B, C, D, T) time: tensor of shape (B,) Returns: Tuple with drift and diffusion coefficients. """ pass
[docs] @typecheck( input_types={ "prior_mean": NeuralType(('B', 'C', 'D', 'T'), VoidType()), }, output_types={ "sample": NeuralType(('B', 'C', 'D', 'T'), VoidType()), }, ) @abstractmethod def prior_sampling(self, prior_mean: torch.Tensor) -> torch.Tensor: """Generate a sample from the prior distribution p_T. Args: prior_mean: Mean of the prior distribution Returns: A sample from the prior distribution. """ pass
[docs] def discretize( self, *, state: torch.Tensor, time: torch.Tensor, state_length: Optional[torch.Tensor] = None, **kwargs ) -> Tuple[torch.Tensor, torch.Tensor]: """Assume we have the following SDE: dx = drift(x, t) * dt + diffusion(x, t) * dwt where `wt` is the standard Wiener process. We assume the following discretization: new_state = current_state + total_drift + total_diffusion * z_norm where `z_norm` is sampled from normal distribution with zero mean and unit variance. Args: state: current state of the process, shape (B, C, D, T) time: current time of the process, shape (B,) state_length: length of the valid time steps for each example in the batch, shape (B,) **kwargs: other parameters Returns: Drift and diffusion. """ # Get coefficients drift_coefficient, diffusion_coefficient = self.coefficients( state=state, time=time, state_length=state_length, **kwargs ) # Discretized drift drift = drift_coefficient * self.dt # Note: # Scale with sqrt(dt) because z_norm is sampled from a normal distribution with zero mean and # unit variance and dwt is normally distributed with zero mean and variance dt diffusion = diffusion_coefficient * np.sqrt(self.dt) return drift, diffusion
[docs] @abstractmethod def copy(self): """Create a copy of this SDE.""" pass
def __repr__(self): desc = f'{self.__class__.__name__}(time_min={self.time_min}, time_max={self.time_max}, num_steps={self.num_steps})' desc += f'\n\tdt: {self.dt}' desc += f'\n\ttime_delta: {self.time_delta}' return desc
[docs] class OrnsteinUhlenbeckVarianceExplodingSDE(StochasticDifferentialEquation): """This class implements the Ornstein-Uhlenbeck SDE with variance exploding noise schedule. The SDE is given by: dx = theta * (y - x) dt + g(t) dw where `theta` is the stiffness parameter and `g(t)` is the diffusion coefficient: g(t) = std_min * (std_max/std_min)^t * sqrt(2 * log(std_max/std_min)) References: Richter et al., Speech Enhancement and Dereverberation with Diffusion-based Generative Models, Tr. ASLP 2023 """ def __init__( self, stiffness: float, std_min: float, std_max: float, num_steps: int = 100, time_min: float = 3e-2, time_max: float = 1.0, eps: float = 1e-8, ): super().__init__(time_min=time_min, time_max=time_max, num_steps=num_steps) # Small regularization if eps <= 0: raise ValueError(f'eps should be positive, current value {eps}') self.eps = eps # stifness self.stiffness = stiffness # noise schedule if std_min <= 0: raise ValueError(f'std_min should be positive, current value {std_min}') if std_max <= std_min: raise ValueError(f'std_max should be larger than std_min, current max {std_max} and min {std_min}') self.std_min = std_min self.std_max = std_max logging.debug('Initialized %s with', self.__class__.__name__) logging.debug('\tstiffness: %s', self.stiffness) logging.debug('\tstd_min: %s', self.std_min) logging.debug('\tstd_max: %s', self.std_max) logging.debug('\tnum_steps: %s', self.num_steps) logging.debug('\ttime_min: %s', self.time_min) logging.debug('\ttime_max: %s', self.time_max) logging.debug('\teps: %s', self.eps) @property def std_ratio(self) -> float: return self.std_max / (self.std_min + self.eps) @property def log_std_ratio(self) -> float: return np.log(self.std_ratio + self.eps)
[docs] @typecheck( input_types={ "state": NeuralType(('B', 'C', 'D', 'T'), VoidType()), "prior_mean": NeuralType(('B', 'C', 'D', 'T'), VoidType()), "time": NeuralType(tuple('B'), FloatType()), }, output_types={ "mean": NeuralType(('B', 'C', 'D', 'T'), FloatType()), }, ) def perturb_kernel_mean(self, state: torch.Tensor, prior_mean: torch.Tensor, time: torch.Tensor) -> torch.Tensor: """Return the mean of the perturbation kernel for this SDE. Args: state: current state of the process, shape (B, C, D, T) prior_mean: mean of the prior distribution time: current time of the process, shape (B,) Returns: A tensor of shape (B, C, D, T) """ # exponential weighting weight = torch.exp(-self.stiffness * time) # view as [B, C, D, T] weight = weight.view(-1, 1, 1, 1) # closed-form mean mean = weight * state + (1 - weight) * prior_mean return mean
[docs] @typecheck( input_types={ "time": NeuralType(tuple('B'), FloatType()), }, output_types={ "std": NeuralType(tuple('B'), FloatType()), }, ) def perturb_kernel_std(self, time: torch.Tensor) -> torch.Tensor: """Return the standard deviation of the perturbation kernel for this SDE. Note that the standard deviation depends on the time and the noise schedule, which is parametrized using `self.stiffness`, `self.std_min` and `self.std_max`. Args: time: current time of the process, shape (B,) Returns: A tensor of shape (B,) """ var = (self.std_min**2) * self.log_std_ratio var *= torch.pow(self.std_ratio, 2 * time) - torch.exp(-2 * self.stiffness * time) var /= self.stiffness + self.log_std_ratio std = torch.sqrt(var) return std
[docs] @typecheck( input_types={ "state": NeuralType(('B', 'C', 'D', 'T'), VoidType()), "prior_mean": NeuralType(('B', 'C', 'D', 'T'), VoidType()), "time": NeuralType(tuple('B'), FloatType()), }, output_types={ "mean": NeuralType(('B', 'C', 'D', 'T'), FloatType()), "std": NeuralType(('B', 'C', 'D', 'T'), FloatType()), }, ) def perturb_kernel_params(self, state: torch.Tensor, prior_mean: torch.Tensor, time: torch.Tensor) -> torch.Tensor: """Return the mean and standard deviation of the perturbation kernel for this SDE. Args: state: current state of the process, shape (B, C, D, T) prior_mean: mean of the prior distribution time: current time of the process, shape (B,) """ assert torch.all(time <= self.time_max) assert torch.all(time >= self.time_min) # compute the mean mean = self.perturb_kernel_mean(state=state, prior_mean=prior_mean, time=time) # compute the standard deviation std = self.perturb_kernel_std(time=time) # view as [B, C, D, T] std = std.view(-1, 1, 1, 1) return mean, std
[docs] @typecheck( input_types={ "state": NeuralType(('B', 'C', 'D', 'T'), VoidType()), "time": NeuralType(tuple('B'), VoidType()), "prior_mean": NeuralType(('B', 'C', 'D', 'T'), VoidType()), "state_length": NeuralType(tuple('B'), LengthsType(), optional=True), }, output_types={ "drift_coefficient": NeuralType(('B', 'C', 'D', 'T'), FloatType()), "diffusion_coefficient": NeuralType(('B', 'C', 'D', 'T'), FloatType()), }, ) def coefficients( self, state: torch.Tensor, time: torch.Tensor, prior_mean: torch.Tensor, state_length: Optional[torch.Tensor] = None, ) -> Tuple[torch.Tensor, torch.Tensor]: """Compute drift and diffusion coefficients for this SDE. Args: state: current state of the process, shape (B, C, D, T) time: current time of the process, shape (B,) prior_mean: mean of the prior distribution state_length: length of the valid time steps for each example in the batch Returns: Drift and diffusion coefficients. """ # Drift coefficient drift_coefficient = self.stiffness * (prior_mean - state) # Diffusion coefficient diffusion_coefficient = self.std_min * torch.pow(self.std_ratio, time) * np.sqrt(2 * self.log_std_ratio) # View in the same shape as the state diffusion_coefficient = diffusion_coefficient.view(-1, *([1] * (state.dim() - 1))) if state_length is not None: drift_coefficient = mask_sequence_tensor(drift_coefficient, state_length) diffusion_coefficient = mask_sequence_tensor(diffusion_coefficient, state_length) return drift_coefficient, diffusion_coefficient
[docs] def prior_sampling(self, prior_mean: torch.Tensor) -> torch.Tensor: """Generate a sample from the prior distribution p_T. Args: prior_mean: Mean of the prior distribution """ # Final time step for all samples in the batch time = self.time_max * torch.ones(prior_mean.shape[0], device=prior_mean.device) # Compute the std of the prior distribution std = self.perturb_kernel_std(time=time) # view as [B, C, D, T] std = std.view(-1, 1, 1, 1) # Generate a sample from a normal distribution centered at prior_mean sample = prior_mean + torch.randn_like(prior_mean) * std return sample
[docs] def copy(self): return OrnsteinUhlenbeckVarianceExplodingSDE( stiffness=self.stiffness, std_min=self.std_min, std_max=self.std_max, num_steps=self.num_steps, time_min=self.time_min, time_max=self.time_max, eps=self.eps, )
def __repr__(self): desc = f'{self.__class__.__name__}(stiffness={self.stiffness}, std_min={self.std_min}, std_max={self.std_max}, num_steps={self.num_steps}, time_min={self.time_min}, time_max={self.time_max}, eps={self.eps})' desc += f'\n\tdt: {self.dt}' desc += f'\n\ttime_delta: {self.time_delta}' desc += f'\n\tstd_ratio: {self.std_ratio}' desc += f'\n\tlog_std_ratio: {self.log_std_ratio}' return desc
[docs] class ReverseStochasticDifferentialEquation(StochasticDifferentialEquation): def __init__(self, *, sde: Type[StochasticDifferentialEquation], score_estimator: Type[NeuralModule]): """Use the forward SDE and a score estimator to define the reverse SDE. Args: sde: forward SDE score_estimator: neural score estimator """ super().__init__(time_min=sde.time_min, time_max=sde.time_max, num_steps=sde.num_steps) self.score_estimator = score_estimator self.forward_sde = sde logging.debug('Initialized %s', self.__class__.__name__)
[docs] def coefficients( self, state: torch.Tensor, time: torch.Tensor, score_condition: Optional[torch.Tensor] = None, state_length: Optional[torch.Tensor] = None, **kwargs, ) -> Tuple[torch.Tensor, torch.Tensor]: """Compute drift and diffusion coefficients for the reverse SDE. Args: state: current state of the process, shape (B, C, D, T) time: current time of the process, shape (B,) """ raise NotImplementedError('Coefficients not necessary for the reverse SDE.')
[docs] def prior_sampling(self, shape: torch.Size, device: torch.device) -> torch.Tensor: """Prior sampling is not necessary for the reverse SDE.""" raise NotImplementedError('Prior sampling not necessary for the reverse SDE.')
[docs] def discretize( self, *, state: torch.Tensor, time: torch.Tensor, score_condition: Optional[torch.Tensor] = None, state_length: Optional[torch.Tensor] = None, **kwargs, ) -> Tuple[torch.Tensor, torch.Tensor]: """Discretize the reverse SDE. Args: state: current state of the process, shape (B, C, D, T) time: current time of the process, shape (B,) score_condition: condition for the score estimator state_length: length of the valid time steps for each example in the batch **kwargs: other parameters for discretization of the forward SDE """ # Drift and diffusion from the forward SDE forward_drift, forward_diffusion = self.forward_sde.discretize(state=state, time=time, **kwargs) # For input for the score estimator: # - if no condition is provided, use the state # - if a condition is provided, concatenate the state and the condition along the channel dimension score_input = state if score_condition is None else torch.cat([state, score_condition], dim=1) # Estimate score score, _ = self.score_estimator(input=score_input, input_length=state_length, condition=time) # Adjust drift drift = forward_drift - forward_diffusion.pow(2) * score # Adjust diffusion diffusion = forward_diffusion if state_length is not None: drift = mask_sequence_tensor(drift, state_length) diffusion = mask_sequence_tensor(diffusion, state_length) return drift, diffusion
[docs] def copy(self): return ReverseStochasticDifferentialEquation(sde=self.forward_sde.copy(), score_estimator=self.score_estimator)
def __repr__(self): desc = f'{self.__class__.__name__}(sde={self.forward_sde}, score_estimator={self.score_estimator})' return desc
[docs] class PredictorCorrectorSampler(NeuralModule): """Predictor-Corrector sampler for the reverse SDE. Args: sde: forward SDE score_estimator: neural score estimator predictor: predictor for the reverse process corrector: corrector for the reverse process num_steps: number of time steps for the reverse process num_corrector_steps: number of corrector steps time_max: maximum time time_min: minimum time snr: SNR for Annealed Langevin Dynamics output_type: type of the output ('state' for the final state, or 'mean' for the mean of the final state) References: - Song et al., Score-based generative modeling through stochastic differential equations, 2021 """ def __init__( self, sde, score_estimator, predictor: str = 'reverse_diffusion', corrector: str = 'annealed_langevin_dynamics', num_steps: int = 50, num_corrector_steps: int = 1, time_max: Optional[float] = None, time_min: Optional[float] = None, snr: float = 0.5, output_type: str = 'mean', ): super().__init__() # Create a copy of SDE self.sde = sde.copy() # Update SDE parameters for sampling if time_max is not None: self.sde.time_max = time_max logging.info('sde.time_max set to: %s', self.sde.time_max) if time_min is not None: self.sde.time_min = time_min logging.info('sde.time_min set to: %s', self.sde.time_min) self.sde.num_steps = num_steps logging.info('sde.num_steps set to: %s', self.sde.num_steps) # Update local values self.time_max = self.sde.time_max self.time_min = self.sde.time_min self.num_steps = self.sde.num_steps # Predictor setup if predictor == 'reverse_diffusion': self.predictor = ReverseDiffusionPredictor(sde=self.sde, score_estimator=score_estimator) else: raise RuntimeError(f'Unexpected predictor: {predictor}') # Corrector setup if corrector == 'annealed_langevin_dynamics': self.corrector = AnnealedLangevinDynamics( sde=self.sde, score_estimator=score_estimator, snr=snr, num_steps=num_corrector_steps ) else: raise RuntimeError(f'Unexpected corrector: {corrector}') if output_type not in ['mean', 'state']: raise ValueError(f'Unexpected output type: {output_type}') self.output_type = output_type logging.debug('Initialized %s with', self.__class__.__name__) logging.debug('\tpredictor: %s', predictor) logging.debug('\tcorrector: %s', corrector) logging.debug('\tnum_steps: %s', self.num_steps) logging.debug('\ttime_min: %s', self.time_min) logging.debug('\ttime_max: %s', self.time_max) logging.debug('\tnum_corrector_steps: %s', num_corrector_steps) logging.debug('\tsnr: %s', snr) logging.debug('\toutput_type: %s', self.output_type)
[docs] @typecheck( input_types={ "prior_mean": NeuralType(('B', 'C', 'D', 'T'), SpectrogramType()), "score_condition": NeuralType(('B', 'C', 'D', 'T'), SpectrogramType(), optional=True), "state_length": NeuralType(tuple('B'), LengthsType(), optional=True), }, output_types={ "sample": NeuralType(('B', 'C', 'D', 'T'), SpectrogramType()), "state_length": NeuralType(tuple('B'), LengthsType(), optional=True), }, ) @torch.inference_mode() def forward( self, prior_mean: torch.Tensor, score_condition: torch.Tensor, state_length: Optional[torch.Tensor] = None ) -> torch.Tensor: """Takes prior (noisy) mean and generates a sample by solving the reverse SDE. Args: prior_mean: mean for the prior distribution, e.g., noisy observation score_condition: conditioning for the score estimator state_length: length of the valid time steps for each example in the batch Returns: Generated `sample` and the corresponding `sample_length`. """ # Sample from the prior distribution state = self.sde.prior_sampling(prior_mean=prior_mean) if state_length is not None: state = mask_sequence_tensor(state, state_length) # Time steps for evaluation time_steps = torch.linspace(self.time_max, self.time_min, self.num_steps, device=state.device) # Sampling for t in time_steps: # time steps for the whole batch time = t * torch.ones(state.shape[0], device=state.device) # corrector step state, _ = self.corrector( state=state, time=time, score_condition=score_condition, state_length=state_length ) # predictor step state, state_mean = self.predictor( state=state, time=time, score_condition=score_condition, prior_mean=prior_mean, state_length=state_length, ) # Final output if self.output_type == 'state': sample = state elif self.output_type == 'mean': sample = state_mean else: raise RuntimeError(f'Unexpected output type: {self.output_type}') if state_length is not None: sample = mask_sequence_tensor(sample, state_length) return sample, state_length
[docs] class Predictor(torch.nn.Module, ABC): """Predictor for the reverse process. Args: sde: forward SDE score_estimator: neural score estimator """ def __init__(self, sde, score_estimator): super().__init__() self.reverse_sde = ReverseStochasticDifferentialEquation(sde=sde, score_estimator=score_estimator)
[docs] @abstractmethod @torch.inference_mode() def forward( self, *, state: torch.Tensor, time: torch.Tensor, score_condition: Optional[torch.Tensor] = None, state_length: Optional[torch.Tensor] = None, **kwargs, ): """Predict the next state of the reverse process. Args: state: current state of the process, shape (B, C, D, T) time: current time of the process, shape (B,) score_condition: conditioning for the score estimator state_length: length of the valid time steps for each example in the batch Returns: New state and mean. """ pass
[docs] class ReverseDiffusionPredictor(Predictor): """Predict the next state of the reverse process using the reverse diffusion process. Args: sde: forward SDE score_estimator: neural score estimator """ def __init__(self, sde, score_estimator): super().__init__(sde=sde, score_estimator=score_estimator)
[docs] @torch.inference_mode() def forward(self, *, state, time, score_condition=None, state_length=None, **kwargs): """Predict the next state of the reverse process using the reverse diffusion process. Args: state: current state of the process, shape (B, C, D, T) time: current time of the process, shape (B,) score_condition: conditioning for the score estimator state_length: length of the valid time steps for each example in the batch Returns: New state and mean of the diffusion process. """ drift, diffusion = self.reverse_sde.discretize( state=state, time=time, score_condition=score_condition, state_length=state_length, **kwargs ) # Generate a random sample from a standard normal distribution z_norm = torch.randn_like(state) # Compute the mean of the next state mean = state - drift # Compute new state by sampling new_state = mean + diffusion * z_norm if state_length is not None: new_state = mask_sequence_tensor(new_state, state_length) mean = mask_sequence_tensor(mean, state_length) return new_state, mean
[docs] class Corrector(NeuralModule, ABC): """Corrector for the reverse process. Args: sde: forward SDE score_estimator: neural score estimator snr: SNR for Annealed Langevin Dynamics num_steps: number of steps for the corrector """ def __init__( self, sde: Type[StochasticDifferentialEquation], score_estimator: Type[NeuralModule], snr: float, num_steps: int, ): super().__init__() self.sde = sde self.score_estimator = score_estimator self.snr = snr self.num_steps = num_steps logging.debug('Initialized %s with', self.__class__.__name__) logging.debug('\tsnr: %s', snr) logging.debug('\tnum_steps: %s', num_steps)
[docs] @abstractmethod @typecheck( input_types={ "state": NeuralType(('B', 'C', 'D', 'T'), VoidType()), "time": NeuralType(tuple('B'), FloatType()), "score_condition": NeuralType(('B', 'C', 'D', 'T'), VoidType(), optional=True), "state_length": NeuralType(tuple('B'), LengthsType(), optional=True), }, output_types={ "state": NeuralType(('B', 'C', 'D', 'T'), VoidType()), }, ) @torch.inference_mode() def forward(self, state, time, score_condition=None, state_length=None): """ Args: state: current state of the process, shape (B, C, D, T) time: current time of the process, shape (B,) score_condition: conditioning for the score estimator state_length: length of the valid time steps for each example in the batch Returns: New state and mean. """ pass
[docs] class AnnealedLangevinDynamics(Corrector): """Annealed Langevin Dynamics for the reverse process. References: - Song et al., Score-based generative modeling through stochastic differential equations, 2021 """ def __init__(self, sde, **kwargs): if not isinstance(sde, OrnsteinUhlenbeckVarianceExplodingSDE): raise ValueError(f'Expected an instance of OrnsteinUhlenbeckVarianceExplodingSDE, got {type(sde)}') super().__init__(sde=sde, **kwargs)
[docs] @torch.inference_mode() def forward(self, state, time, score_condition=None, state_length=None): """Correct the state using Annealed Langevin Dynamics. Args: state: current state of the process, shape (B, C, D, T) time: current time of the process, shape (B,) score_condition: conditioning for the score estimator state_length: length of the valid time steps for each example in the batch Returns: New state and mean of the diffusion process. References: Alg. 4 in http://arxiv.org/abs/2011.13456 """ # Compute the standard deviation of the diffusion process std = self.sde.perturb_kernel_std(time=time) # View as [B, 1, 1, 1] std = std.view(-1, *([1] * (state.dim() - 1))) for i in range(self.num_steps): # prepare input for the score estimator, concatenate conditioning along the channel dimension score_input = state if score_condition is None else torch.cat([state, score_condition], dim=1) # calculate the score score, _ = self.score_estimator(input=score_input, input_length=state_length, condition=time) # generate a sample from a standard normal distribution z_norm = torch.randn_like(state) # compute the step size # note: this is slightly different than in the paper, where std = ||z_norm||_2 / ||score||_2 step_size = 2 * (self.snr * std).pow(2) # update the mean mean = state + step_size * score # update the state state = mean + z_norm * torch.sqrt(step_size * 2) if state_length is not None: state = mask_sequence_tensor(state, state_length) mean = mask_sequence_tensor(mean, state_length) return state, mean