Source code for physicsnemo.diffusion.multi_diffusion.predictor

# SPDX-FileCopyrightText: Copyright (c) 2023 - 2026 NVIDIA CORPORATION & AFFILIATES.
# SPDX-FileCopyrightText: 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.

"""Multi-diffusion predictor wrapper for patch-based diffusion sampling."""

import warnings
from typing import Any, Callable, Iterator, cast

import torch
from jaxtyping import Float
from tensordict import TensorDict
from torch import Tensor
from torch.utils.checkpoint import checkpoint

from physicsnemo.diffusion.base import Predictor, PredictorType
from physicsnemo.diffusion.multi_diffusion.models import MultiDiffusionModel2D
from physicsnemo.diffusion.multi_diffusion.patching import GridPatching2D
from physicsnemo.diffusion.utils.utils import _unwrap_module


[docs] class MultiDiffusionPredictor(Predictor): r"""Predictor for sampling from a trained :class:`~physicsnemo.diffusion.multi_diffusion.MultiDiffusionModel2D`. Satisfies the :class:`~physicsnemo.diffusion.Predictor` protocol, so it plugs into any sampling utility that accepts a ``Predictor`` (:func:`~physicsnemo.diffusion.samplers.sample`, :meth:`~physicsnemo.diffusion.noise_schedulers.NoiseScheduler.get_denoiser`, and all standard solvers) with no other changes. All patch-based logic (patching the state, running the per-patch predictions, fusing them back to the global domain) is handled internally. The patching strategy must be configured by calling :meth:`set_patching` before any other method. ``patch_shape`` and ``global_shape`` default to the values saved on the wrapped model (typically restored from the training-time checkpoint), while ``overlap_pix`` and ``boundary_pix`` must be provided explicitly because they cannot be inferred from a model trained with random patching. On very large global domains the full :math:`P \times B` activation tensor may not fit in GPU memory, leading to OOM errors. Two independent strategies mitigate this: - ``chunk_size`` processes the :math:`P \times B` patches in consecutive chunks of at most ``chunk_size`` rows. Useful for both plain inference and gradient-based use cases. Trades batch parallelism for memory. - ``use_checkpointing`` recomputes activations on demand during backpropagation instead of storing them. Only meaningful when gradients flow through the predictor, the typical use case being DPS guidance (see :class:`~physicsnemo.diffusion.multi_diffusion.MultiDiffusionDPSScorePredictor`). Trades compute for memory. The two options can be combined; for use cases that need explicit control over chunk-level processing, the streaming iterator :meth:`chunks` exposes the per-chunk model outputs directly. .. warning:: :class:`MultiDiffusionPredictor` is intended for **test-time sampling**: it is not suitable for training. The wrapped multi-diffusion model should already be trained before being passed to the predictor. Parameters ---------- model : MultiDiffusionModel2D A trained multi-diffusion model. The grid patching configuration must be supplied through :meth:`set_patching` after construction. condition : torch.Tensor, TensorDict, or None, optional, default=None Conditioning at the global resolution, bound once at construction and reused at every diffusion step. Shape :math:`(B, *cond\_dims)`. Pass ``None`` for unconditional models. fuse : bool, default=True Whether to fuse per-patch outputs back to the global resolution before returning. chunk_size : int or None, default=None Number of patch rows along the :math:`P \times B` dimension processed per model call. ``None`` runs all patches in a single call. Set to a small integer to reduce peak GPU memory when the full :math:`P \times B` activation tensor does not fit at once. use_checkpointing : bool, default=False Trade compute for memory: activations are recomputed on demand during backpropagation instead of being stored from the forward pass. Useful when differentiating through the predictor on large domains. Works with or without ``chunk_size``. prediction_type : PredictorType, default="x0" Output type of the wrapped model. One of ``"x0"``, ``"score"``, or ``"epsilon"``. The predictor always exposes an x0-compatible output; pass the appropriate conversion function below when the model does not directly predict x0. score_to_x0_fn : callable, optional Conversion ``(score, x_t, t) -> x0`` applied to the model output. Required when ``prediction_type="score"``. Typically obtained from a noise scheduler, e.g. :meth:`~physicsnemo.diffusion.noise_schedulers.LinearGaussianNoiseScheduler.score_to_x0`. epsilon_to_x0_fn : callable, optional Conversion ``(epsilon, x_t, t) -> x0`` applied to the model output. Required when ``prediction_type="epsilon"``. Typically obtained from a noise scheduler, e.g. :meth:`~physicsnemo.diffusion.noise_schedulers.LinearGaussianNoiseScheduler.epsilon_to_x0`. **model_kwargs : Any Additional keyword arguments bound once at construction and forwarded to the wrapped model at every call. See Also -------- :class:`~physicsnemo.diffusion.multi_diffusion.MultiDiffusionModel2D` : The multi-diffusion wrapper used for training. :class:`~physicsnemo.diffusion.Predictor` : The protocol this class implements. :func:`~physicsnemo.diffusion.samplers.sample` : The main sampling entry point. :class:`~physicsnemo.diffusion.multi_diffusion.MultiDiffusionDPSScorePredictor` : Patch-local DPS score predictor that consumes :meth:`chunks` for memory-efficient guided sampling. Examples -------- **Example 1:** Predictor in isolation. Input and output live at the global resolution; patching, per-patch prediction, and fusing are all handled internally: >>> import torch >>> from physicsnemo.core import Module >>> from physicsnemo.diffusion.multi_diffusion import ( ... MultiDiffusionModel2D, ... MultiDiffusionPredictor, ... ) >>> class Backbone(Module): ... def __init__(self): ... super().__init__() ... self.net = torch.nn.Conv2d(3, 3, 1) ... def forward(self, x, t, condition=None): ... return self.net(x) >>> >>> # Train and save the model (training omitted here) >>> md = MultiDiffusionModel2D(Backbone(), global_spatial_shape=(16, 16)) >>> md.set_random_patching(patch_shape=(8, 8), patch_num=4) # training config >>> _ = md.eval() >>> >>> predictor = MultiDiffusionPredictor(md) >>> predictor.set_patching(overlap_pix=0, boundary_pix=0) # P = 4 patches per sample >>> x = torch.randn(2, 3, 16, 16) # global-resolution state >>> t = 0.5 * torch.ones(2) >>> predictor(x, t).shape # fused output at global resolution torch.Size([2, 3, 16, 16]) >>> >>> predictor.fuse = False # raw per-patch predictions instead >>> predictor(x, t).shape torch.Size([8, 3, 8, 8]) **Example 2:** Unconditional sampling. The predictor plugs straight into the standard diffusion sampling stack (noise scheduler, denoiser, solver): >>> from physicsnemo.diffusion.noise_schedulers import EDMNoiseScheduler >>> from physicsnemo.diffusion.samplers import sample >>> >>> md = MultiDiffusionModel2D(Backbone(), global_spatial_shape=(16, 16)) >>> md.set_random_patching(patch_shape=(8, 8), patch_num=4) >>> _ = md.eval() >>> >>> predictor = MultiDiffusionPredictor(md) >>> predictor.set_patching(overlap_pix=2, boundary_pix=0) >>> scheduler = EDMNoiseScheduler() >>> denoiser = scheduler.get_denoiser(x0_predictor=predictor) >>> xN = torch.randn(2, 3, 16, 16) # initial noise at global resolution >>> x0 = sample(denoiser, xN, scheduler, num_steps=4) >>> x0.shape torch.Size([2, 3, 16, 16]) **Example 3:** Conditional sampling with mixed conditioning, an image sharing the spatial resolution (patched like the state) and a vector (repeated across patches). Both kinds are bound once at construction and handled internally: >>> from tensordict import TensorDict >>> class MultiCondBackbone(Module): ... def __init__(self): ... super().__init__() ... self.conv = torch.nn.Conv2d(6, 3, 1) ... self.vec_proj = torch.nn.Linear(5, 3 * 8 * 8) ... def forward(self, x, t, condition=None): ... img = condition["image"] ... vec = condition["vector"] ... h = self.conv(torch.cat([x, img], dim=1)) ... return h + self.vec_proj(vec).view_as(h) >>> >>> md = MultiDiffusionModel2D( ... MultiCondBackbone(), ... global_spatial_shape=(16, 16), ... condition_patch={"image": True}, # image is patched ... ) >>> md.set_random_patching(patch_shape=(8, 8), patch_num=4) >>> _ = md.eval() >>> >>> condition = TensorDict({ ... "image": torch.randn(2, 3, 16, 16), ... "vector": torch.randn(2, 5), ... }, batch_size=[2]) >>> predictor = MultiDiffusionPredictor(md, condition=condition) >>> predictor.set_patching(overlap_pix=0, boundary_pix=0) >>> denoiser = scheduler.get_denoiser(x0_predictor=predictor) >>> xN = torch.randn(2, 3, 16, 16) >>> x0 = sample(denoiser, xN, scheduler, num_steps=4) >>> x0.shape torch.Size([2, 3, 16, 16]) **Example 4:** Memory-efficient inference on large domains. ``chunk_size`` and ``use_checkpointing`` are independent and address different bottlenecks. ``chunk_size`` is useful for any kind of inference (with or without gradients) and reduces peak memory by sacrificing some batch parallelism. ``use_checkpointing`` is only meaningful when gradients flow through the predictor (see :class:`~physicsnemo.diffusion.multi_diffusion.MultiDiffusionDPSScorePredictor` for the DPS guidance use case) and trades compute for memory by replaying the forward during backpropagation. Set ``chunk_size`` to process patches in chunks instead of all at once (helpful for plain inference on a domain that would otherwise OOM): >>> md = MultiDiffusionModel2D(Backbone(), global_spatial_shape=(16, 16)) >>> md.set_random_patching(patch_shape=(8, 8), patch_num=4) >>> _ = md.eval() >>> predictor = MultiDiffusionPredictor(md, chunk_size=2) >>> predictor.set_patching(overlap_pix=0, boundary_pix=0) >>> x = torch.randn(3, 3, 16, 16) # B = 3 >>> t = 0.5 * torch.ones(3) >>> predictor(x, t).shape # fused (B, C, H, W) torch.Size([3, 3, 16, 16]) Set ``use_checkpointing=True`` (independent of chunking) when differentiating through the predictor on a large domain: >>> predictor = MultiDiffusionPredictor(md, use_checkpointing=True) >>> predictor.set_patching(overlap_pix=0, boundary_pix=0) >>> x = torch.randn(3, 3, 16, 16, requires_grad=True) >>> y = predictor(x, t) >>> grad = torch.autograd.grad(y.sum(), x)[0] >>> grad.shape torch.Size([3, 3, 16, 16]) Combine both for differentiable inference on very large domains. A typical use case is computing the gradient of a DPS guidance likelihood through the predictor (see :class:`~physicsnemo.diffusion.multi_diffusion.MultiDiffusionDPSScorePredictor`): >>> predictor = MultiDiffusionPredictor(md, chunk_size=2, use_checkpointing=True) >>> predictor.set_patching(overlap_pix=0, boundary_pix=0) >>> x = torch.randn(3, 3, 16, 16, requires_grad=True) >>> y = predictor(x, t) >>> grad = torch.autograd.grad(y.sum(), x)[0] >>> grad.shape torch.Size([3, 3, 16, 16]) Use :meth:`chunks` for explicit per-chunk control. The iterator yields ``(slice_start, x0_chunk, x_chunk, t_chunk)`` tuples; the caller is responsible for fusing via :meth:`fuse_fn` after the loop. This is functionally equivalent to ``__call__`` with ``chunk_size`` set, but exposes the intermediate per-chunk values so callers can interleave their own per-chunk processing (e.g. accumulating patch-local guidance terms): >>> predictor = MultiDiffusionPredictor(md, chunk_size=4, use_checkpointing=True) >>> predictor.set_patching(overlap_pix=0, boundary_pix=0) >>> x = torch.randn(3, 3, 16, 16, requires_grad=True) # B = 3, P*B = 12 >>> t = 0.5 * torch.ones(3) >>> outs = [] >>> for s, x0_c, x_c, t_c in predictor.chunks(x, t): ... outs.append(x0_c) # (chunk_size=4, C, Hp, Wp) >>> x0_patched = torch.cat(outs, dim=0) # (P*B=12, C, Hp, Wp) >>> x0_global = predictor.fuse_fn(x0_patched) # (B=3, C, H, W) >>> grad = torch.autograd.grad(x0_global.sum(), x)[0] >>> grad.shape torch.Size([3, 3, 16, 16]) """ def __init__( self, model: MultiDiffusionModel2D, condition: Float[Tensor, " B *cond_dims"] | TensorDict | None = None, fuse: bool = True, chunk_size: int | None = None, use_checkpointing: bool = False, prediction_type: PredictorType = "x0", score_to_x0_fn: Callable[ [Float[Tensor, " B *dims"], Float[Tensor, " B *dims"], Float[Tensor, " B"]], Float[Tensor, " B *dims"], ] | None = None, epsilon_to_x0_fn: Callable[ [Float[Tensor, " B *dims"], Float[Tensor, " B *dims"], Float[Tensor, " B"]], Float[Tensor, " B *dims"], ] | None = None, **model_kwargs: Any, ) -> None: self._md_model: MultiDiffusionModel2D = _unwrap_module( model, MultiDiffusionModel2D ) self.model = model self._model_kwargs = model_kwargs self._chunk_size = chunk_size self._use_checkpointing = use_checkpointing self._fuse: bool = fuse self._cond_input = condition # Predictor-owned patching parameters; default to the values saved on # the wrapped model. set_patching() overrides them at call time and # warns when the override differs from the saved value. self._patch_shape: tuple[int, int] | None = self._md_model.patch_shape self._global_shape: tuple[int, int] = tuple(self._md_model.global_spatial_shape) # Caches populated by set_patching(); guarded against use before then. self._patching: GridPatching2D | None = None self._P: int | None = None self._cond_patched: Tensor | TensorDict | None = None self._pos_embd_patched: Tensor | None = None # PE injection is handled by this class from a pre-patched cache; # suppress the wrapper's per-step PE injection. self._md_model._skip_positional_embedding_injection = True # Internal model fusing is handled externally by this predictor (via # fuse_fn); keep it disabled so gradient checkpointing replays observe # a stable model state across forward and backward passes. self._md_model._fuse = False # Prediction-type conversion (same pattern as MultiDiffusionMSEDSMLoss). match prediction_type: case "x0": self._to_x0: Callable[[Tensor, Tensor, Tensor], Tensor] = ( lambda pred, _x, _t: pred ) case "score": if score_to_x0_fn is None: raise ValueError( "score_to_x0_fn must be provided when prediction_type='score'." ) self._to_x0 = score_to_x0_fn case "epsilon": if epsilon_to_x0_fn is None: raise ValueError( "epsilon_to_x0_fn must be provided when prediction_type='epsilon'." ) self._to_x0 = epsilon_to_x0_fn case _: raise ValueError( f"prediction_type must be 'x0', 'score', or 'epsilon', " f"got '{prediction_type}'." ) # Bind the model-call helper once so the use_checkpointing branch is # resolved at construction rather than on every forward call. self._call_model: Callable[[Tensor, Tensor, Any], Tensor] = ( self._call_model_with_checkpoint if self._use_checkpointing else self._call_model_direct ) @property def fuse(self) -> bool: """Whether per-patch outputs are fused back to the global resolution before being returned.""" return self._fuse @fuse.setter def fuse(self, value: bool) -> None: """Set whether per-patch outputs are fused before being returned.""" self._fuse = value @property def patch_shape(self) -> tuple[int, int] | None: r"""Spatial shape :math:`(H_p, W_p)` of each patch.""" return self._md_model.patch_shape
[docs] def patch_fn( self, x: Float[Tensor, "B C H W"], ) -> Float[Tensor, "P_times_B C Hp Wp"]: r"""Patch a global-resolution spatial tensor. Forwards to :meth:`physicsnemo.diffusion.multi_diffusion.MultiDiffusionModel2D.patch_x` on the wrapped model. Useful for pre-patching auxiliary tensors (observations, masks) outside the predictor — for example in the constructor of a DPS guidance — so that all patching uses the same grid configuration as the predictor. Parameters ---------- x : Tensor Global-resolution tensor of shape :math:`(B, C, H, W)`. Returns ------- Tensor Patched tensor of shape :math:`(P \times B, C, H_p, W_p)`. """ self._check_patching_set() return self._md_model.patch_x(x)
[docs] def fuse_fn( self, patched: Float[Tensor, "P_times_B C Hp Wp"], ) -> Float[Tensor, "B C H W"]: r"""Fuse a complete patched tensor back to the global resolution. General-purpose fusing utility: takes a patched tensor of shape :math:`(P \times B, C, H_p, W_p)` and returns the corresponding global-resolution tensor :math:`(B, C, H, W)`. The original batch size :math:`B` is inferred as ``patched.shape[0] // patch_num``; the input must therefore contain the full :math:`P \times B` rows. Parameters ---------- patched : Tensor Full patched tensor of shape :math:`(P \times B, C, H_p, W_p)`. Returns ------- Tensor Fused tensor of shape :math:`(B, C, H, W)`. Raises ------ ValueError If ``patched.shape[0]`` is not divisible by ``patch_num``. """ self._check_patching_set() P = cast(int, self._P) if not torch.compiler.is_compiling() and patched.shape[0] % P != 0: raise ValueError( f"patched.shape[0] ({patched.shape[0]}) is not divisible by " f"patch_num ({P}); fuse_fn requires the full " f"(P*B, …) tensor." ) B = patched.shape[0] // P return self._md_model.fuse(patched, batch_size=B)
[docs] def set_patching( self, overlap_pix: int, boundary_pix: int, *, patch_shape: tuple[int, int] | None = None, global_shape: tuple[int, int] | None = None, ) -> None: r"""Set the grid patching configuration. Must be called once after construction and before any other method (:meth:`__call__`, :meth:`chunks`, :meth:`patch_fn`, :meth:`fuse_fn`, ...). Calling it again reconfigures the patching and rebuilds the internal pre-patched caches. ``overlap_pix`` and ``boundary_pix`` are required: they cannot be inferred from the wrapped model (which is typically trained with a random patching strategy that has neither concept). ``patch_shape`` and ``global_shape`` default to the values saved in the wrapped model. Overriding either emits a warning, since mismatching the training-time geometry can produce unexpected results (in particular, positional embeddings baked at model construction will no longer match a different ``global_shape``). Parameters ---------- overlap_pix : int Overlapping pixels between adjacent patches. boundary_pix : int Boundary pixels padded on each side. patch_shape : tuple[int, int], optional Override for the patch spatial shape :math:`(H_p, W_p)`. Default to the value saved on the wrapped model. global_shape : tuple[int, int], optional Override for the global spatial shape :math:`(H, W)`. Default to the value saved on the wrapped model. """ if patch_shape is not None: new_ps = tuple(patch_shape) if self._patch_shape is not None and new_ps != self._patch_shape: warnings.warn( f"Overriding saved patch_shape {self._patch_shape} with " f"{new_ps}. Inference-time patching that differs from " f"training may produce unexpected results.", stacklevel=2, ) self._patch_shape = new_ps if global_shape is not None: new_gs = tuple(global_shape) if new_gs != self._global_shape: warnings.warn( f"Overriding saved global_spatial_shape " f"{self._global_shape} with {new_gs}. Positional " f"embeddings baked at model construction will no " f"longer match the new shape.", stacklevel=2, ) self._global_shape = new_gs if self._patch_shape is None: raise RuntimeError( "patch_shape is not available on the wrapped model and was " "not provided. Pass patch_shape=(Hp, Wp) explicitly." ) # Sync the underlying model's global_spatial_shape so its patching # logic uses the same geometry as the predictor. self._md_model.global_spatial_shape = self._global_shape self._md_model.set_grid_patching( patch_shape=self._patch_shape, overlap_pix=overlap_pix, boundary_pix=boundary_pix, fuse=False, ) self._patching = cast(GridPatching2D, self._md_model._patching) self._P = self._patching.patch_num # Rebuild pre-patched condition / PE caches now that patching is set. self._cond_patched = self._md_model.patch_condition(self._cond_input) if self._md_model.pos_embd is not None: self._pos_embd_patched = self._md_model.patch_x( self._md_model.pos_embd.unsqueeze(0) ) # (P, C_PE, Hp, Wp) else: self._pos_embd_patched = None
def _build_cond(self, B: int) -> Tensor | TensorDict | None: # Expand the pre-patched condition to batch size B and inject PE. cond = self._cond_patched if self._pos_embd_patched is not None: P = cast(int, self._P) pe = self._pos_embd_patched.repeat_interleave(B, dim=0) cond = self._md_model._inject_patched_pos_embd(cond, pe, P * B) return cond
[docs] def chunks( self, x: Float[Tensor, "B C H W"], t: Float[Tensor, " B"], ) -> Iterator[ tuple[ int, # slice_start: row index of x0_chunk along (P*B) Float[Tensor, "K C Hp Wp"], # x0_chunk: model output (converted to x0) Float[Tensor, "K C Hp Wp"], # x_chunk: noisy input slice Float[Tensor, " K"], # t_chunk: time slice ] ]: r"""Stream the per-chunk model outputs alongside their inputs. Always returns patched outputs and does not fuse them. This makes the iterator particularly useful for use cases that need to combine the per-chunk model output with auxiliary patched data before fusing, such as patch-local DPS guidance (see :class:`~physicsnemo.diffusion.multi_diffusion.MultiDiffusionDPSScorePredictor`). On large global domains, the equivalent non-chunked call :meth:`__call__` may run out of GPU memory; iterating the patches in chunks of size ``chunk_size`` keeps the peak activation footprint bounded. Use :meth:`fuse_fn` after the loop to fuse the concatenated chunks back to the global resolution. Functionally equivalent to ``__call__`` with ``chunk_size`` set (which streams internally and fuses at the end). Use this iterator when explicit per-chunk control is needed; otherwise prefer the higher-level ``__call__``. Patching is performed once at the start of the iteration; subsequent iterations only slice the pre-patched tensors. Requires ``chunk_size`` to be set at construction. Parameters ---------- x : Tensor Noisy latent at global resolution, shape :math:`(B, C, H, W)`. t : Tensor Diffusion time, shape :math:`(B,)`. Returns ------- Iterator yielding tuples ``(slice_start, x0_chunk, x_chunk, t_chunk)``: - ``slice_start`` (``int``): row index of the chunk along the :math:`(P \times B)` dimension. Allows downstream consumers (e.g. patch-local DPS guidances) to align their own pre-patched data with the current chunk. - ``x0_chunk`` (``Tensor``): model output for this chunk (already converted to x0 when a conversion was configured), shape :math:`(K, C, H_p, W_p)` with :math:`K \leq chunk\_size`. All chunks have ``chunk_size`` rows except possibly the last, which may be smaller when :math:`P \times B` is not divisible by ``chunk_size``. - ``x_chunk`` (``Tensor``): noisy input slice corresponding to ``x0_chunk``, same shape. - ``t_chunk`` (``Tensor``): time slice corresponding to ``x0_chunk``, shape :math:`(K,)`. Raises ------ RuntimeError If ``chunk_size`` was not set at construction, or if :meth:`set_patching` has not been called. Examples -------- Streaming inference with a per-chunk computation in the loop body (here just collecting a per-chunk statistic, but in practice this is where DPS guidance terms or other patch-local processing would plug in): >>> import torch >>> from physicsnemo.core import Module >>> from physicsnemo.diffusion.multi_diffusion import ( ... MultiDiffusionModel2D, MultiDiffusionPredictor, ... ) >>> class Backbone(Module): ... def __init__(self): ... super().__init__() ... self.net = torch.nn.Conv2d(3, 3, 1) ... def forward(self, x, t, condition=None): ... return self.net(x) >>> md = MultiDiffusionModel2D(Backbone(), global_spatial_shape=(16, 16)) >>> md.set_random_patching(patch_shape=(8, 8), patch_num=4) >>> _ = md.eval() >>> predictor = MultiDiffusionPredictor(md, chunk_size=2) >>> predictor.set_patching(overlap_pix=0, boundary_pix=0) >>> x = torch.randn(2, 3, 16, 16) >>> t = 0.5 * torch.ones(2) >>> chunks_list, chunk_norms = [], [] >>> for s, x0_c, x_c, t_c in predictor.chunks(x, t): ... chunk_norms.append(x0_c.pow(2).sum()) # per-chunk computation ... chunks_list.append(x0_c) >>> x0_global = predictor.fuse_fn(torch.cat(chunks_list, dim=0)) >>> x0_global.shape torch.Size([2, 3, 16, 16]) """ self._check_patching_set() if self._chunk_size is None: raise RuntimeError( "chunk_size must be set at construction to use chunks(). " "Pass chunk_size=<int> to MultiDiffusionPredictor.__init__." ) B = x.shape[0] x_patched = self._md_model.patch_x(x) # (P*B, C, Hp, Wp) t_patched = self._md_model.patch_t(t) # (P*B,) cond = self._build_cond(B) K = self._chunk_size PB = x_patched.shape[0] for s in range(0, PB, K): e = min(s + K, PB) x_c = x_patched[s:e] t_c = t_patched[s:e] c_c = cond[s:e] if cond is not None else None out = self._call_model(x_c, t_c, c_c) x0_c = self._to_x0(out, x_c, t_c) yield s, x0_c, x_c, t_c
def _check_patching_set(self) -> None: """Raise if :meth:`set_patching` has not been called yet. Guarded by ``torch.compiler.is_compiling`` so it is a no-op under ``torch.compile``. """ if not torch.compiler.is_compiling() and self._patching is None: raise RuntimeError( "Grid patching is not configured. Call set_patching(" "overlap_pix, boundary_pix, ...) before any other method." ) def _call_model_direct( self, x_p: Tensor, t_p: Tensor, cond: Any, ) -> Tensor: """Call the wrapped model on the patched inputs, no checkpointing.""" return self._md_model( x_p, t_p, condition=cond, x_is_patched=True, t_is_patched=True, condition_is_patched=True, **self._model_kwargs, ) def _call_model_with_checkpoint( self, x_p: Tensor, t_p: Tensor, cond: Any, ) -> Tensor: """Call the wrapped model under :func:`torch.utils.checkpoint.checkpoint`. ``cond`` is captured as a default argument so each invocation binds its own value, which is required for correct backward replay when called inside a loop. """ def _inner(xc: Tensor, tc: Tensor, _cond=cond) -> Tensor: # noqa: ANN001 return self._md_model( xc, tc, condition=_cond, x_is_patched=True, t_is_patched=True, condition_is_patched=True, **self._model_kwargs, ) return checkpoint(_inner, x_p, t_p, use_reentrant=False) def __call__( self, x: Float[Tensor, "B C H W"], t: Float[Tensor, " B"], ) -> Float[Tensor, "B C H W"] | Float[Tensor, "P_times_B C Hp Wp"]: r"""Run the predictor on a noisy latent and diffusion time. Parameters ---------- x : torch.Tensor Noisy latent at global resolution, shape :math:`(B, C, H, W)`. t : torch.Tensor Diffusion time, shape :math:`(B,)`. Returns ------- torch.Tensor If ``self.fuse=True``: prediction at the global resolution, shape :math:`(B, C, H, W)`. Otherwise: per-patch predictions, shape :math:`(P \times B, C, H_p, W_p)`. """ self._check_patching_set() if self._chunk_size is not None: x0_chunks = [x0_c for _, x0_c, _, _ in self.chunks(x, t)] output = torch.cat(x0_chunks, dim=0) # (P*B, C, Hp, Wp) return self.fuse_fn(output) if self._fuse else output B = x.shape[0] x_patched = self._md_model.patch_x(x) # (P*B, C, Hp, Wp) t_patched = self._md_model.patch_t(t) # (P*B,) cond = self._build_cond(B) result = self._call_model(x_patched, t_patched, cond) result = self._to_x0(result, x_patched, t_patched) return self.fuse_fn(result) if self._fuse else result