Source code for flashdreams.infra.diffusion.scheduler.base
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
"""Diffusion scheduler interface."""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Protocol
import torch
import torch.nn as nn
from torch import Tensor
from flashdreams.infra.config import InstantiateConfig
[docs]
class FlowPredictor(Protocol):
"""Closure ``(noisy_latent, timestep) -> predicted_flow``.
Built by ``DiffusionModel.generate`` by binding the per-AR-step ``cache``
/ ``input`` to the transformer's ``predict_flow``. A scheduler invokes
it once per denoising iteration. The scheduler decides the timestep
dtype (UniPC uses int64, flow-match uses float).
"""
def __call__(self, noisy_latent: Tensor, timestep: Tensor) -> Tensor:
"""Predict the flow at ``timestep``."""
...
[docs]
class Scheduler(nn.Module, ABC):
"""Denoising scheduler.
Owns the entire denoising loop. Callers see only ``noise → clean``;
the loop shape (renoise / multistep / plain ODE) is private.
Concrete configs inherit ``SchedulerConfig`` and declare their own
``num_inference_steps`` / ``shift`` fields (the base holds no
shared dataclass fields).
Examples:
scheduler = config.setup()
clean = scheduler.sample(initial_noise=noise, predict_flow=predictor)
noisy = scheduler.add_noise(clean_input=clean, timestep=t)
"""
def __init__(self, config: SchedulerConfig) -> None:
super().__init__()
self.config = config
[docs]
@abstractmethod
def sample(
self,
initial_noise: Tensor,
predict_flow: FlowPredictor,
rng: torch.Generator | None = None,
) -> Tensor:
"""Run the full denoising loop and return the clean latent.
Schedulers are shape-agnostic: every internal op broadcasts against
per-step scalar sigmas. In practice ``initial_noise`` is a video
latent ``[B, C, T, H, W]``, conventionally treated as a sample at
``sigma=1``.
Args:
initial_noise: Gaussian noise on the caller's device/dtype.
predict_flow: Per-step closure invoked
``num_inference_steps`` times.
rng: Generator on the same device. Used by self-forcing renoise
loops; pure ODE solvers ignore it.
Returns:
Clean latent with the same shape, device, and dtype as
``initial_noise``.
"""
[docs]
@abstractmethod
def add_noise(
self,
clean_input: Tensor,
timestep: Tensor,
rng: torch.Generator | None = None,
) -> Tensor:
"""Apply the forward corruption ``x_t = (1 - sigma(t)) * x_0 + sigma(t) * eps``.
Timestep value semantics are scheduler-specific: all schedulers snap
to the nearest entry of their inference schedule.
"""
@dataclass(kw_only=True)
class SchedulerConfig(InstantiateConfig):
"""Category base for every denoising-scheduler config."""
_target: type["Scheduler"] = field(default_factory=lambda: Scheduler)