Source code for physicsnemo.models.dit.dit

# 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.

import warnings
from dataclasses import dataclass
from typing import Any, Dict, Literal, Optional, Tuple, Union

import torch
import torch.nn as nn
import torch.nn.functional as F
from jaxtyping import Float

from physicsnemo.core.meta import ModelMetaData
from physicsnemo.core.module import Module
from physicsnemo.nn import (
    ConditioningEmbedder,
    ConditioningEmbedderType,
    DetokenizerModuleBase,
    DiTBlock,
    RotaryEmbedding2DTables,
    TokenizerModuleBase,
    get_conditioning_embedder,
    get_detokenizer,
    get_tokenizer,
)


@dataclass
class MetaData(ModelMetaData):
    # Optimization
    jit: bool = False
    cuda_graphs: bool = False
    amp_cpu: bool = False
    amp_gpu: bool = True
    torch_fx: bool = False
    # Data type
    bf16: bool = True
    # Inference
    onnx: bool = False
    # Physics informed
    func_torch: bool = False
    auto_grad: bool = False


[docs] class DiT(Module): r""" The Diffusion Transformer (DiT) model. Parameters ---------- input_size : Union[int, Tuple[int]] Spatial dimensions of the input. If an integer is provided, the input is assumed to be on a square 2D domain. If a tuple is provided, the input is assumed to be on a multi-dimensional domain. in_channels : int The number of input channels. patch_size : Union[int, Tuple[int]], optional, default=(8, 8) The size of each image patch. If an integer is provided, a square 2D patch is assumed. If a tuple is provided, a multi-dimensional patch is assumed. tokenizer : Union[Literal["patch_embed_2d", "hpx_patch_embed"], Module], optional, default="patch_embed_2d" The tokenizer to use. Either a string in ``{"patch_embed_2d", "hpx_patch_embed"}`` or an instantiated PhysicsNeMo :class:`~physicsnemo.core.Module` implementing :class:`~physicsnemo.nn.TokenizerModuleBase`, with forward accepting input of shape :math:`(B, C, *\text{spatial\_dims})` and returning :math:`(B, L, D)`. detokenizer : Union[Literal["proj_reshape_2d", "proj_reshape_2d_conv", "hpx_patch_detokenizer"], Module], optional, default="proj_reshape_2d" The detokenizer to use. Either a string in ``{"proj_reshape_2d", "proj_reshape_2d_conv", "hpx_patch_detokenizer"}`` or an instantiated PhysicsNeMo :class:`~physicsnemo.core.Module` implementing :class:`~physicsnemo.nn.DetokenizerModuleBase`, with forward accepting :math:`(B, L, D)` and :math:`(B, D)` and returning :math:`(B, C, *\text{spatial\_dims})`. out_channels : Union[None, int], optional, default=None The number of output channels. If ``None``, set to ``in_channels``. hidden_size : int, optional, default=384 The dimensionality of the transformer embeddings. depth : int, optional, default=12 The number of transformer blocks. num_heads : int, optional, default=8 The number of attention heads. mlp_ratio : float, optional, default=4.0 The ratio of the MLP hidden dimension to the embedding dimension. attention_backend : Literal["timm", "transformer_engine", "natten2d", "natten2d_rope"], optional, default="timm" The attention backend to use. See :class:`~physicsnemo.nn.DiTBlock` for a description of each built-in backend. ``"natten2d_rope"`` applies axial 2D rotary position embeddings inside NATTEN; selecting it forces ``pos_embed="none"`` in the tokenizer (additive positional embedding is disabled to avoid double-counting position) and emits a warning if a conflicting ``pos_embed`` was explicitly passed. layernorm_backend : Literal["apex", "torch"], optional, default="torch" If ``"apex"``, uses FusedLayerNorm from apex. If ``"torch"``, uses :class:`torch.nn.LayerNorm`. Also passed to :class:`~physicsnemo.nn.Natten2DSelfAttention` when ``qk_norm=True``. condition_dim : int, optional, default=None Dimensionality of conditioning. If ``None``, the model is unconditional. dit_initialization : bool, optional, default=True If ``True``, applies DiT-specific initialization. conditioning_embedder : Literal["dit", "edm", "zero"] or ConditioningEmbedder, optional, default="dit" The conditioning embedder type or an instantiated :class:`~physicsnemo.nn.ConditioningEmbedder`. conditioning_embedder_kwargs : Dict[str, Any], optional, default={} Additional keyword arguments for the conditioning embedder. tokenizer_kwargs : Dict[str, Any], optional, default={} Additional keyword arguments for the tokenizer module. detokenizer_kwargs : Dict[str, Any], optional, default={} Additional keyword arguments for the detokenizer module. block_kwargs : Dict[str, Any], optional, default={} Additional keyword arguments for the DiTBlock modules. attn_kwargs : Dict[str, Any], optional, default={} Additional keyword arguments for the attention module constructor (e.g. ``na2d_kwargs`` when using ``attention_backend="natten2d"``). drop_path_rates : list[float], optional, default=None DropPath (stochastic depth) rates, one per block. Must have length equal to ``depth``. If ``None``, no drop path is applied. force_tokenization_fp32 : bool, optional, default=False If ``True``, forces tokenization and de-tokenization to run in fp32. use_nan_mask_tokens : bool, optional, default=False If ``True``, every NATTEN block overwrites invalid spatial tokens with a per-block learned ``mask_token`` immediately before the QKV projection, so the neighborhood window mixes in a single learned feature instead of corrupted (e.g. NaN-padded) signal. Requires a NATTEN attention backend (``"natten2d"`` or ``"natten2d_rope"``). This only allocates the learned ``mask_token`` parameters; the invalid pattern itself is supplied dynamically per forward call via the ``invalid_mask`` argument (see :meth:`forward`). When no ``invalid_mask`` is passed, all tokens are treated as valid and behavior is identical to ``use_nan_mask_tokens=False``. Forward ------- x : torch.Tensor Spatial inputs of shape :math:`(N, C, *\text{spatial\_dims})`. ``spatial_dims`` is determined by ``input_size``. t : torch.Tensor Diffusion timesteps of shape :math:`(N,)`. condition : Optional[torch.Tensor] Conditions of shape :math:`(N, d)`. p_dropout : Optional[Union[float, torch.Tensor]], optional Dropout probability for the intermediate dropout (pre-attention) in each DiTBlock. If ``None``, no dropout. If a scalar, same for all samples; if a tensor, shape :math:`(B,)` for per-sample dropout. attn_kwargs : Dict[str, Any], optional Additional keyword arguments passed to the attention module's forward method. tokenizer_kwargs : Dict[str, Any], optional Additional keyword arguments passed to the tokenizer's forward method. invalid_mask : Optional[torch.Tensor], optional Per-sample boolean (or float) invalid-region mask of shape :math:`(N, *\text{spatial\_dims})` or :math:`(N, 1, *\text{spatial\_dims})`, ``True`` (or ``1``) at invalid pixels (e.g. NaN-padded / outside sensor coverage). It is max-pooled to patch (token) granularity and the flagged tokens are replaced by each NATTEN block's learned ``mask_token`` before attention. The pattern may differ per sample (dynamic, batch-variable masking) and per forward call. Requires ``use_nan_mask_tokens=True``. Because the splice does not sanitize non-finite values, invalid pixels in ``x`` must be finite (e.g. pass ``x`` through :func:`torch.nan_to_num` first). Under domain parallelism, pass ``invalid_mask`` as a ``ShardTensor`` sharded along height exactly like ``x``. Outputs ------- torch.Tensor Output tensor of shape :math:`(N, \text{out\_channels}, *\text{spatial\_dims})`. Notes ----- Reference: Peebles, W., & Xie, S. (2023). Scalable diffusion models with transformers. In Proceedings of the IEEE/CVF International Conference on Computer Vision (pp. 4195-4205). Under domain parallelism (the model wrapped with ``distribute_module``), the spatial input ``x`` is a sharded ``ShardTensor`` while the model's buffers and parameters are ``DTensor``s. The non-spatial inputs ``t`` and ``condition`` must therefore be passed as ``Replicate`` ``DTensor``s on the same mesh (rather than plain tensors), so they compose with the distributed buffers/parameters (e.g. the timestep embedder's ``freqs``). Examples -------- >>> model = DiT( ... input_size=(32, 64), ... patch_size=4, ... in_channels=3, ... out_channels=3, ... condition_dim=8, ... ) >>> x = torch.randn(2, 3, 32, 64) >>> t = torch.randint(0, 1000, (2,)) >>> condition = torch.randn(2, 8) >>> output = model(x, t, condition) >>> output.shape torch.Size([2, 3, 32, 64]) """ __model_checkpoint_version__ = "0.2.0" __supported_model_checkpoint_version__ = { "0.1.0": "Automatically converting legacy DiT checkpoint timestep / conditioning embedder arguments.", } @classmethod def _backward_compat_arg_mapper( cls, version: str, args: Dict[str, Any] ) -> Dict[str, Any]: r""" Map arguments from legacy checkpoints to the current format. Parameters ---------- version : str Version of the checkpoint being loaded. args : Dict[str, Any] Arguments dictionary from the checkpoint. Returns ------- Dict[str, Any] Updated arguments dictionary compatible with the current version. """ args = super()._backward_compat_arg_mapper(version, args) if version != "0.1.0": return args if "timestep_embed_kwargs" in args: args["conditioning_embedder_kwargs"] = args.pop("timestep_embed_kwargs") return args def __init__( self, input_size: Union[int, Tuple[int]], in_channels: int, patch_size: Union[int, Tuple[int]] = (8, 8), tokenizer: Union[ Literal["patch_embed_2d", "hpx_patch_embed"], Module ] = "patch_embed_2d", detokenizer: Union[ Literal["proj_reshape_2d", "proj_reshape_2d_conv", "hpx_patch_detokenizer"], Module, ] = "proj_reshape_2d", out_channels: Optional[int] = None, hidden_size: int = 384, depth: int = 12, num_heads: int = 8, mlp_ratio: float = 4.0, attention_backend: Literal[ "timm", "transformer_engine", "natten2d", "natten2d_rope" ] = "timm", layernorm_backend: Literal["apex", "torch"] = "torch", condition_dim: Optional[int] = None, conditioning_embedder: Literal["dit", "edm", "zero"] | ConditioningEmbedder = "dit", dit_initialization: Optional[int] = True, conditioning_embedder_kwargs: Dict[str, Any] = {}, tokenizer_kwargs: Dict[str, Any] = {}, detokenizer_kwargs: Dict[str, Any] = {}, block_kwargs: Dict[str, Any] = {}, attn_kwargs: Dict[str, Any] = {}, drop_path_rates: list[float] | None = None, force_tokenization_fp32: bool = False, use_nan_mask_tokens: bool = False, ): super().__init__(meta=MetaData()) self.input_size = ( input_size if isinstance(input_size, (tuple, list)) else (input_size, input_size) ) self.in_channels = in_channels if out_channels: self.out_channels = out_channels else: self.out_channels = in_channels self.patch_size = ( patch_size if isinstance(patch_size, (tuple, list)) else (patch_size, patch_size) ) self.num_heads = num_heads self.condition_dim = condition_dim # Input validation if attention_backend not in [ "timm", "transformer_engine", "natten2d", "natten2d_rope", ]: raise ValueError( "attention_backend must be one of 'timm', 'transformer_engine', 'natten2d', 'natten2d_rope'" ) if layernorm_backend not in ["apex", "torch"]: raise ValueError("layernorm_backend must be one of 'apex', 'torch'") is_natten = attention_backend in ("natten2d", "natten2d_rope") # Only NATTEN uses the latent grid; other backends may pass a non-2D input_size if is_natten: self._latent_h = self.input_size[0] // self.patch_size[0] self._latent_w = self.input_size[1] // self.patch_size[1] latent_hw = (self._latent_h, self._latent_w) else: self._latent_h = self._latent_w = None latent_hw = None # Keyword arguments threaded into every attention module's forward. if is_natten: self.attn_kwargs_forward = {"latent_hw": latent_hw} else: self.attn_kwargs_forward = {} # NaN-mask-token handling: replace invalid spatial tokens with a learned # per-block mask token before NATTEN. Only valid with a NATTEN backend. self._use_nan_mask_tokens = use_nan_mask_tokens if use_nan_mask_tokens and not is_natten: raise ValueError( "use_nan_mask_tokens=True requires a NATTEN attention backend " "('natten2d' or 'natten2d_rope')" ) # Constructor-time attention kwargs (copied so the caller's dict is not # mutated). The mask-token backends need to allocate their learned mask # parameter. RoPE tables are not built per block: the model owns a single # RotaryEmbedding2DTables provider (below) and passes its tables into # every block's forward, so the tables are built, stored, and — under # domain parallelism — sharded exactly once for the whole model. attn_kwargs = dict(attn_kwargs) self._is_rope = attention_backend == "natten2d_rope" self.rope = None if self._is_rope: rope_theta = attn_kwargs.pop("rope_theta", 10000.0) self.rope = RotaryEmbedding2DTables( head_dim=hidden_size // num_heads, latent_hw=latent_hw, theta=rope_theta, ) if use_nan_mask_tokens: attn_kwargs["use_mask_token"] = True # Using RoPE alongside an additive positional embedding double-counts # position. Force the (patch-based) tokenizer's pos_embed to "none", # warning if a conflicting value was explicitly requested. if attention_backend == "natten2d_rope" and tokenizer == "patch_embed_2d": tokenizer_kwargs = dict(tokenizer_kwargs) requested_pos_embed = tokenizer_kwargs.get("pos_embed", None) if requested_pos_embed not in (None, "none"): warnings.warn( "attention_backend='natten2d_rope' uses rotary position " "embeddings; overriding the requested " f"pos_embed={requested_pos_embed!r} with 'none' to avoid " "double-counting the positional signal.", UserWarning, stacklevel=2, ) tokenizer_kwargs["pos_embed"] = "none" if isinstance(tokenizer, str) and tokenizer not in [ "patch_embed_2d", "hpx_patch_embed", ]: raise ValueError("tokenizer must be 'patch_embed_2d' or 'hpx_patch_embed'") if isinstance(detokenizer, str) and detokenizer not in [ "proj_reshape_2d", "proj_reshape_2d_conv", "hpx_patch_detokenizer", ]: raise ValueError( "detokenizer must be 'proj_reshape_2d', 'proj_reshape_2d_conv', or 'hpx_patch_detokenizer'" ) # Tokenizer module: accept string or pre-instantiated PhysicsNeMo Module if isinstance(tokenizer, str): self.tokenizer = get_tokenizer( input_size=self.input_size, patch_size=self.patch_size, in_channels=in_channels, hidden_size=hidden_size, tokenizer=tokenizer, **tokenizer_kwargs, ) else: if not isinstance(tokenizer, TokenizerModuleBase): raise TypeError( "tokenizer must be a string or a physicsnemo.core.Module instance subclassing physicsnemo.nn.TokenizerModuleBase" ) self.tokenizer = tokenizer # Conditioning embedder: accept enum or pre-instantiated Module if isinstance(conditioning_embedder, str): self.conditioning_embedder = get_conditioning_embedder( ConditioningEmbedderType[conditioning_embedder.upper()], hidden_size=hidden_size, condition_dim=condition_dim or 0, amp_mode=self.meta.amp_gpu, **conditioning_embedder_kwargs, ) else: if not isinstance(conditioning_embedder, ConditioningEmbedder): raise TypeError( "conditioning_embedder must be a ConditioningEmbedderType or a Module implementing the ConditioningEmbedder protocol" ) self.conditioning_embedder = conditioning_embedder # Detokenizer module: accept string or pre-instantiated PhysicsNeMo Module if isinstance(detokenizer, str): self.detokenizer = get_detokenizer( input_size=self.input_size, patch_size=self.patch_size, out_channels=self.out_channels, hidden_size=hidden_size, layernorm_backend=layernorm_backend, detokenizer=detokenizer, **detokenizer_kwargs, ) else: if not isinstance(detokenizer, DetokenizerModuleBase): raise TypeError( "detokenizer must be a string or a physicsnemo.core.Module instance subclassing physicsnemo.nn.DetokenizerModuleBase" ) self.detokenizer = detokenizer # Validate drop_path_rates if drop_path_rates is None: drop_path_rates = [0.0] * depth else: if len(drop_path_rates) != depth: raise ValueError( f"drop_path_rates length ({len(drop_path_rates)}) must match DiT depth ({depth})" ) self.blocks = nn.ModuleList( [ DiTBlock( hidden_size, num_heads, attention_backend=attention_backend, layernorm_backend=layernorm_backend, mlp_ratio=mlp_ratio, drop_path=drop_path_rates[i], condition_embed_dim=self.conditioning_embedder.output_dim, **block_kwargs, **attn_kwargs, ) for i in range(depth) ] ) if dit_initialization: self.initialize_weights() self.force_tokenization_fp32 = force_tokenization_fp32 self.register_load_state_dict_pre_hook(self._migrate_legacy_checkpoint) @staticmethod def _migrate_legacy_checkpoint( module, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs, ): r"""Remap legacy state_dict keys where timestep embedder was at root. Previous versions stored the timestep embedder at root (e.g. ``t_embedder.mlp.0.weight``). The current model nests it under ``conditioning_embedder`` (e.g. ``conditioning_embedder.t_embedder.mlp.0.weight``). This pre-hook rewrites those keys in-place so loading succeeds. It also drops the positional embedding ``freqs`` key, which is not part of the state_dict anymore due to the usage of ``persistent=False``. Parameters ---------- module : torch.nn.Module The module being loaded (unused; required by ``register_load_state_dict_pre_hook``). state_dict : dict State dict being loaded; modified in-place. prefix : str Prefix for the module (unused). local_metadata : dict, optional Local metadata (unused). strict : bool Whether strict loading is requested (unused). missing_keys : list of str List of missing keys (unused). unexpected_keys : list of str List of unexpected keys (unused). error_msgs : list of str Error messages (unused). Returns ------- None Modifies ``state_dict`` in-place; no return value. """ legacy_prefix = "t_embedder." new_prefix = "conditioning_embedder.t_embedder." # Iterate over a snapshot of keys to avoid mutating dict while iterating for old_key in list(state_dict.keys()): if not old_key.startswith(legacy_prefix): continue new_key = new_prefix + old_key[len(legacy_prefix) :] if old_key == legacy_prefix + "freqs": del state_dict[old_key] elif new_key not in state_dict: state_dict[new_key] = state_dict.pop(old_key)
[docs] def initialize_weights(self): r"""Apply DiT-specific weight initialization. Applies Xavier uniform to linear layers, then delegates to tokenizer, detokenizer, and each block's ``initialize_weights``. Parameters ---------- None Uses ``self`` (module state). Returns ------- None Modifies module parameters in-place. """ # Apply a basic Xavier uniform initialization to all linear layers. def _basic_init(module): if isinstance(module, nn.Linear): torch.nn.init.xavier_uniform_(module.weight) if module.bias is not None: nn.init.constant_(module.bias, 0) self.apply(_basic_init) # Delegate custom weight initialization to the tokenizer, detokenizer, and blocks self.tokenizer.initialize_weights() self.detokenizer.initialize_weights() for block in self.blocks: block.initialize_weights()
def _pixel_mask_to_token_mask( self, invalid_mask: torch.Tensor, ) -> Float[torch.Tensor, "batch sequence"]: r"""Reduce a per-sample pixel-level invalid mask to token granularity. Aggregates an invalid-pixel mask of shape :math:`(B, H, W)` or :math:`(B, 1, H, W)` to a flattened patch-level mask of shape :math:`(B, L)` with ``L = h_lat * w_lat``: a patch (token) is marked invalid if *any* pixel in its ``patch_size`` block is invalid. The flattening order (row-major over ``(h_lat, w_lat)``) matches the tokenizer's ``flatten(2)`` token ordering, so the returned mask aligns positionally with the token sequence consumed by the NATTEN blocks. Implemented with :func:`torch.nn.functional.max_pool2d`, which is registered for ``ShardTensor``: with ``kernel_size == stride == patch_size`` the pooling is non-overlapping and stays local under height-sharded domain parallelism, mirroring how the tokenizer's strided convolution produces the sharded token sequence. Parameters ---------- invalid_mask : torch.Tensor Boolean/float mask, ``True`` (or ``>0``) at invalid pixels. Returns ------- torch.Tensor Boolean token mask of shape :math:`(B, L)`. """ if invalid_mask.ndim == len(self.input_size) + 1: # (B, *spatial) -> (B, 1, *spatial) invalid_mask = invalid_mask.unsqueeze(1) if invalid_mask.ndim != len(self.input_size) + 2 or invalid_mask.shape[1] != 1: raise ValueError( "invalid_mask must have shape (B, *spatial_dims) or " "(B, 1, *spatial_dims) matching the DiT spatial input; got " f"shape {tuple(invalid_mask.shape)}" ) # Any invalid pixel within a patch -> invalid token. Pool in float so # the op is well-defined; threshold back to bool afterwards. patch_mask = F.max_pool2d( invalid_mask.to(torch.float32), kernel_size=self.patch_size, stride=self.patch_size, ) # (B, 1, h_lat, w_lat) # (B, 1, h_lat, w_lat) -> (B, L). Row-major flatten matches the # tokenizer; under ShardTensor this merges the height shard with the # (replicated) width axis, exactly as the static-buffer path did. return (patch_mask > 0).reshape(patch_mask.shape[0], -1) def forward( self, x: Float[torch.Tensor, "batch in_channels *spatial_dims"], t: Float[torch.Tensor, " batch"], condition: Optional[Float[torch.Tensor, "batch condition_dim"]] = None, p_dropout: Optional[float | Float[torch.Tensor, " batch"]] = None, attn_kwargs: Dict[str, Any] = {}, tokenizer_kwargs: Dict[str, Any] = {}, invalid_mask: Optional[Float[torch.Tensor, " batch *spatial_dims"]] = None, ) -> Float[torch.Tensor, "batch out_channels *spatial_dims"]: if invalid_mask is not None and not self._use_nan_mask_tokens: raise ValueError( "invalid_mask was provided but the DiT was constructed with " "use_nan_mask_tokens=False, so no learned mask tokens were " "allocated. Rebuild the model with use_nan_mask_tokens=True to " "use dynamic invalid-region masking." ) # Tokenize: (B, C, H, W) -> (B, L, D) if self.force_tokenization_fp32: dtype = x.dtype x = x.to(torch.float32) with torch.autocast(device_type="cuda", enabled=False): x = self.tokenizer(x, **tokenizer_kwargs) x = x.to(dtype) else: x = self.tokenizer(x, **tokenizer_kwargs) # Compute conditioning embedding c = self.conditioning_embedder(t, condition=condition) # (B, D) block_attn_kwargs = {**self.attn_kwargs_forward, **attn_kwargs} if self._is_rope: # Fetch the shared RoPE tables once and pass them into every block's # forward. Only forward a latent_hw *override* explicitly supplied by # the caller (variable-resolution inference); the fixed # construction-time grid stays None so the provider returns its # prebuilt tables with no per-call shape comparison on the hot path. rope_cos, rope_sin = self.rope(attn_kwargs.get("latent_hw")) block_attn_kwargs["rope_cos"] = rope_cos block_attn_kwargs["rope_sin"] = rope_sin if invalid_mask is not None: # Reduce the (B, *spatial) pixel mask to a (B, L) token mask aligned # with the token sequence. Under domain parallelism invalid_mask is a # ShardTensor sharded along height like x, so the pooled token mask # is sharded along the sequence axis exactly like the tokens. block_attn_kwargs.setdefault( "invalid_token_mask", self._pixel_mask_to_token_mask(invalid_mask) ) for block in self.blocks: x = block( x, c, p_dropout=p_dropout, attn_kwargs=block_attn_kwargs, ) # (B, L, D) # De-tokenize: (B, L, D) -> (B, C, H, W) if self.force_tokenization_fp32: dtype = x.dtype x = x.to(torch.float32) with torch.autocast(device_type="cuda", enabled=False): x = self.detokenizer(x, c) x = x.to(dtype) else: x = self.detokenizer(x, c) return x