# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from abc import ABC, abstractmethod from collections import defaultdict from contextlib import nullcontext from dataclasses import dataclass, fields from typing import Any, Dict, List, Optional, Tuple, TypeVar, Union import omegaconf import torch import torch.nn as nn from torch.distributed import ProcessGroup def batch_mul(x, y): """Broadcast-multiply x by y, padding the shorter shape with trailing 1s.""" nd1, nd2 = x.ndim, y.ndim common = min(nd1, nd2) for axis in range(common): assert x.shape[axis] == y.shape[axis], f"Dimensions not equal at axis {axis}" if nd1 < nd2: x = x.reshape(x.shape + (1,) * (nd2 - nd1)) elif nd2 < nd1: y = y.reshape(y.shape + (1,) * (nd1 - nd2)) return x * y from pid._ext.imaginaire.lazy_config import instantiate from pid._ext.imaginaire.utils import log from pid._ext.imaginaire.utils.count_params import count_params, disabled_train from pid._src.utils.context_parallel import broadcast T = TypeVar("T", bound="BaseCondition") def broadcast_condition(condition: BaseCondition, process_group: Optional[ProcessGroup] = None) -> BaseCondition: """ Broadcast the condition from the minimum rank in the specified group(s). """ if condition.is_broadcasted: return condition kwargs = condition.to_dict(skip_underscore=False) for key, value in kwargs.items(): if value is not None: kwargs[key] = broadcast(value, process_group) kwargs["_is_broadcasted"] = True return type(condition)(**kwargs) @dataclass(frozen=True) class BaseCondition(ABC): """ Attributes: _is_broadcasted: Flag indicating if parallel broadcast splitting has been performed. This is an internal implementation detail. """ _is_broadcasted: bool = False def to_dict(self, skip_underscore: bool = True) -> Dict[str, Any]: """Converts the condition to a dictionary. Returns: Dictionary containing the condition's fields and values. """ # return {f.name: getattr(self, f.name) for f in fields(self) if not f.name.startswith("_")} return {f.name: getattr(self, f.name) for f in fields(self) if not (f.name.startswith("_") and skip_underscore)} @property def is_broadcasted(self) -> bool: return self._is_broadcasted def broadcast(self, process_group: torch.distributed.ProcessGroup) -> BaseCondition: """Broadcasts and splits the condition across the checkpoint parallelism group. For most condition, such asT2VCondition, we do not need split. Args: process_group: The process group for broadcast and split Returns: A new BaseCondition instance with the broadcasted and split condition. """ if self.is_broadcasted: return self return broadcast_condition(self, process_group) @dataclass(frozen=True) class PixelDiTCondition(BaseCondition): """Condition for PixelDiT T2I models. caption: list[str] — raw caption strings (after dropout). The model's internal text encoder (e.g. Gemma-2-2b-it) handles encoding. """ caption: Optional[list] = None @dataclass(frozen=True) class PidCondition(BaseCondition): """Condition for PID (PixelDiT SR) models. caption: list[str] — raw caption strings (after dropout). lq_video_or_image: [B, 3, H_lq, W_lq] — LQ image at original low resolution. lq_latent: [B, z_dim, zH, zW] — LQ VAE latent. """ caption: Optional[list] = None lq_video_or_image: Optional[torch.Tensor] = None lq_latent: Optional[torch.Tensor] = None class AbstractEmbModel(nn.Module): def __init__(self): super().__init__() self._is_trainable = None self._dropout_rate = None self._input_key = None self._return_dict = False @property def is_trainable(self) -> bool: return self._is_trainable @property def dropout_rate(self) -> Union[float, torch.Tensor]: return self._dropout_rate @property def input_key(self) -> str: return self._input_key @property def is_return_dict(self) -> bool: return self._return_dict @is_trainable.setter def is_trainable(self, value: bool): self._is_trainable = value @dropout_rate.setter def dropout_rate(self, value: Union[float, torch.Tensor]): self._dropout_rate = value @input_key.setter def input_key(self, value: str): self._input_key = value @is_return_dict.setter def is_return_dict(self, value: bool): self._return_dict = value @is_trainable.deleter def is_trainable(self): del self._is_trainable @dropout_rate.deleter def dropout_rate(self): del self._dropout_rate @input_key.deleter def input_key(self): del self._input_key @is_return_dict.deleter def is_return_dict(self): del self._return_dict def random_dropout_input( self, in_tensor: torch.Tensor, dropout_rate: Optional[float] = None, key: Optional[str] = None ) -> torch.Tensor: del key dropout_rate = dropout_rate if dropout_rate is not None else self.dropout_rate return batch_mul( torch.bernoulli((1.0 - dropout_rate) * torch.ones(in_tensor.shape[0])).type_as(in_tensor), in_tensor, ) def details(self) -> str: return "" def summary(self) -> str: input_key = self.input_key if self.input_key is not None else getattr(self, "input_keys", None) return ( f"{self.__class__.__name__} \n\tinput key: {input_key}" f"\n\tParam count: {count_params(self, False)} \n\tTrainable: {self.is_trainable}" f"\n\tDropout rate: {self.dropout_rate}" f"\n\t{self.details()}" ) class CaptionStringDrop(AbstractEmbModel): """Embedder for raw caption strings with dropout (replaces with empty string). Unlike TextAttrEmptyStringDrop which operates on pre-computed tensor embeddings, this embedder handles raw caption strings (list[str]) from the data batch. On dropout, the caption is replaced with an empty string so the model's own text encoder produces null embeddings. Used by PixelDiT which encodes text inside the model (Gemma-2-2b-it) rather than consuming pre-computed UMT5 embeddings from the dataset. Args: input_key: key in data_batch containing caption strings (default: "caption") output_key: key in condition output (default: "caption") dropout_rate: probability of replacing caption with "" (for CFG training) """ def __init__(self, input_key: str = "caption", output_key: str = "caption", dropout_rate: float = 0.0): super().__init__() self._input_key = input_key self._dropout_rate = dropout_rate self._output_key = output_key def forward(self, captions): # Ensure list[str] — random_dropout_input normalizes, but guard forward too if isinstance(captions, str): captions = [captions] return {self._output_key: captions} def random_dropout_input(self, in_data, dropout_rate=None, key=None): """Per-sample caption dropout: replace each caption with "" independently.""" del key import random as _random if in_data is None: return in_data # Normalize: webdataset collate may return a single string when batch_size=1 if isinstance(in_data, str): in_data = [in_data] dropout_rate = dropout_rate if dropout_rate is not None else self.dropout_rate if dropout_rate <= 0: return in_data return ["" if _random.random() < dropout_rate else cap for cap in in_data] def details(self) -> str: return f"Output key: [{self._output_key}]" class GeneralConditioner(nn.Module, ABC): """ An abstract module designed to handle various embedding models with conditional and unconditional configurations. This abstract base class initializes and manages a collection of embedders that can dynamically adjust their dropout rates based on conditioning. Attributes: KEY2DIM (dict): A mapping from output keys to dimensions used for concatenation. embedders (nn.ModuleDict): A dictionary containing all embedded models initialized and configured based on the provided configurations. Parameters: emb_models (Union[List, Any]): A dictionary where keys are embedder names and values are configurations for initializing the embedders. Example: See Edify4ConditionerConfig """ KEY2DIM = {"crossattn_emb": 1} def __init__(self, **emb_models: Union[List, Any]): super().__init__() self.embedders = nn.ModuleDict() for n, (emb_name, emb_config) in enumerate(emb_models.items()): embedder = instantiate(emb_config) # assert isinstance( # embedder, AbstractEmbModel # ), f"embedder model {embedder.__class__.__name__} has to inherit from AbstractEmbModel" embedder.is_trainable = getattr(emb_config, "is_trainable", True) embedder.dropout_rate = getattr(emb_config, "dropout_rate", 0.0) if not embedder.is_trainable: embedder.train = disabled_train for param in embedder.parameters(): param.requires_grad = False embedder.eval() log.info(f"Initialized embedder #{n}-{emb_name}: \n {embedder.summary()}") self.embedders[emb_name] = embedder @abstractmethod def forward( self, batch: Dict, override_dropout_rate: Optional[Dict[str, float]] = None, ) -> Any: """Should be implemented in subclasses to handle conditon datatype""" raise NotImplementedError def _forward( self, batch: Dict, override_dropout_rate: Optional[Dict[str, float]] = None, ) -> Dict: """ Processes the input batch through all configured embedders, applying conditional dropout rates if specified. Output tensors for each key are concatenated along the dimensions specified in KEY2DIM. Parameters: batch (Dict): The input data batch to process. override_dropout_rate (Optional[Dict[str, float]]): Optional dictionary to override default dropout rates per embedder key. Returns: Dict: A dictionary of output tensors concatenated by specified dimensions. Note: In case the network code is sensitive to the order of concatenation, you can either control the order via \ config file or make sure the embedders return a unique key for each output. """ output = defaultdict(list) if override_dropout_rate is None: override_dropout_rate = {} # make sure emb_name in override_dropout_rate is valid for emb_name in override_dropout_rate.keys(): assert emb_name in self.embedders, f"invalid name found {emb_name}" for emb_name, embedder in self.embedders.items(): embedding_context = nullcontext if embedder.is_trainable else torch.no_grad with embedding_context(): if isinstance(embedder.input_key, str): emb_out = embedder( embedder.random_dropout_input( batch[embedder.input_key], override_dropout_rate.get(emb_name, None) ) ) elif isinstance(embedder.input_key, (list, omegaconf.listconfig.ListConfig)): emb_out = embedder( *[ embedder.random_dropout_input(batch.get(k), override_dropout_rate.get(emb_name, None), k) for k in embedder.input_key ] ) else: raise KeyError( f"Embedder '{embedder.__class__.__name__}' requires an 'input_key' attribute to be defined as either a string or list of strings" ) for k, v in emb_out.items(): output[k].append(v) # Concatenate the outputs return {k: torch.cat(v, dim=self.KEY2DIM.get(k, -1)) for k, v in output.items()} def get_condition_uncondition( self, data_batch: Dict, ) -> Tuple[Any, Any]: """ Processes the provided data batch to generate two sets of outputs: conditioned and unconditioned. This method manipulates the dropout rates of embedders to simulate two scenarios — one where all conditions are applied (conditioned), and one where they are removed or reduced to the minimum (unconditioned). This method first sets the dropout rates to zero for the conditioned scenario to fully apply the embedders' effects. For the unconditioned scenario, it sets the dropout rates to 1 (or to 0 if the initial unconditional dropout rate is insignificant) to minimize the embedders' influences, simulating an unconditioned generation. Parameters: data_batch (Dict): The input data batch that contains all necessary information for embedding processing. The data is expected to match the required format and keys expected by the embedders. Returns: Tuple[Any, Any]: A tuple containing two condition: - The first one contains the outputs with all embedders fully applied (conditioned outputs). - The second one contains the outputs with embedders minimized or not applied (unconditioned outputs). """ cond_dropout_rates, dropout_rates = {}, {} for emb_name, embedder in self.embedders.items(): cond_dropout_rates[emb_name] = 0.0 dropout_rates[emb_name] = 1.0 if embedder.dropout_rate > 1e-4 else 0.0 condition: Any = self(data_batch, override_dropout_rate=cond_dropout_rates) un_condition: Any = self(data_batch, override_dropout_rate=dropout_rates) return condition, un_condition class PixelDiTConditioner(GeneralConditioner): """Conditioner for PixelDiT T2I models. Returns PixelDiTCondition. Unlike FPDConditioner which works with pre-computed tensor embeddings, this conditioner handles raw caption strings. The model's internal text encoder does the actual encoding after conditioning. Overrides _forward to skip torch.cat (caption outputs are list[str], not tensors). Embedders typically include: - caption: CaptionStringDrop (raw string with empty-string dropout for CFG) """ def _forward( self, batch: Dict, override_dropout_rate: Optional[Dict[str, float]] = None, ) -> Dict: """Like GeneralConditioner._forward but returns values directly (no torch.cat).""" output = {} if override_dropout_rate is None: override_dropout_rate = {} for emb_name, embedder in self.embedders.items(): embedding_context = nullcontext if embedder.is_trainable else torch.no_grad with embedding_context(): in_data = batch[embedder.input_key] in_data = embedder.random_dropout_input(in_data, override_dropout_rate.get(emb_name, None)) emb_out = embedder(in_data) output.update(emb_out) return output def forward( self, batch: Dict, override_dropout_rate: Optional[Dict[str, float]] = None, ) -> PixelDiTCondition: output = self._forward(batch, override_dropout_rate) return PixelDiTCondition(**output) def get_condition_uncondition(self, data_batch: Dict) -> Tuple[PixelDiTCondition, PixelDiTCondition]: """Returns (condition, uncondition) pair for CFG inference.""" condition = self(data_batch, override_dropout_rate={n: 0.0 for n in self.embedders}) uncondition = self(data_batch, override_dropout_rate={n: 1.0 for n in self.embedders}) return condition, uncondition # ============================================================================= # PID (PixelDiT SR) — condition, embedder, and conditioner # ============================================================================= class LQTensorDrop(AbstractEmbModel): """Embedder for LQ tensors (image or latent) with per-sample zero dropout. On dropout, the tensor is replaced with a zero tensor of the same shape. Supports coupled dropout: when coupled_with is set, this embedder reuses the dropout mask from the coupled embedder (stored in _shared_lq_keep_mask). Args: input_key: key in data_batch (e.g. "LQ_video_or_image" or "LQ_latent"). output_key: key in condition output (e.g. "lq_video_or_image" or "lq_latent"). dropout_rate: probability of zeroing out the tensor (for CFG training). is_primary: if True, this embedder generates the shared dropout mask. If False, it reuses the mask from the primary embedder. """ # Class-level shared mask for coupled dropout (reset each forward pass) _shared_lq_keep_mask: Optional[torch.Tensor] = None def __init__( self, input_key: str = "LQ_video_or_image", output_key: str = "lq_video_or_image", dropout_rate: float = 0.0, is_primary: bool = True, ): super().__init__() self._input_key = input_key self._dropout_rate = dropout_rate self._output_key = output_key self._is_primary = is_primary def forward(self, element: torch.Tensor) -> Dict[str, torch.Tensor]: return {self._output_key: element} def random_dropout_input( self, in_tensor: torch.Tensor, dropout_rate: Optional[float] = None, key: Optional[str] = None ) -> torch.Tensor: del key dropout_rate = dropout_rate if dropout_rate is not None else self.dropout_rate if dropout_rate <= 0 or in_tensor is None: if self._is_primary: LQTensorDrop._shared_lq_keep_mask = None return in_tensor B = in_tensor.shape[0] if self._is_primary: # Generate and store shared mask keep_mask = torch.bernoulli((1.0 - dropout_rate) * torch.ones(B, device=in_tensor.device)) LQTensorDrop._shared_lq_keep_mask = keep_mask else: # Reuse mask from primary embedder keep_mask = LQTensorDrop._shared_lq_keep_mask if keep_mask is None: # Fallback: generate own mask if primary hasn't run yet keep_mask = torch.bernoulli((1.0 - dropout_rate) * torch.ones(B, device=in_tensor.device)) keep_mask_expanded = keep_mask.view(B, *[1] * (in_tensor.dim() - 1)).type_as(in_tensor) return keep_mask_expanded * in_tensor def details(self) -> str: return f"Output key: {self._output_key}, primary: {self._is_primary}" class PidConditioner(PixelDiTConditioner): """Conditioner for PID (PixelDiT SR) models. Returns PidCondition. Handles caption strings (CaptionStringDrop) + LQ tensors (LQTensorDrop). LQ image and LQ latent share coupled dropout: when one is dropped, both are. Inherits get_condition_uncondition from GeneralConditioner which respects per-embedder dropout_rate: if caption dropout_rate=0, caption is never dropped in uncondition (only LQ gets dropped for CFG). Embedders typically include: - caption: CaptionStringDrop (raw string dropout) - lq_video_or_image: LQTensorDrop (primary, generates shared mask) - lq_latent: LQTensorDrop (secondary, reuses shared mask) """ def _forward( self, batch: Dict, override_dropout_rate: Optional[Dict[str, float]] = None, ) -> Dict: """Process embedders. Handles both string (caption) and tensor (LQ) outputs.""" output = {} if override_dropout_rate is None: override_dropout_rate = {} # Reset shared mask at start of each forward LQTensorDrop._shared_lq_keep_mask = None for emb_name, embedder in self.embedders.items(): embedding_context = nullcontext if embedder.is_trainable else torch.no_grad with embedding_context(): in_data = batch[embedder.input_key] in_data = embedder.random_dropout_input(in_data, override_dropout_rate.get(emb_name, None)) emb_out = embedder(in_data) output.update(emb_out) return output def forward( self, batch: Dict, override_dropout_rate: Optional[Dict[str, float]] = None, ) -> PidCondition: output = self._forward(batch, override_dropout_rate) return PidCondition(**output) def get_condition_uncondition(self, data_batch: Dict) -> Tuple[PidCondition, PidCondition]: """Returns (condition, uncondition) pair for CFG inference. Respects per-embedder dropout_rate: embedders with dropout_rate=0 in config are NOT dropped in uncondition (e.g. caption with dropout_rate=0 stays). """ cond_dropout_rates, uncond_dropout_rates = {}, {} for emb_name, embedder in self.embedders.items(): cond_dropout_rates[emb_name] = 0.0 uncond_dropout_rates[emb_name] = 1.0 if embedder.dropout_rate > 1e-4 else 0.0 condition = self(data_batch, override_dropout_rate=cond_dropout_rates) uncondition = self(data_batch, override_dropout_rate=uncond_dropout_rates) return condition, uncondition