# Copyright 2026 The AnyFlow Team, NVIDIA Corp., and The HuggingFace Team. 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 dataclasses import dataclass from typing import List, Optional, Tuple, Union import torch from ..configuration_utils import ConfigMixin, register_to_config from ..utils import BaseOutput, logging from .scheduling_utils import SchedulerMixin logger = logging.get_logger(__name__) # pylint: disable=invalid-name @dataclass class FlowMapEulerDiscreteSchedulerOutput(BaseOutput): """ Output class for the scheduler's `step` function output. Args: prev_sample (`torch.Tensor`): Computed sample :math:`z_r` at the target flow-map timestep `r_timestep`. Should be used as the next denoising input. """ prev_sample: torch.Tensor class FlowMapEulerDiscreteScheduler(SchedulerMixin, ConfigMixin): """ Euler-style sampler for flow-map-distilled diffusion models. Flow-map models learn arbitrary-interval transitions :math:`z_t \\to z_r` rather than the fixed :math:`z_t \\to z_0` mapping of consistency models, so a single distilled checkpoint can be evaluated at 1, 2, 4, 8, ... NFE without retraining. The `step` method advances the sample from `timestep` to `r_timestep` along the predicted velocity. Introduced in [AnyFlow: Any-Step Video Diffusion Model with On-Policy Flow Map Distillation](https://huggingface.co/papers/2605.13724) by Yuchao Gu, Guian Fang et al. This scheduler inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic methods implemented for all schedulers (loading, saving, etc.). Args: num_train_timesteps (`int`, defaults to 1000): The number of diffusion steps used to train the underlying flow-map model. shift (`float`, defaults to 1.0): Multiplicative timestep shift applied to the inference schedule. ``shift=1.0`` is the identity; values greater than 1.0 push the schedule toward more denoising at later steps (e.g., ``shift=5`` matches the Wan2.1 default). """ _compatibles = [] order = 1 @register_to_config def __init__( self, num_train_timesteps: int = 1000, shift: float = 1.0, ): # `_step_index` and `_begin_index` mirror `FlowMatchEulerDiscreteScheduler`'s state machine: # `_step_index` advances on every `step()` so callbacks and composable schedulers can read it; # `_begin_index` is honoured on the very first `step()` after `set_timesteps` to support # mid-schedule restarts (e.g. image-to-image style use). self._step_index: Optional[int] = None self._begin_index: Optional[int] = None self.set_timesteps(num_train_timesteps, device="cpu") @property def step_index(self) -> Optional[int]: """The index counter for current timestep. Returns ``None`` before the first :meth:`step` call after :meth:`set_timesteps`.""" return self._step_index @property def begin_index(self) -> Optional[int]: """The index for the first timestep — set by :meth:`set_begin_index`. Defaults to ``None``.""" return self._begin_index def set_begin_index(self, begin_index: int = 0): """Set the begin index for the scheduler. Pipelines that start mid-schedule (e.g. image-to-image) call this between :meth:`set_timesteps` and the first :meth:`step` to anchor the rollout.""" self._begin_index = begin_index def scale_model_input(self, sample: torch.Tensor, *args, **kwargs) -> torch.Tensor: """No-op identity scaling. Provided for API compatibility with other Diffusers schedulers.""" return sample def scale_noise( self, sample: torch.FloatTensor, timestep: Union[float, torch.FloatTensor], noise: Optional[torch.FloatTensor] = None, ) -> torch.FloatTensor: """Linearly interpolate ``sample`` toward ``noise`` according to the normalized ``timestep``.""" timestep = timestep.to(device=sample.device, dtype=sample.dtype) timestep = timestep / self.config.num_train_timesteps timestep = timestep.view(*timestep.shape, *([1] * (noise.ndim - timestep.ndim))) sample = timestep * noise + (1.0 - timestep) * sample return sample def apply_shift(self, sigmas: torch.Tensor) -> torch.Tensor: """Apply the configured shift transformation to a sigma tensor.""" if self.config.shift == 1.0: return sigmas return self.config.shift * sigmas / (1 + (self.config.shift - 1) * sigmas) def set_timesteps( self, num_inference_steps: Optional[int] = None, device: Union[str, torch.device] = None, sigmas: Optional[List[float]] = None, timesteps: Optional[List[float]] = None, ) -> None: """Build the inference timestep schedule. Internally tracks ``self.sigmas`` of length ``num_inference_steps + 1`` (the configured shift applied to a linspace from ``1.0`` to ``0.0`` by default); ``self.timesteps`` exposes the first ``num_inference_steps`` sigmas scaled by ``num_train_timesteps`` — i.e. one timestep per inference step, matching :class:`~diffusers.schedulers.FlowMatchEulerDiscreteScheduler`. The final sigma (``0``) is the implicit r-endpoint of the last step and is appended automatically when ``sigmas`` / ``timesteps`` are user-provided. Args: num_inference_steps (`int`, *optional*): Number of inference steps. If ``None``, must pass ``sigmas`` or ``timesteps``. device (`str` or `torch.device`, *optional*): Target device for ``self.sigmas`` / ``self.timesteps``. sigmas (`List[float]`, *optional*): Custom sigma schedule of length ``num_inference_steps``. The terminal ``0`` sigma is appended automatically. The configured ``shift`` is applied on top. timesteps (`List[float]`, *optional*): Custom timestep schedule of length ``num_inference_steps``, in the same units as ``self.timesteps`` (i.e. scaled by ``num_train_timesteps``). Converted to sigmas internally. If both ``sigmas`` and ``timesteps`` are passed, their lengths must match. """ if sigmas is not None and timesteps is not None and len(sigmas) != len(timesteps): raise ValueError("`sigmas` and `timesteps` should have the same length") if num_inference_steps is not None: if (sigmas is not None and len(sigmas) != num_inference_steps) or ( timesteps is not None and len(timesteps) != num_inference_steps ): raise ValueError( "`sigmas` and `timesteps` should have the same length as `num_inference_steps` when both are provided" ) elif sigmas is not None: num_inference_steps = len(sigmas) elif timesteps is not None: num_inference_steps = len(timesteps) else: raise ValueError("`num_inference_steps` must be provided when both `sigmas` and `timesteps` are `None`") # MPS / NPU don't support float64 — build the schedule in float64 on CPU and only move # the final tensors to the requested device (with a float32 downcast for MPS / NPU). device_obj = torch.device(device) if device is not None and not isinstance(device, torch.device) else device is_mps = device_obj is not None and device_obj.type == "mps" is_npu = device_obj is not None and device_obj.type == "npu" out_dtype = torch.float32 if (is_mps or is_npu) else torch.float64 # Build the working sigma sequence (length N) before appending the terminal 0. if sigmas is not None: working_sigmas = torch.tensor(sigmas, dtype=torch.float64) elif timesteps is not None: working_sigmas = torch.tensor(timesteps, dtype=torch.float64) / self.config.num_train_timesteps else: working_sigmas = torch.linspace(1.0, 0.0, num_inference_steps + 1, dtype=torch.float64)[:-1] working_sigmas = self.apply_shift(working_sigmas) # Append the terminal 0 sigma as the r-endpoint of the last step. `new_zeros(1)` inherits both # device and dtype from `working_sigmas` so `torch.cat` stays device-consistent on CUDA. full_sigmas = torch.cat([working_sigmas, working_sigmas.new_zeros(1)]) self.num_inference_steps = num_inference_steps self.sigmas = full_sigmas.to(device=device, dtype=out_dtype) self.timesteps = (self.sigmas[:-1] * self.config.num_train_timesteps).to(device=device, dtype=out_dtype) # Reset the state machine — first `step()` after this will re-initialize `_step_index`. self._step_index = None self._begin_index = None def _init_step_index(self, timestep: Union[float, torch.FloatTensor]) -> None: """Initialize ``self._step_index`` on the first :meth:`step` call after :meth:`set_timesteps`. Off-schedule timesteps are allowed (any-step sampling is documented in :meth:`step`); in that case the counter starts at 0 so it can still be used as an observable rollout marker. """ if self._begin_index is not None: self._step_index = self._begin_index return idx = self.index_for_timestep(timestep) self._step_index = idx if idx is not None else 0 def index_for_timestep(self, timestep: Union[float, torch.FloatTensor]) -> Optional[int]: """Return the index of ``timestep`` on the current schedule, or ``None`` if off-schedule. Lookup is done against ``self.timesteps`` with a small fp tolerance. Used to recover the corresponding sigma without assuming the linear ``timesteps = sigmas * num_train_timesteps`` relationship — that way a custom schedule (e.g. non-linear shift, manually-set timesteps) still resolves correctly. """ if self.timesteps is None: return None t_value = float(timestep.flatten()[0].item()) if torch.is_tensor(timestep) else float(timestep) diffs = (self.timesteps.float() - t_value).abs() idx = int(diffs.argmin().item()) if diffs[idx].item() > 1e-3: return None return idx def step( self, model_output: torch.FloatTensor, timestep: Union[float, torch.FloatTensor], sample: torch.FloatTensor, r_timestep: Optional[Union[float, torch.FloatTensor]] = None, return_dict: bool = True, ) -> Union[FlowMapEulerDiscreteSchedulerOutput, Tuple[torch.Tensor]]: """ Advance ``sample`` from ``timestep`` to ``r_timestep`` using the model-predicted velocity. Unlike a standard Euler scheduler, both endpoints of the interval can be caller-provided so that any-step sampling is possible: a single model call can step from `t` to any chosen target `r` (including `r=0` for a one-shot generation). When ``r_timestep`` is omitted, it defaults to the next timestep on the schedule (matching ``FlowMatchEulerDiscreteScheduler`` semantics). Internally the source and target sigmas are recovered by indexing ``self.sigmas`` via :meth:`index_for_timestep` rather than by dividing the input timesteps by ``num_train_timesteps``, so any schedule whose timestep / sigma relationship is non-linear (for example a custom shift) stays correct. For an off-schedule ``r_timestep``, the scheduler falls back to ``r_timestep / num_train_timesteps`` so any-step sampling outside the schedule remains supported. Args: model_output (`torch.Tensor`): Direct output from the flow-map model (predicted mean velocity). timestep (`float` or `torch.Tensor`): Source timestep ``t`` in the same units as ``self.timesteps``. sample (`torch.Tensor`): Current sample :math:`z_t`. r_timestep (`float` or `torch.Tensor`, *optional*): Target timestep ``r``. Defaults to the next timestep on the schedule when ``None``; pass an explicit value for any-step sampling. ``r_timestep == timestep`` is a no-op. return_dict (`bool`, defaults to `True`): Whether to return a [`FlowMapEulerDiscreteSchedulerOutput`] (the default) or a plain tuple. Returns: [`FlowMapEulerDiscreteSchedulerOutput`] or `tuple`: When ``return_dict=True``, returns a [`FlowMapEulerDiscreteSchedulerOutput`] whose ``prev_sample`` is :math:`z_r`. Otherwise returns a 1-tuple ``(prev_sample,)``. """ if self.sigmas is None or self.timesteps is None: raise ValueError("`set_timesteps` has not been called.") # `_step_index` is maintained purely as observable state for callbacks / composable schedulers. # Sigma resolution stays a pure function of the passed-in (`timestep`, `r_timestep`) so the call is # idempotent — calling `step` twice with the same arguments always returns the same `prev_sample`. if self._step_index is None: self._init_step_index(timestep) # Resolve source sigma via index lookup; fall back to / num_train_timesteps only if `timestep` is off-schedule. t_idx = self.index_for_timestep(timestep) if t_idx is not None: sigma_t = self.sigmas[t_idx].to(device=sample.device, dtype=self.sigmas.dtype) else: t_value = timestep.to(self.sigmas.dtype) if torch.is_tensor(timestep) else torch.tensor(timestep) sigma_t = (t_value / self.config.num_train_timesteps).to(device=sample.device, dtype=self.sigmas.dtype) # Resolve target sigma. None defaults to sigmas[t_idx + 1] when on-schedule; otherwise the caller's # explicit `r_timestep` is used (sigma lookup first, fall back to scaling for off-schedule any-step). if r_timestep is None: if t_idx is None: raise ValueError( "`r_timestep` is None but `timestep` is not on the current schedule, so `r` cannot be inferred. " "Please pass an explicit `r_timestep` for any-step sampling outside the schedule." ) sigma_r = self.sigmas[t_idx + 1].to(device=sample.device, dtype=self.sigmas.dtype) else: r_idx = self.index_for_timestep(r_timestep) if r_idx is not None: sigma_r = self.sigmas[r_idx].to(device=sample.device, dtype=self.sigmas.dtype) else: r_value = r_timestep.to(self.sigmas.dtype) if torch.is_tensor(r_timestep) else torch.tensor(r_timestep) sigma_r = (r_value / self.config.num_train_timesteps).to(device=sample.device, dtype=self.sigmas.dtype) sigma_t = sigma_t.view(*sigma_t.shape, *([1] * (model_output.ndim - sigma_t.ndim))) sigma_r = sigma_r.view(*sigma_r.shape, *([1] * (model_output.ndim - sigma_r.ndim))) prev_sample = sample - (sigma_t - sigma_r) * model_output prev_sample = prev_sample.to(model_output.dtype) # Advance state machine so downstream callbacks / composable schedulers observe correct `step_index`. self._step_index += 1 if not return_dict: return (prev_sample,) return FlowMapEulerDiscreteSchedulerOutput(prev_sample=prev_sample)