# 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 model wrapper for patch-based diffusion."""
import warnings
from collections import defaultdict
from typing import Any, Dict, Literal, Tuple
import numpy as np
import torch
import torch.nn.functional as F
from jaxtyping import Float
from tensordict import TensorDict
from torch import Tensor
from physicsnemo.core import Module
from physicsnemo.diffusion.multi_diffusion.patching import (
GridPatching2D,
RandomPatching2D,
)
[docs]
class MultiDiffusionModel2D(Module):
r"""Multi-diffusion model wrapper for 2D patch-based diffusion.
Multi-diffusion is a method useful for scaling diffusion models to large
domains. A multi-diffusion model splits a 2D latent state into smaller
patches, processes each patch independently through the wrapped model, and
optionally fuses the outputs back into a full-resolution image.
The wrapper handles:
- Patching the state :math:`\mathbf{x}` into :math:`P` smaller
patches, expanding the batch dimension from :math:`B` to
:math:`P \times B`.
- For conditional diffusion models, pre-processing each conditioning tensor
according to some specified strategies: patching, interpolating to patch
resolution, or simply repeating along the batch dimension.
- Extracting positional embeddings for each patch and injecting them into
the condition under the key ``"positional_embedding"``. This optional
feature is useful to encode the relative position of the patches within
the global domain into the condition.
- Calling the wrapped model on the patched inputs and the pre-processed
conditioning tensors.
- Optionally fusing the per-patch outputs back to the global spatial
resolution (grid patching only).
Before a forward pass, a patching strategy must be configured via
:meth:`set_random_patching` (typically for training) or
:meth:`set_grid_patching` (typically for sampling/inference).
The wrapped ``model`` must be an instance of
:class:`~physicsnemo.core.Module` that satisfies the
:class:`~physicsnemo.diffusion.DiffusionModel` interface:
.. code-block:: python
model(
x: torch.Tensor, # Noisy state, shape: (P*B, C, Hp, Wp)
t: torch.Tensor, # Diffusion time, shape: (P*B,)
condition: TensorDict | None = None, # Pre-processed conditioning tensors, shape: (P*B, *cond_dims)
**model_kwargs: Any,
) -> torch.Tensor # Prediction, shape: (P*B, C, Hp, Wp)
The ``MultiDiffusionModel2D`` wrapper itself also satisfies the
:class:`~physicsnemo.diffusion.DiffusionModel` interface.
.. important::
The wrapped model is responsible for consuming the patched inputs and
the pre-processed conditioning tensors appropriately. For example, if
the wrapped model concatenates conditioning tensors to the input, it
should be designed to handle the pre-processed (patched, interpolated,
expanded, etc.) conditioning tensors.
**Condition pre-processing strategies.** Each conditioning tensor is
pre-processed according to one of three mutually exclusive strategies
controlled by ``condition_patch`` and ``condition_interp``:
- **Patch** (``condition_patch=True``): the conditioning tensor is
decomposed into the same spatial patches as the state
:math:`\mathbf{x}`. Requires a 4D tensor :math:`(B, C, H, W)` whose
spatial dimensions match ``global_spatial_shape``. Useful to provide
local, patch-level information to the model.
- **Interpolate** (``condition_interp=True``): the conditioning tensor
is bilinearly interpolated to the patch spatial resolution
:math:`(H_p, W_p)` and repeated for each of the :math:`P` patches.
Requires a 4D tensor :math:`(B, C, H', W')` (spatial dimensions
need not match the global shape). Useful to encode a coarse global
view into each patch.
- **Default** (both ``False``): the tensor is simply repeated
:math:`P` times along the batch dimension without spatial
processing. Useful for vector-valued conditioning.
A given conditioning key cannot have both ``condition_patch`` and
``condition_interp`` set to ``True`` simultaneously.
**Converting inputs to patch-compatible format.** The wrapper exposes
three public methods that convert each input to the patch-compatible
format with shape :math:`(P \times B, ...)`:
- :meth:`patch_x`: spatial patching of the global domain, or global state
:math:`\mathbf{x}`. Can be used to patch any global spatial tensor with
shape :math:`(B, C, H, W)` to the patch-compatible format with shape
:math:`(P \times B, C, H_p, W_p)`.
- :meth:`patch_t`: batch-dimension expansion of the diffusion time.
- :meth:`patch_condition`: patching / interpolation / expansion of
the condition, depending on the configured strategy.
These methods are called internally by :meth:`forward`, but can also
be called externally for finer control (e.g., from a loss function
that needs to add per-patch noise).
Parameters
----------
model : physicsnemo.Module
The underlying neural network to wrap, with the signature described
above. Must be an instance of :class:`~physicsnemo.core.Module` that
satisfies the :class:`~physicsnemo.diffusion.DiffusionModel` protocol.
global_spatial_shape : Tuple[int, int]
Height and width :math:`(H, W)` of the global (un-patched) spatial
domain.
positional_embedding : Literal["learnable", "sinusoidal", "linear"] | None, default=None
Type of positional embedding to generate. Controls how global spatial
coordinates are encoded into the conditioning. ``"learnable"`` creates
a trainable parameter grid. ``"sinusoidal"`` uses fixed sin/cos
encodings. ``"linear"`` uses a rectilinear grid over
:math:`[-1, 1]^2`. ``None`` disables positional embeddings. When
enabled, patches of the embedding grid are extracted and injected into
the condition under the key ``"positional_embedding"`` during
:meth:`forward`. The wrapped model must accept a ``TensorDict``
condition and consume this key.
channels_positional_embedding : int, default=4
Number of channels :math:`C_{PE}` in the positional embedding grid.
For ``"sinusoidal"`` must be a multiple of 4. For ``"linear"`` must
be 2. For ``"learnable"`` can be any positive integer. Ignored when
``positional_embedding`` is ``None``.
condition_patch : bool | Dict[str, bool], default=False
Controls whether conditioning tensors are patched. When a single
``bool``, the flag applies uniformly to every conditioning tensor
(or to the single ``Tensor`` condition). When a ``Dict[str, bool]``,
each key maps to a specific key in a ``TensorDict`` condition; keys
not present default to ``False``.
condition_interp : bool or Dict[str, bool], default=False
Controls whether conditioning tensors are interpolated to patch
resolution. Follows the same ``bool`` / ``Dict[str, bool]``
convention as ``condition_patch``.
Forward
-------
x : torch.Tensor
Noisy latent state. Shape :math:`(B, C, H, W)` at global resolution,
or :math:`(P \times B, C, H_p, W_p)` if ``x_is_patched=True``.
t : torch.Tensor
Diffusion time. Shape :math:`(B,)`, or :math:`(P \times B,)` if
``t_is_patched=True``.
condition : torch.Tensor, TensorDict, or None, optional, default=None
Conditioning information at **global** resolution (batch size
:math:`B`), or already in patch-compatible format if
``condition_is_patched=True``. When positional embeddings are enabled
and ``condition_is_patched=False``, must be a ``TensorDict`` or
``None``.
x_is_patched : bool, default=False
If ``True``, ``x`` is assumed to already be in patch-compatible
shape :math:`(P \times B, C, H_p, W_p)` and :meth:`patch_x` is
skipped.
t_is_patched : bool, default=False
If ``True``, ``t`` is assumed to already be in patch-compatible
shape :math:`(P \times B,)` and :meth:`patch_t` is skipped.
condition_is_patched : bool, default=False
If ``True``, ``condition`` is assumed to already be in
patch-compatible format and both :meth:`patch_condition` and
positional-embedding injection are skipped.
**model_kwargs : Any
Additional keyword arguments forwarded to the wrapped model.
Outputs
-------
torch.Tensor
If fusing is enabled (grid patching with ``fuse=True``), the output
has shape :math:`(B, C, H, W)`. Otherwise the output has shape
:math:`(P \times B, C, H_p, W_p)`.
Notes
-----
Reference: Bar-Tal, O., Yariv, L., Lipman, Y. and Dekel, T., 2023.
`MultiDiffusion: Fusing Diffusion Paths for Controlled Image Generation
<https://arxiv.org/abs/2302.08113>`_
See Also
--------
:class:`~physicsnemo.diffusion.multi_diffusion.MultiDiffusionMSEDSMLoss` :
Patch-based denoising score matching loss for use with this wrapper.
:class:`~physicsnemo.diffusion.multi_diffusion.RandomPatching2D` :
Random patching strategy (training).
:class:`~physicsnemo.diffusion.multi_diffusion.GridPatching2D` :
Grid patching strategy (sampling).
Examples
--------
**Example 1:** Unconditional model: training with random patches then
sampling with grid patches:
>>> import torch
>>> from physicsnemo.core import Module
>>> from physicsnemo.diffusion.multi_diffusion import MultiDiffusionModel2D
>>>
>>> class UnconditionalModel(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)
>>>
>>> model = UnconditionalModel()
>>> md_model = MultiDiffusionModel2D(model, global_spatial_shape=(16, 16))
>>>
>>> # Training: random patching, P=6 patches of 8x8 per batch element
>>> md_model.set_random_patching(patch_shape=(8, 8), patch_num=6)
>>> x0 = torch.randn(2, 3, 16, 16) # clean global state
>>> sigma = 0.5
>>> xt = x0 + sigma * torch.randn_like(x0) # noisy global state
>>> t = sigma * torch.ones(2)
>>> x0_hat = md_model(xt, t) # patched denoised estimate, shape: (P*B, C, Hp, Wp)
>>> x0_hat.shape
torch.Size([12, 3, 8, 8])
>>> # Patch x0 for loss computation
>>> x0_patched = md_model.patch_x(x0) # patched global state, shape: (P*B, C, Hp, Wp)
>>> loss = ((x0_hat - x0_patched) ** 2).mean()
>>>
>>> # Re-draw random patch positions for the next training step
>>> md_model.reset_patch_indices()
>>> x0_hat = md_model(xt, t)
>>> x0_hat.shape
torch.Size([12, 3, 8, 8])
>>>
>>> # One can also patch x and t externally
>>> xt_patched = md_model.patch_x(xt)
>>> t_patched = md_model.patch_t(t)
>>> x0_hat = md_model(xt_patched, t_patched, x_is_patched=True, t_is_patched=True)
>>> x0_hat.shape
torch.Size([12, 3, 8, 8])
>>>
>>> # -- Sampling: grid patching with overlap and fusion --
>>> _ = md_model.eval()
>>> md_model.set_grid_patching(patch_shape=(8, 8), overlap_pix=2, fuse=True)
>>> xN = torch.randn(2, 3, 16, 16) # noisy global state
>>> t = 0.5 * torch.ones(2)
>>> denoised = md_model(xN, t) # denoised global state, shape: (B, C, H, W)
>>> denoised.shape
torch.Size([2, 3, 16, 16])
**Example 2:** Conditional model with a single image condition that is
patched alongside the state:
>>> class ConditionalModel(Module):
... def __init__(self):
... super().__init__()
... # 5 input channels: 3 (state) + 2 (conditioning image)
... self.net = torch.nn.Conv2d(5, 3, 1)
... def forward(self, x, t, condition=None):
... return self.net(torch.cat([x, condition], dim=1))
>>>
>>> cond_md_model = MultiDiffusionModel2D(
... model=ConditionalModel(),
... global_spatial_shape=(16, 16),
... condition_patch=True,
... )
>>>
>>> # Training: random patching
>>> cond_md_model.set_random_patching(patch_shape=(8, 8), patch_num=6)
>>> x0 = torch.randn(2, 3, 16, 16) # clean global state
>>> xt = x0 + 0.5 * torch.randn_like(x0) # noisy global state
>>> t = 0.5 * torch.ones(2)
>>> cond_img = torch.randn(2, 2, 16, 16) # conditioning image
>>> x0_hat = cond_md_model(xt, t, condition=cond_img)
>>> x0_hat.shape
torch.Size([12, 3, 8, 8])
>>> x0_patched = cond_md_model.patch_x(x0)
>>> loss = ((x0_hat - x0_patched) ** 2).mean()
>>>
>>> # Re-draw random patch positions for the next training step
>>> cond_md_model.reset_patch_indices()
>>> x0_hat = cond_md_model(xt, t, condition=cond_img)
>>> x0_hat.shape
torch.Size([12, 3, 8, 8])
>>>
>>> # One can also patch x and t externally
>>> xt_patched = cond_md_model.patch_x(xt)
>>> t_patched = cond_md_model.patch_t(t)
>>> x0_hat = cond_md_model(xt_patched, t_patched,
... condition=cond_img,
... x_is_patched=True, t_is_patched=True)
>>> x0_hat.shape
torch.Size([12, 3, 8, 8])
>>>
>>> # -- Sampling: grid patching with overlap and fusion --
>>> _ = cond_md_model.eval()
>>> cond_md_model.set_grid_patching(patch_shape=(8, 8), overlap_pix=2, fuse=True)
>>> xN = torch.randn(2, 3, 16, 16) # noisy global state
>>> denoised = cond_md_model(xN, t, condition=cond_img) # denoised global state, shape: (B, C, H, W)
>>> denoised.shape
torch.Size([2, 3, 16, 16])
**Example 3:** Conditional model with positional embeddings and two
conditioning tensors (an image interpolated to patch resolution and a
vector repeated for each patch):
>>> class MultiCondModel(Module):
... def __init__(self):
... super().__init__()
... # 9 input channels: 3 (state) + 2 (interpolated image) + 4 (positional embedding)
... self.net = torch.nn.Conv2d(9, 3, 1)
... self.vec_proj = torch.nn.Linear(5, 3 * 8 * 8)
... def forward(self, x, t, condition=None):
... # Wrapped model is designed to consume the conditioning
... # tensors and positional embeddings
... img = condition["image"]
... pe = condition["positional_embedding"]
... vec = condition["vector"]
... h = self.net(torch.cat([x, img, pe], dim=1))
... return h + self.vec_proj(vec).view_as(h)
>>>
>>> from tensordict import TensorDict
>>> mc_md_model = MultiDiffusionModel2D(
... model=MultiCondModel(),
... global_spatial_shape=(16, 16),
... positional_embedding="sinusoidal",
... channels_positional_embedding=4,
... condition_interp={"image": True},
... )
>>>
>>> # Training: random patching
>>> mc_md_model.set_random_patching(patch_shape=(8, 8), patch_num=6)
>>> x0 = torch.randn(2, 3, 16, 16)
>>> xt = x0 + 0.5 * torch.randn_like(x0) # noisy global state
>>> t = 0.5 * torch.ones(2)
>>> cond = TensorDict({
... "image": torch.randn(2, 2, 16, 16),
... "vector": torch.randn(2, 5),
... }, batch_size=[2])
>>> x0_hat = mc_md_model(xt, t, condition=cond)
>>> x0_hat.shape
torch.Size([12, 3, 8, 8])
>>> x0_patched = mc_md_model.patch_x(x0)
>>> loss = ((x0_hat - x0_patched) ** 2).mean()
>>>
>>> # Re-draw random patch positions for the next training step
>>> mc_md_model.reset_patch_indices()
>>> x0_hat = mc_md_model(xt, t, condition=cond)
>>> x0_hat.shape
torch.Size([12, 3, 8, 8])
>>>
>>> # One can also patch x and t externally
>>> xt_patched = mc_md_model.patch_x(xt)
>>> t_patched = mc_md_model.patch_t(t)
>>> x0_hat = mc_md_model(xt_patched, t_patched,
... condition=cond,
... x_is_patched=True, t_is_patched=True)
>>> x0_hat.shape
torch.Size([12, 3, 8, 8])
>>>
>>> # -- Sampling: grid patching with overlap and fusion --
>>> _ = mc_md_model.eval()
>>> mc_md_model.set_grid_patching(patch_shape=(8, 8), overlap_pix=2, fuse=True)
>>> xN = torch.randn(2, 3, 16, 16) # noisy global state
>>> denoised = mc_md_model(xN, t, condition=cond) # denoised global state, shape: (B, C, H, W)
>>> denoised.shape
torch.Size([2, 3, 16, 16])
"""
# Class-level type annotation so static type checkers resolve the subscript
# operations on _patch_shape (set in __init__ via register_buffer).
_patch_shape: Tensor
def __init__(
self,
model: Module,
global_spatial_shape: Tuple[int, int],
positional_embedding: Literal["learnable", "sinusoidal", "linear"]
| None = None,
channels_positional_embedding: int = 4,
condition_patch: bool | Dict[str, bool] = False,
condition_interp: bool | Dict[str, bool] = False,
) -> None:
super().__init__()
self.model = model
self.global_spatial_shape = tuple(global_spatial_shape)
self._patching: RandomPatching2D | GridPatching2D | None = None
self._fuse: bool = False
self._skip_positional_embedding_injection: bool = False
# Persistent buffer so that patch_shape survives checkpoint save/load.
# Zeros sentinel means "not yet configured".
self.register_buffer(
"_patch_shape", torch.zeros(2, dtype=torch.long), persistent=True
)
# Normalise condition flags to defaultdict for uniform access
if not isinstance(condition_patch, (bool, dict)):
raise TypeError(
f"condition_patch must be bool or Dict[str, bool], "
f"got {type(condition_patch).__name__}."
)
if not isinstance(condition_interp, (bool, dict)):
raise TypeError(
f"condition_interp must be bool or Dict[str, bool], "
f"got {type(condition_interp).__name__}."
)
self._condition_has_per_key_flags = isinstance(
condition_patch, dict
) or isinstance(condition_interp, dict)
self._condition_patch: defaultdict[str, bool] = (
defaultdict(lambda: condition_patch)
if isinstance(condition_patch, bool)
else defaultdict(lambda: False, condition_patch)
)
self._condition_interp: defaultdict[str, bool] = (
defaultdict(lambda: condition_interp)
if isinstance(condition_interp, bool)
else defaultdict(lambda: False, condition_interp)
)
# Positional embedding
if positional_embedding is not None:
H, W = self.global_spatial_shape
C = channels_positional_embedding
if positional_embedding == "learnable":
self.pos_embd = torch.nn.Parameter(torch.randn(C, H, W))
elif positional_embedding == "linear":
if C != 2:
raise ValueError(
"channels_positional_embedding must be 2 for "
"'linear' positional embedding."
)
gx, gy = np.meshgrid(np.linspace(-1, 1, W), np.linspace(-1, 1, H))
grid = torch.from_numpy(np.stack([gy, gx], axis=0)).float()
self.register_buffer("pos_embd", grid)
elif positional_embedding == "sinusoidal":
if C % 4 != 0:
raise ValueError(
"channels_positional_embedding must be a multiple "
"of 4 for 'sinusoidal' positional embedding."
)
num_freq = C // 4
freq_bands = 2.0 ** np.arange(num_freq)
gx, gy = np.meshgrid(
np.linspace(0, 2 * np.pi, W),
np.linspace(0, 2 * np.pi, H),
)
grids = []
for freq in freq_bands:
for fn in [np.sin, np.cos]:
grids.append(fn(gx * freq))
grids.append(fn(gy * freq))
grid = torch.from_numpy(np.stack(grids, axis=0)).float()
self.register_buffer("pos_embd", grid, persistent=False)
else:
raise ValueError(
f"positional_embedding must be 'learnable', "
f"'sinusoidal', 'linear', or None, "
f"got '{positional_embedding}'."
)
else:
self.pos_embd = None
# ------------------------------------------------------------------
# Properties for condition pre-processing flags
# ------------------------------------------------------------------
@property
def condition_patch(self) -> bool | Dict[str, bool]:
"""Whether conditioning tensors are patched."""
return self._condition_patch
@property
def condition_interp(self) -> bool | Dict[str, bool]:
"""Whether conditioning tensors are interpolated to patch resolution."""
return self._condition_interp
@property
def patch_shape(self) -> tuple[int, int] | None:
r"""Spatial shape :math:`(H_p, W_p)` of each patch, or ``None`` if no
patching strategy has been configured yet.
The value is read from the live patching object when available, and
falls back to the persistent checkpoint buffer when the model was
loaded from a checkpoint but ``set_grid_patching`` / ``set_random_patching``
have not been called yet.
Examples
--------
>>> import torch
>>> from physicsnemo.core import Module
>>> from physicsnemo.diffusion.multi_diffusion import MultiDiffusionModel2D
>>> class M(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(M(), global_spatial_shape=(16, 16))
>>> md.patch_shape is None
True
>>> md.set_grid_patching(patch_shape=(8, 8))
>>> md.patch_shape
(8, 8)
"""
patching = self._patching
if patching is not None:
return patching.patch_shape
ps = self._patch_shape
if int(ps[0]) > 0 or int(ps[1]) > 0:
return (int(ps[0]), int(ps[1]))
return None
# ------------------------------------------------------------------
# Patching strategy configuration
# ------------------------------------------------------------------
[docs]
def set_random_patching(
self,
patch_shape: Tuple[int, int],
patch_num: int,
) -> None:
r"""Configure random patching.
After calling this method, the forward pass decomposes each input
sample into ``patch_num`` randomly placed patches of size
``patch_shape``, expanding the batch dimension from :math:`B` to
:math:`P \times B`. This is typically used during training. Random
patches cannot be fused back to the global resolution.
To re-draw random patch positions between training
steps, call :meth:`reset_patch_indices`.
Parameters
----------
patch_shape : Tuple[int, int]
Height and width :math:`(H_p, W_p)` of each patch.
patch_num : int
Number of patches :math:`P` to extract per sample.
Examples
--------
>>> import torch
>>> from physicsnemo.core import Module
>>> from physicsnemo.diffusion.multi_diffusion import MultiDiffusionModel2D
>>> class M(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(M(), global_spatial_shape=(16, 16))
>>> md.set_random_patching(patch_shape=(8, 8), patch_num=4)
>>> md(torch.randn(2, 3, 16, 16), torch.rand(2)).shape
torch.Size([8, 3, 8, 8])
>>> md.reset_patch_indices() # re-draw positions for next step
>>> md(torch.randn(2, 3, 16, 16), torch.rand(2)).shape
torch.Size([8, 3, 8, 8])
"""
self._patching = RandomPatching2D(
img_shape=self.global_spatial_shape,
patch_shape=patch_shape,
patch_num=patch_num,
)
self._fuse = False
self._patch_shape[0] = patch_shape[0]
self._patch_shape[1] = patch_shape[1]
[docs]
def reset_patch_indices(self) -> None:
r"""Re-draw random patch positions for the current random patching
strategy.
Raises
------
RuntimeError
If the current patching strategy is not random patching.
"""
if not isinstance(self._patching, RandomPatching2D):
raise RuntimeError(
"reset_patch_indices() is only available when random "
"patching is active. Call set_random_patching() first."
)
self._patching.reset_patch_indices()
[docs]
def set_grid_patching(
self,
patch_shape: Tuple[int, int],
overlap_pix: int = 0,
boundary_pix: int = 0,
fuse: bool = True,
) -> None:
r"""Configure deterministic grid patching. Typically used for sampling.
The global domain is tiled with a regular grid of patches. When
``fuse=True``, the per-patch outputs are stitched back into a
full-resolution image at the end of each forward pass (overlapping
regions are averaged).
Parameters
----------
patch_shape : Tuple[int, int]
Height and width :math:`(H_p, W_p)` of each patch.
overlap_pix : int, default=0
Number of overlapping pixels between adjacent patches.
boundary_pix : int, default=0
Number of boundary pixels to pad on each side.
fuse : bool, default=True
If ``True``, per-patch outputs are fused back to global
resolution. Set to ``False`` when you want to model's forward pass
to return the per-patch outputs, i.e. (P*B, C, Hp, Wp).
Examples
--------
>>> import torch
>>> from physicsnemo.core import Module
>>> from physicsnemo.diffusion.multi_diffusion import MultiDiffusionModel2D
>>> class M(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(M(), global_spatial_shape=(16, 16))
>>> md.set_grid_patching(patch_shape=(8, 8), overlap_pix=2, fuse=True)
>>> md(torch.randn(2, 3, 16, 16), torch.rand(2)).shape
torch.Size([2, 3, 16, 16])
"""
self._patching = GridPatching2D(
img_shape=self.global_spatial_shape,
patch_shape=patch_shape,
overlap_pix=overlap_pix,
boundary_pix=boundary_pix,
)
self._fuse = fuse
self._patch_shape[0] = patch_shape[0]
self._patch_shape[1] = patch_shape[1]
# ------------------------------------------------------------------
# Public patching utilities
# ------------------------------------------------------------------
[docs]
def patch_x(
self, x: Float[Tensor, "B C H W"]
) -> Float[Tensor, "P_times_B C Hp Wp"]:
r"""Convert a global spatial 2D tensor to patch-compatible format.
Decomposes ``x`` into :math:`P` tiles according to the active
patching strategy. Can be used on any 4D tensor that shares the
same global spatial layout as the global state (e.g., ground-truth data
or weight masks).
Parameters
----------
x : Tensor
Tensor of shape :math:`(B, C, H, W)`.
Returns
-------
Tensor
Patched tensor of shape :math:`(P \times B, C, H_p, W_p)`.
Raises
------
RuntimeError
If no patching strategy has been configured.
"""
patching = self._patching
if patching is None:
raise RuntimeError(
"No patching strategy set. Call set_random_patching() "
"or set_grid_patching() first."
)
if not torch.compiler.is_compiling():
if x.ndim != 4:
raise ValueError(
f"patch_x expects a 4D tensor (B, C, H, W), got {x.ndim}D."
)
if tuple(x.shape[2:]) != self.global_spatial_shape:
raise ValueError(
f"Spatial dimensions {tuple(x.shape[2:])} do not match "
f"global_spatial_shape {self.global_spatial_shape}."
)
return patching.apply(x)
[docs]
def patch_t(self, t: Float[Tensor, " B"]) -> Float[Tensor, " P_times_B"]:
r"""Convert a diffusion-time tensor to patch-compatible format.
Repeats ``t`` :math:`P` times along the batch dimension so that
each patch receives the same diffusion time as its parent sample.
Parameters
----------
t : Tensor
Diffusion time of shape :math:`(B,)`.
Returns
-------
Tensor
Expanded tensor of shape :math:`(P \times B,)`.
Raises
------
RuntimeError
If no patching strategy has been configured.
"""
patching = self._patching
if patching is None:
raise RuntimeError(
"No patching strategy set. Call set_random_patching() "
"or set_grid_patching() first."
)
return t.repeat(patching.patch_num)
[docs]
def patch_condition(
self,
condition: Float[Tensor, " B *cond_dims"] | TensorDict | None,
) -> Tensor | TensorDict | None:
r"""Convert the condition to patch-compatible format.
Each tensor in the condition is pre-processed according to the
strategy set by the arguments ``condition_patch`` and
``condition_interp``: patched, interpolated, or simply repeated along
the batch dimension (default).
Positional embeddings are **not** injected by this method; they are
handled internally by :meth:`forward`.
Parameters
----------
condition : Tensor, TensorDict, or None
Conditioning information at global resolution (batch size
:math:`B`).
Returns
-------
Tensor, TensorDict, or None
Condition in patch-compatible format (batch size
:math:`P \times B`), or ``None`` if the input is ``None``.
Raises
------
RuntimeError
If no patching strategy has been configured.
Examples
--------
>>> import torch
>>> from tensordict import TensorDict
>>> from physicsnemo.core import Module
>>> from physicsnemo.diffusion.multi_diffusion import MultiDiffusionModel2D
>>> class M(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(
... M(), global_spatial_shape=(16, 16),
... condition_patch={"img": True},
... )
>>> md.set_random_patching(patch_shape=(8, 8), patch_num=4)
>>> cond = TensorDict({
... "img": torch.randn(2, 3, 16, 16),
... "vec": torch.randn(2, 5),
... }, batch_size=[2])
>>> cp = md.patch_condition(cond)
>>> cp["img"].shape # patched: (P*B, C, Hp, Wp)
torch.Size([8, 3, 8, 8])
>>> cp["vec"].shape # default: repeated P times
torch.Size([8, 5])
"""
patching = self._patching
if patching is None:
raise RuntimeError(
"No patching strategy set. Call set_random_patching() "
"or set_grid_patching() first."
)
if condition is None:
return None
P = patching.patch_num
if isinstance(condition, Tensor):
if self._condition_has_per_key_flags:
raise ValueError(
"condition_patch and condition_interp must be bool (not "
"dict) when condition is a plain Tensor. Use a TensorDict "
"for per-key control."
)
do_patch = self._condition_patch[""]
do_interp = self._condition_interp[""]
if not torch.compiler.is_compiling():
if do_patch and condition.ndim == 4:
if tuple(condition.shape[2:]) != self.global_spatial_shape:
raise ValueError(
f"condition_patch=True requires spatial dims "
f"{tuple(condition.shape[2:])} to match "
f"global_spatial_shape "
f"{self.global_spatial_shape}."
)
return self._process_condition_tensor(
condition, do_patch=do_patch, do_interp=do_interp, P=P
)
if isinstance(condition, TensorDict):
B = condition.batch_size[0]
if not torch.compiler.is_compiling():
cond_keys = set(condition.keys())
extra = set(self._condition_patch) - cond_keys
if extra:
raise ValueError(
f"condition_patch has keys {extra} not present "
f"in condition. Available keys: {cond_keys}."
)
extra = set(self._condition_interp) - cond_keys
if extra:
raise ValueError(
f"condition_interp has keys {extra} not present "
f"in condition. Available keys: {cond_keys}."
)
result = {}
for key in condition.keys():
do_patch = self._condition_patch[key]
do_interp = self._condition_interp[key]
tensor = condition[key]
if not torch.compiler.is_compiling():
if do_patch and tensor.ndim == 4:
if tuple(tensor.shape[2:]) != self.global_spatial_shape:
raise ValueError(
f"condition_patch=True for key '{key}' "
f"requires spatial dims "
f"{tuple(tensor.shape[2:])} to match "
f"global_spatial_shape "
f"{self.global_spatial_shape}."
)
result[key] = self._process_condition_tensor(
tensor, do_patch, do_interp, P
)
return TensorDict(result, batch_size=[P * B])
raise TypeError(
f"condition must be Tensor, TensorDict, or None, "
f"got {type(condition).__name__}."
)
[docs]
def fuse(
self,
input: Float[Tensor, "P_times_B C Hp Wp"],
batch_size: int,
) -> Float[Tensor, "B C H W"]:
r"""Fuse patches back into a full-resolution image.
Only supported when :meth:`set_grid_patching` has been called.
Random patches cannot be fused because their positions may overlap
arbitrarily.
Parameters
----------
input : Tensor
Patched tensor of shape :math:`(P \times B, C, H_p, W_p)`.
batch_size : int
Original batch size :math:`B` before patching.
Returns
-------
Tensor
Fused tensor of shape :math:`(B, C, H, W)`.
Raises
------
RuntimeError
If no patching strategy is set, or if the current strategy is
random patching.
Examples
--------
>>> import torch
>>> from physicsnemo.core import Module
>>> from physicsnemo.diffusion.multi_diffusion import MultiDiffusionModel2D
>>> class M(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(M(), global_spatial_shape=(16, 16))
>>> md.set_grid_patching(patch_shape=(8, 8))
>>> x = torch.randn(2, 3, 16, 16)
>>> x_patched = md.patch_x(x)
>>> torch.allclose(md.fuse(x_patched, batch_size=2), x)
True
"""
if not isinstance(self._patching, GridPatching2D):
raise RuntimeError(
"Fusing is only supported with grid patching. "
"Call set_grid_patching() first."
)
return self._patching.fuse(input, batch_size=batch_size)
# ------------------------------------------------------------------
# Forward
# ------------------------------------------------------------------
def forward(
self,
x: Float[Tensor, "B C H W"] | Float[Tensor, "P_times_B C Hp Wp"],
t: Float[Tensor, " B"] | Float[Tensor, " P_times_B"],
condition: Float[Tensor, " B *cond_dims"]
| Float[Tensor, " P_times_B *cond_dims"]
| TensorDict
| None = None,
x_is_patched: bool = False,
t_is_patched: bool = False,
condition_is_patched: bool = False,
**model_kwargs: Any,
) -> Float[Tensor, "P_times_B C Hp Wp"] | Float[Tensor, "B C H W"]:
# No patching strategy: warn and pass through
patching = self._patching
if patching is None:
if not torch.compiler.is_compiling():
warnings.warn(
"No patching strategy set on MultiDiffusionModel2D. "
"The model will run without patching.",
stacklevel=2,
)
if (
self.pos_embd is not None
and not self._skip_positional_embedding_injection
):
B = x.shape[0]
# .expand creates a stride-0 view that can trip up downstream
# torch ops (e.g. nn.ReflectionPad2d / F.unfold on torch 2.10).
# Materialise a contiguous copy before handing it off.
pos_embd = self.pos_embd.unsqueeze(0).expand(B, -1, -1, -1).contiguous()
condition = self._inject_patched_pos_embd(condition, pos_embd, B)
return self.model(x, t, condition=condition, **model_kwargs)
P = patching.patch_num
# B is only consumed by PE injection and fusing. When neither runs
# (e.g., MultiDiffusionPredictor calls into this method with both
# disabled to stream partial chunks), B is unused — skip computing
# and validating it so partial (K, C, Hp, Wp) tensors with K < P
# can be passed through.
_b_consumed = (
self.pos_embd is not None and not self._skip_positional_embedding_injection
) or self._fuse
if x_is_patched:
if (
_b_consumed
and not torch.compiler.is_compiling()
and x.shape[0] % P != 0
):
raise ValueError(
f"x_is_patched=True but x batch dim ({x.shape[0]}) is "
f"not divisible by patch_num ({P})."
)
B = x.shape[0] // P if _b_consumed else 0
else:
B = x.shape[0]
# Convert each input to patch-compatible format
if not x_is_patched:
x = self.patch_x(x)
if not t_is_patched:
t = self.patch_t(t)
if not condition_is_patched:
condition = self.patch_condition(condition)
# Positional embeddings injected here unless _skip_positional_embedding_injection
# is set (e.g. by MultiDiffusionPredictor which pre-patches PE at construction time)
if self.pos_embd is not None and not self._skip_positional_embedding_injection:
# .expand creates a stride-0 view that can trip up downstream
# torch ops (e.g. nn.ReflectionPad2d / F.unfold on torch 2.10).
# Materialise a contiguous copy before passing to patching.
pos_embd_patched = patching.apply(
self.pos_embd.unsqueeze(0).expand(B, -1, -1, -1).contiguous()
) # (P*B, C_PE, Hp, Wp)
condition = self._inject_patched_pos_embd(
condition, pos_embd_patched, P * B
)
output = self.model(x, t, condition=condition, **model_kwargs)
if self._fuse:
output = patching.fuse(output, batch_size=B)
return output
# ------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------
def _process_condition_tensor(
self,
tensor: Tensor,
do_patch: bool,
do_interp: bool,
P: int,
) -> Tensor:
"""Apply the appropriate pre-processing to a single condition tensor.
Exactly one of ``do_patch`` / ``do_interp`` may be ``True``, or both
``False`` (default: repeat along batch dimension).
"""
if do_patch and do_interp:
raise ValueError(
"condition_patch and condition_interp cannot both be True "
"for the same condition key. Use one or the other."
)
if (do_patch or do_interp) and tensor.ndim != 4:
raise ValueError(
f"condition_patch={do_patch} or "
f"condition_interp={do_interp} requires a 4D tensor "
f"(B, C, H, W), got {tensor.ndim}D."
)
# Default case: no patching needed, just repeat along the batch dim.
if not do_patch and not do_interp:
return tensor.repeat(P, *([1] * (tensor.ndim - 1)))
# Both patch and interp need an active patching strategy.
patching = self._patching
if patching is None:
raise RuntimeError(
"No patching strategy set. Call set_random_patching() "
"or set_grid_patching() first."
)
if do_patch:
return patching.apply(tensor)
# do_interp case
Hp, Wp = patching.patch_shape
tensor = F.interpolate(tensor, size=(Hp, Wp), mode="bilinear")
return tensor.repeat(P, 1, 1, 1)
def _inject_patched_pos_embd(
self,
condition: Tensor | TensorDict | None,
pos_embd_patched: Float[Tensor, "P_times_B C_PE Hp Wp"],
PB: int,
) -> TensorDict:
"""Inject an already-patched positional embedding into the (possibly
already-patched) condition under the ``"positional_embedding"`` key.
Common logic factored out of :meth:`forward` so it can be reused by
:class:`~physicsnemo.diffusion.multi_diffusion.MultiDiffusionPredictor`.
When ``condition`` is a ``TensorDict`` it is mutated in place for
efficiency; otherwise a new ``TensorDict`` is built.
"""
if condition is None:
return TensorDict(
{"positional_embedding": pos_embd_patched},
batch_size=[PB],
)
if isinstance(condition, TensorDict):
condition["positional_embedding"] = pos_embd_patched
return condition
if isinstance(condition, Tensor):
return TensorDict(
{"condition": condition, "positional_embedding": pos_embd_patched},
batch_size=[PB],
)
raise ValueError(
"When positional embeddings are configured, condition must be a "
f"Tensor, TensorDict, or None, got {type(condition).__name__}."
)