| """
|
| Upgraded UNet with self-attention + VQGAN latent-space wrapper.
|
|
|
| Two modes:
|
| 1. Pixel-space (no VQGAN): use BBDMUNet directly, same as before but with attention
|
| 2. Latent-space (with VQGAN): use LatentBBDM which wraps encode/decode around BBDMUNet
|
|
|
| Usage:
|
| # Pixel-space (drop-in replacement for SimpleUNet)
|
| model = SimpleUNet(in_channels=3, base_channels=64, image_size=256)
|
| noise_pred = model(x_t, x_T, t)
|
|
|
| # Latent-space (requires pretrained VQGAN)
|
| latent_model = LatentBBDM(
|
| unet_channels=64,
|
| latent_channels=4, # VQGAN latent dim (usually 3 or 4)
|
| vqgan_ckpt="path/to/vqgan.ckpt",
|
| vqgan_config="path/to/vqgan_config.yaml",
|
| )
|
| """
|
|
|
| import math
|
| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
|
|
|
|
|
|
|
|
|
|
|
|
| class SinusoidalPosEmb(nn.Module):
|
| def __init__(self, dim):
|
| super().__init__()
|
| self.dim = dim
|
|
|
| def forward(self, t):
|
| half = self.dim // 2
|
| emb = math.log(10000) / (half - 1)
|
| emb = torch.exp(torch.arange(half, device=t.device) * -emb)
|
| emb = t.float().unsqueeze(1) * emb.unsqueeze(0)
|
| return torch.cat([emb.sin(), emb.cos()], dim=-1)
|
|
|
|
|
| class ResBlock(nn.Module):
|
| """Residual block with timestep conditioning via addition."""
|
|
|
| def __init__(self, in_ch, out_ch, t_dim, dropout=0.0):
|
| super().__init__()
|
| self.norm1 = nn.GroupNorm(min(32, in_ch), in_ch)
|
| self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1)
|
| self.t_proj = nn.Linear(t_dim, out_ch)
|
| self.norm2 = nn.GroupNorm(min(32, out_ch), out_ch)
|
| self.dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
| self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1)
|
| self.skip = nn.Conv2d(in_ch, out_ch, 1) if in_ch != out_ch else nn.Identity()
|
|
|
| def forward(self, x, t_emb):
|
| h = self.conv1(F.silu(self.norm1(x)))
|
| h = h + self.t_proj(F.silu(t_emb)).unsqueeze(-1).unsqueeze(-1)
|
| h = self.conv2(self.dropout(F.silu(self.norm2(h))))
|
| return h + self.skip(x)
|
|
|
|
|
| class SelfAttention(nn.Module):
|
| """
|
| Multi-head self-attention for spatial feature maps.
|
| Applied at 16x16 and 32x32 resolutions to capture global context.
|
| """
|
|
|
| def __init__(self, channels, num_heads=4):
|
| super().__init__()
|
| self.channels = channels
|
| self.num_heads = num_heads
|
| assert channels % num_heads == 0
|
|
|
| self.norm = nn.GroupNorm(min(32, channels), channels)
|
| self.qkv = nn.Conv1d(channels, channels * 3, 1)
|
| self.proj = nn.Conv1d(channels, channels, 1)
|
| self.scale = (channels // num_heads) ** -0.5
|
|
|
| def forward(self, x):
|
| B, C, H, W = x.shape
|
| h = self.norm(x).view(B, C, H * W)
|
|
|
| qkv = self.qkv(h).view(B, 3, self.num_heads, C // self.num_heads, H * W)
|
| q, k, v = qkv[:, 0], qkv[:, 1], qkv[:, 2]
|
|
|
| q = q.permute(0, 1, 3, 2)
|
| k = k.permute(0, 1, 2, 3)
|
| v = v.permute(0, 1, 3, 2)
|
|
|
| attn = torch.matmul(q, k) * self.scale
|
| attn = attn.softmax(dim=-1)
|
| out = torch.matmul(attn, v)
|
| out = out.permute(0, 1, 3, 2).contiguous().view(B, C, H * W)
|
| out = self.proj(out).view(B, C, H, W)
|
|
|
| return x + out
|
|
|
|
|
| class CrossAttention(nn.Module):
|
| """
|
| Multi-head cross-attention for spatial feature maps.
|
| Queries come from x_t trunk, keys/values come from y trunk.
|
| """
|
|
|
| def __init__(self, channels, num_heads=4):
|
| super().__init__()
|
| self.channels = channels
|
| self.num_heads = num_heads
|
| assert channels % num_heads == 0
|
|
|
| self.norm_q = nn.GroupNorm(min(32, channels), channels)
|
| self.norm_kv = nn.GroupNorm(min(32, channels), channels)
|
| self.to_q = nn.Conv1d(channels, channels, 1)
|
| self.to_kv = nn.Conv1d(channels, channels * 2, 1)
|
| self.proj = nn.Conv1d(channels, channels, 1)
|
| self.scale = (channels // num_heads) ** -0.5
|
|
|
| def forward(self, x, context):
|
| """
|
| Args:
|
| x: query features [B, C, H, W] from x_t trunk
|
| context: key/value features [B, C, H, W] from y trunk
|
| """
|
| B, C, H, W = x.shape
|
| q_in = self.norm_q(x).view(B, C, H * W)
|
| kv_in = self.norm_kv(context).view(B, C, H * W)
|
|
|
| q = self.to_q(q_in).view(B, self.num_heads, C // self.num_heads, H * W)
|
| kv = self.to_kv(kv_in).view(B, 2, self.num_heads, C // self.num_heads, H * W)
|
| k, v = kv[:, 0], kv[:, 1]
|
|
|
| q = q.permute(0, 1, 3, 2)
|
| k = k.permute(0, 1, 2, 3)
|
| v = v.permute(0, 1, 3, 2)
|
|
|
| attn = torch.matmul(q, k) * self.scale
|
| attn = attn.softmax(dim=-1)
|
| out = torch.matmul(attn, v)
|
| out = out.permute(0, 1, 3, 2).contiguous().view(B, C, H * W)
|
| out = self.proj(out).view(B, C, H, W)
|
|
|
| return x + out
|
|
|
|
|
| class Downsample(nn.Module):
|
| def __init__(self, channels):
|
| super().__init__()
|
| self.conv = nn.Conv2d(channels, channels, 3, stride=2, padding=1)
|
|
|
| def forward(self, x):
|
| return self.conv(x)
|
|
|
|
|
| class Upsample(nn.Module):
|
| def __init__(self, channels):
|
| super().__init__()
|
| self.conv = nn.Conv2d(channels, channels, 3, padding=1)
|
|
|
| def forward(self, x):
|
| x = F.interpolate(x, scale_factor=2, mode="nearest")
|
| return self.conv(x)
|
|
|
|
|
|
|
|
|
|
|
|
|
| class BBDMUNet(nn.Module):
|
| """
|
| UNet for BBDM with self-attention at specified resolutions.
|
|
|
| Changes from SimpleUNet:
|
| - Self-attention at 16x16 and 32x32
|
| - Norm-before-conv ordering (DDPM paper convention)
|
| - Interpolation upsampling (fewer checkerboard artifacts)
|
| - Zero-initialized output conv
|
| - Larger timestep embedding (256-dim with hidden expansion)
|
| """
|
|
|
| def __init__(
|
| self,
|
| in_channels=3,
|
| base_channels=64,
|
| t_dim=256,
|
| image_size=256,
|
| channel_mults=(1, 2, 4, 4),
|
| attn_resolutions=(16, 32),
|
| num_heads=4,
|
| dropout=0.0,
|
| dual_output=False,
|
| use_x1_cond=True,
|
| ):
|
| super().__init__()
|
| self.image_size = image_size
|
| self.in_channels = in_channels
|
| self.dual_output = dual_output
|
| self.use_x1_cond = use_x1_cond
|
|
|
|
|
| self.time_mlp = nn.Sequential(
|
| SinusoidalPosEmb(t_dim),
|
| nn.Linear(t_dim, t_dim * 4),
|
| nn.SiLU(),
|
| nn.Linear(t_dim * 4, t_dim),
|
| )
|
|
|
| ch = base_channels
|
|
|
|
|
| n_in = in_channels * (2 if use_x1_cond else 1)
|
| self.input_conv = nn.Conv2d(n_in, ch, 3, padding=1)
|
|
|
|
|
| self.enc_blocks = nn.ModuleList()
|
| self.enc_attns = nn.ModuleList()
|
| self.enc_downs = nn.ModuleList()
|
|
|
| skip_channels = [ch]
|
| cur_ch = ch
|
| cur_res = image_size
|
|
|
| for i, mult in enumerate(channel_mults):
|
| out_ch = ch * mult
|
| self.enc_blocks.append(ResBlock(cur_ch, out_ch, t_dim, dropout))
|
| cur_ch = out_ch
|
| skip_channels.append(cur_ch)
|
|
|
| if cur_res in attn_resolutions:
|
| self.enc_attns.append(SelfAttention(cur_ch, num_heads))
|
| else:
|
| self.enc_attns.append(nn.Identity())
|
|
|
| if i < len(channel_mults) - 1:
|
| self.enc_downs.append(Downsample(cur_ch))
|
| cur_res //= 2
|
| else:
|
| self.enc_downs.append(nn.Identity())
|
|
|
|
|
| self.mid1 = ResBlock(cur_ch, cur_ch, t_dim, dropout)
|
| self.mid_attn = SelfAttention(cur_ch, num_heads)
|
| self.mid2 = ResBlock(cur_ch, cur_ch, t_dim, dropout)
|
|
|
|
|
| self.dec_blocks = nn.ModuleList()
|
| self.dec_attns = nn.ModuleList()
|
| self.dec_ups = nn.ModuleList()
|
|
|
| for i in reversed(range(len(channel_mults))):
|
| mult = channel_mults[i]
|
| out_ch = ch * mult
|
| skip_ch = skip_channels.pop()
|
|
|
| self.dec_blocks.append(ResBlock(cur_ch + skip_ch, out_ch, t_dim, dropout))
|
| cur_ch = out_ch
|
|
|
| dec_res = image_size // (2 ** i) if i < len(channel_mults) - 1 else cur_res
|
| if dec_res in attn_resolutions:
|
| self.dec_attns.append(SelfAttention(cur_ch, num_heads))
|
| else:
|
| self.dec_attns.append(nn.Identity())
|
|
|
| if i > 0:
|
| self.dec_ups.append(Upsample(cur_ch))
|
| else:
|
| self.dec_ups.append(nn.Identity())
|
|
|
|
|
| self.out_norm = nn.GroupNorm(min(32, cur_ch), cur_ch)
|
| if dual_output:
|
|
|
| self.out_z = nn.Conv2d(cur_ch, in_channels, 3, padding=1)
|
| self.out_x0 = nn.Conv2d(cur_ch, in_channels, 3, padding=1)
|
| nn.init.zeros_(self.out_z.weight)
|
| nn.init.zeros_(self.out_z.bias)
|
| nn.init.zeros_(self.out_x0.weight)
|
| nn.init.zeros_(self.out_x0.bias)
|
| else:
|
| self.out_conv = nn.Conv2d(cur_ch, in_channels, 3, padding=1)
|
| nn.init.zeros_(self.out_conv.weight)
|
| nn.init.zeros_(self.out_conv.bias)
|
|
|
| def forward(self, x_t, x_T, t, cond_mask=None):
|
| """
|
| Args:
|
| x_t: noisy intermediate [B, C, H, W]
|
| x_T: source conditioning [B, C, H, W]
|
| t: timestep [B]
|
| cond_mask: optional [B, 1, 1, 1] binary mask. 0 = drop conditioning (for CFG training).
|
|
|
| Returns:
|
| single tensor [B, C, H, W] when dual_output=False (default),
|
| tuple (z_pred, x0_pred) each [B, C, H, W] when dual_output=True.
|
| """
|
| t_emb = self.time_mlp(t)
|
| if self.use_x1_cond:
|
| if cond_mask is not None:
|
| x_T = x_T * cond_mask
|
| x = torch.cat([x_t, x_T], dim=1)
|
| else:
|
| x = x_t
|
| h = self.input_conv(x)
|
|
|
|
|
| skips = [h]
|
| for block, attn, down in zip(self.enc_blocks, self.enc_attns, self.enc_downs):
|
| h = block(h, t_emb)
|
| h = attn(h)
|
| skips.append(h)
|
| h = down(h)
|
|
|
|
|
| h = self.mid1(h, t_emb)
|
| h = self.mid_attn(h)
|
| h = self.mid2(h, t_emb)
|
|
|
|
|
| for block, attn, up in zip(self.dec_blocks, self.dec_attns, self.dec_ups):
|
| h = torch.cat([h, skips.pop()], dim=1)
|
| h = block(h, t_emb)
|
| h = attn(h)
|
| h = up(h)
|
|
|
| h = F.silu(self.out_norm(h))
|
| if self.dual_output:
|
| return self.out_z(h), self.out_x0(h)
|
| return self.out_conv(h)
|
|
|
|
|
|
|
|
|
|
|
|
|
| class DualTrunkBBDMUNet(nn.Module):
|
| """
|
| Dual-trunk UNet for BBDM with cross-attention conditioning.
|
|
|
| y-trunk (encoder only): Extracts multi-resolution features from source image y.
|
| x_t-trunk (full UNet): Denoises x_t, cross-attending to y-trunk features at each
|
| resolution level that has attention.
|
|
|
| CFG: During training, y-trunk features are randomly replaced with zeros.
|
| At inference, the model handles CFG internally via encode_y() + forward().
|
|
|
| Inspired by TryOnDiffusion's parallel UNet architecture.
|
| """
|
|
|
| def __init__(
|
| self,
|
| in_channels=3,
|
| base_channels=64,
|
| t_dim=256,
|
| image_size=256,
|
| channel_mults=(1, 2, 4, 4),
|
| attn_resolutions=(16, 32),
|
| num_heads=4,
|
| dropout=0.0,
|
| ):
|
| super().__init__()
|
| self.image_size = image_size
|
| self.in_channels = in_channels
|
| self.is_dual_trunk = True
|
|
|
| ch = base_channels
|
|
|
|
|
| self.time_mlp = nn.Sequential(
|
| SinusoidalPosEmb(t_dim),
|
| nn.Linear(t_dim, t_dim * 4),
|
| nn.SiLU(),
|
| nn.Linear(t_dim * 4, t_dim),
|
| )
|
|
|
|
|
|
|
|
|
| self.y_input_conv = nn.Conv2d(in_channels, ch, 3, padding=1)
|
|
|
| self.y_enc_blocks = nn.ModuleList()
|
| self.y_enc_attns = nn.ModuleList()
|
| self.y_enc_downs = nn.ModuleList()
|
|
|
| cur_ch = ch
|
| cur_res = image_size
|
|
|
| self._y_channels = [cur_ch]
|
|
|
| for i, mult in enumerate(channel_mults):
|
| out_ch = ch * mult
|
|
|
| self.y_enc_blocks.append(ResBlock(cur_ch, out_ch, t_dim, dropout))
|
| cur_ch = out_ch
|
| self._y_channels.append(cur_ch)
|
|
|
| if cur_res in attn_resolutions:
|
| self.y_enc_attns.append(SelfAttention(cur_ch, num_heads))
|
| else:
|
| self.y_enc_attns.append(nn.Identity())
|
|
|
| if i < len(channel_mults) - 1:
|
| self.y_enc_downs.append(Downsample(cur_ch))
|
| cur_res //= 2
|
| else:
|
| self.y_enc_downs.append(nn.Identity())
|
|
|
|
|
| self.y_mid1 = ResBlock(cur_ch, cur_ch, t_dim, dropout)
|
| self.y_mid_attn = SelfAttention(cur_ch, num_heads)
|
| self.y_mid2 = ResBlock(cur_ch, cur_ch, t_dim, dropout)
|
|
|
|
|
|
|
|
|
| cur_ch = ch
|
| cur_res = image_size
|
|
|
| self.input_conv = nn.Conv2d(in_channels, ch, 3, padding=1)
|
|
|
|
|
| self.enc_blocks = nn.ModuleList()
|
| self.enc_self_attns = nn.ModuleList()
|
| self.enc_cross_attns = nn.ModuleList()
|
| self.enc_downs = nn.ModuleList()
|
|
|
| skip_channels = [ch]
|
|
|
| for i, mult in enumerate(channel_mults):
|
| out_ch = ch * mult
|
| self.enc_blocks.append(ResBlock(cur_ch, out_ch, t_dim, dropout))
|
| cur_ch = out_ch
|
| skip_channels.append(cur_ch)
|
|
|
| if cur_res in attn_resolutions:
|
| self.enc_self_attns.append(SelfAttention(cur_ch, num_heads))
|
| self.enc_cross_attns.append(CrossAttention(cur_ch, num_heads))
|
| else:
|
| self.enc_self_attns.append(nn.Identity())
|
| self.enc_cross_attns.append(None)
|
|
|
| if i < len(channel_mults) - 1:
|
| self.enc_downs.append(Downsample(cur_ch))
|
| cur_res //= 2
|
| else:
|
| self.enc_downs.append(nn.Identity())
|
|
|
|
|
|
|
| self._enc_cross_attn_indices = []
|
| for i, ca in enumerate(self.enc_cross_attns):
|
| if ca is not None:
|
| self._enc_cross_attn_indices.append(i)
|
|
|
| self.enc_cross_attns = nn.ModuleList(
|
| [ca if ca is not None else nn.Identity() for ca in self.enc_cross_attns]
|
| )
|
|
|
|
|
| self.mid1 = ResBlock(cur_ch, cur_ch, t_dim, dropout)
|
| self.mid_self_attn = SelfAttention(cur_ch, num_heads)
|
| self.mid_cross_attn = CrossAttention(cur_ch, num_heads)
|
| self.mid2 = ResBlock(cur_ch, cur_ch, t_dim, dropout)
|
|
|
|
|
| self.dec_blocks = nn.ModuleList()
|
| self.dec_self_attns = nn.ModuleList()
|
| self.dec_cross_attns = nn.ModuleList()
|
| self.dec_ups = nn.ModuleList()
|
|
|
| self._dec_cross_attn_indices = []
|
| for i in reversed(range(len(channel_mults))):
|
| mult = channel_mults[i]
|
| out_ch = ch * mult
|
| skip_ch = skip_channels.pop()
|
|
|
| self.dec_blocks.append(ResBlock(cur_ch + skip_ch, out_ch, t_dim, dropout))
|
| cur_ch = out_ch
|
|
|
| dec_res = image_size // (2 ** i) if i < len(channel_mults) - 1 else cur_res
|
| if dec_res in attn_resolutions:
|
| self.dec_self_attns.append(SelfAttention(cur_ch, num_heads))
|
| self.dec_cross_attns.append(CrossAttention(cur_ch, num_heads))
|
| self._dec_cross_attn_indices.append(len(self.dec_cross_attns) - 1)
|
| else:
|
| self.dec_self_attns.append(nn.Identity())
|
| self.dec_cross_attns.append(nn.Identity())
|
|
|
| if i > 0:
|
| self.dec_ups.append(Upsample(cur_ch))
|
| else:
|
| self.dec_ups.append(nn.Identity())
|
|
|
|
|
| self.out_norm = nn.GroupNorm(min(32, cur_ch), cur_ch)
|
| self.out_conv = nn.Conv2d(cur_ch, in_channels, 3, padding=1)
|
| nn.init.zeros_(self.out_conv.weight)
|
| nn.init.zeros_(self.out_conv.bias)
|
|
|
|
|
| self.register_buffer("_zero_t_emb", torch.zeros(1, t_dim))
|
|
|
| def encode_y(self, y):
|
| """
|
| Run y-trunk encoder to get multi-resolution features.
|
| Returns list of features: [after_input_conv, after_level_0, after_level_1, ..., bottleneck]
|
| """
|
| zero_t = self._zero_t_emb.expand(y.shape[0], -1)
|
| h = self.y_input_conv(y)
|
| features = [h]
|
|
|
| for block, attn, down in zip(self.y_enc_blocks, self.y_enc_attns, self.y_enc_downs):
|
| h = block(h, zero_t)
|
| h = attn(h)
|
| features.append(h)
|
| h = down(h)
|
|
|
|
|
| h = self.y_mid1(h, zero_t)
|
| h = self.y_mid_attn(h)
|
| h = self.y_mid2(h, zero_t)
|
| features.append(h)
|
|
|
| return features
|
|
|
| def forward(self, x_t, x_T, t, cond_mask=None, y_features=None):
|
| """
|
| Args:
|
| x_t: noisy intermediate [B, C, H, W]
|
| x_T: source conditioning image [B, C, H, W]
|
| t: timestep [B]
|
| cond_mask: [B, 1, 1, 1] binary mask. 0 = null conditioning (for CFG training).
|
| y_features: precomputed y-trunk features (optional, for efficient CFG inference).
|
| If None, runs y-trunk on x_T.
|
| """
|
|
|
| if y_features is None:
|
| y_features = self.encode_y(x_T)
|
|
|
|
|
| if cond_mask is not None:
|
| y_features = [f * cond_mask for f in y_features]
|
|
|
| t_emb = self.time_mlp(t)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| h = self.input_conv(x_t)
|
| skips = [h]
|
|
|
| for i, (block, self_attn, cross_attn, down) in enumerate(
|
| zip(self.enc_blocks, self.enc_self_attns, self.enc_cross_attns, self.enc_downs)
|
| ):
|
| h = block(h, t_emb)
|
| h = self_attn(h)
|
|
|
| if i in self._enc_cross_attn_indices:
|
| h = cross_attn(h, y_features[i + 1])
|
| skips.append(h)
|
| h = down(h)
|
|
|
|
|
| h = self.mid1(h, t_emb)
|
| h = self.mid_self_attn(h)
|
| h = self.mid_cross_attn(h, y_features[-1])
|
| h = self.mid2(h, t_emb)
|
|
|
|
|
| for i, (block, self_attn, cross_attn, up) in enumerate(
|
| zip(self.dec_blocks, self.dec_self_attns, self.dec_cross_attns, self.dec_ups)
|
| ):
|
| h = torch.cat([h, skips.pop()], dim=1)
|
| h = block(h, t_emb)
|
| h = self_attn(h)
|
| if i in self._dec_cross_attn_indices:
|
|
|
|
|
| n_levels = len(self.enc_blocks)
|
| enc_idx = n_levels - i
|
| if enc_idx < len(y_features) - 1:
|
| h = cross_attn(h, y_features[enc_idx])
|
| h = up(h)
|
|
|
| return self.out_conv(F.silu(self.out_norm(h)))
|
|
|
|
|
|
|
|
|
|
|
|
|
| class LatentBBDM(nn.Module):
|
| """
|
| Wraps BBDMUNet with a frozen pretrained VQGAN for latent-space diffusion.
|
|
|
| Uses taming-transformers VQGAN-f4:
|
| - 256x256x3 images -> 64x64x3 pre-quant continuous latents
|
| - Latent features have ~unit variance (constrained by VQ codebook)
|
| - Compatible with SimpleUNet channel_mults=(1,2,4,4)
|
|
|
| The encode path uses encoder + quant_conv (pre-quantization features),
|
| NOT the quantized codes. This matches the original BBDM paper.
|
| """
|
|
|
| def __init__(
|
| self,
|
| unet_channels=128,
|
| latent_channels=3,
|
| image_size=256,
|
| latent_size=64,
|
| t_dim=256,
|
| attn_resolutions=(16, 32),
|
| channel_mults=(1, 2, 4, 4),
|
| num_heads=4,
|
| dropout=0.0,
|
| vqgan_ckpt=None,
|
| vqgan_config=None,
|
| ):
|
| super().__init__()
|
| self.image_size = image_size
|
| self.latent_size = latent_size
|
| self.latent_channels = latent_channels
|
| self.has_vqgan = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| self.unet = BBDMUNet(
|
| in_channels=latent_channels,
|
| base_channels=unet_channels,
|
| t_dim=t_dim,
|
| image_size=latent_size,
|
| channel_mults=channel_mults,
|
| attn_resolutions=attn_resolutions,
|
| num_heads=num_heads,
|
| dropout=dropout,
|
| )
|
|
|
| if vqgan_ckpt is not None:
|
| self._load_vqgan(vqgan_ckpt, vqgan_config)
|
|
|
| def _load_vqgan(self, ckpt_path, config_path):
|
| """Load taming-transformers VQGAN from checkpoint + config."""
|
| from omegaconf import OmegaConf
|
| from taming.models.vqgan import VQModel
|
|
|
| config = OmegaConf.load(config_path)
|
| params = config.model.params
|
| self.vqgan = VQModel(
|
| ddconfig=params.ddconfig,
|
| lossconfig=params.lossconfig,
|
| n_embed=params.n_embed,
|
| embed_dim=params.embed_dim,
|
| )
|
|
|
| sd = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
| if "state_dict" in sd:
|
| sd = sd["state_dict"]
|
| self.vqgan.load_state_dict(sd, strict=False)
|
| self.vqgan.eval()
|
|
|
| for p in self.vqgan.parameters():
|
| p.requires_grad_(False)
|
|
|
| self.has_vqgan = True
|
| self.latent_channels = params.ddconfig.z_channels
|
| print(f"Loaded VQGAN from {ckpt_path} (z_channels={self.latent_channels})")
|
|
|
| @torch.no_grad()
|
| def encode(self, x):
|
| """Encode to pre-quant continuous latent (NOT quantized codes)."""
|
| if not self.has_vqgan:
|
| return x
|
| h = self.vqgan.encoder(x)
|
| h = self.vqgan.quant_conv(h)
|
| return h
|
|
|
| @torch.no_grad()
|
| def decode(self, z):
|
| """Decode from pre-quant latent: quantize through VQ codebook, then decode."""
|
| if not self.has_vqgan:
|
| return z
|
|
|
|
|
| z_quant, _, _ = self.vqgan.quantize(z)
|
| return self.vqgan.decode(z_quant)
|
|
|
| def forward(self, z_t, z_T, t, cond_mask=None):
|
| return self.unet(z_t, z_T, t, cond_mask=cond_mask)
|
|
|
|
|
|
|
|
|
|
|
|
|
| class PatchGANDiscriminator(nn.Module):
|
| """
|
| PatchGAN discriminator (70x70 receptive field).
|
|
|
| Takes concatenated (source, target_or_generated) as input [B, 6, H, W].
|
| Outputs patch-level real/fake predictions [B, 1, N, N].
|
| """
|
|
|
| def __init__(self, in_channels=6, base_channels=64, n_layers=3):
|
| super().__init__()
|
| layers = [
|
| nn.Conv2d(in_channels, base_channels, 4, stride=2, padding=1),
|
| nn.LeakyReLU(0.2, inplace=True),
|
| ]
|
|
|
| ch = base_channels
|
| for i in range(1, n_layers):
|
| out_ch = min(ch * 2, 512)
|
| layers += [
|
| nn.Conv2d(ch, out_ch, 4, stride=2, padding=1),
|
| nn.InstanceNorm2d(out_ch),
|
| nn.LeakyReLU(0.2, inplace=True),
|
| ]
|
| ch = out_ch
|
|
|
|
|
| out_ch = min(ch * 2, 512)
|
| layers += [
|
| nn.Conv2d(ch, out_ch, 4, stride=1, padding=1),
|
| nn.InstanceNorm2d(out_ch),
|
| nn.LeakyReLU(0.2, inplace=True),
|
| ]
|
|
|
|
|
| layers += [nn.Conv2d(out_ch, 1, 4, stride=1, padding=1)]
|
|
|
| self.model = nn.Sequential(*layers)
|
|
|
| def forward(self, source, target):
|
| """
|
| Args:
|
| source: source domain image (HE) [B, 3, H, W]
|
| target: real or generated target image [B, 3, H, W]
|
| Returns:
|
| patch predictions [B, 1, N, N]
|
| """
|
| return self.model(torch.cat([source, target], dim=1))
|
|
|
|
|
|
|
|
|
|
|
|
|
| def SimpleUNet(in_channels=3, base_channels=64, t_dim=128, image_size=64,
|
| dual_output=False, use_x1_cond=True):
|
| """Drop-in replacement for old SimpleUNet. Now with attention.
|
|
|
| use_x1_cond=False removes x_1 from the U-Net input (3 channels in instead of 6).
|
| Conditioning then enters only via the bridge state x_t = α·x_0 + β·x_1 + γ·z.
|
| Useful as a cheat-blocker when schedule learning lets γ blow up.
|
|
|
| Set dual_output=True for the learned-schedule SNR-routed dual-target loss.
|
| """
|
| if image_size <= 64:
|
| attn_res = (16,)
|
| mults = (1, 2, 4)
|
| else:
|
| attn_res = (16, 32)
|
| mults = (1, 2, 4, 4)
|
|
|
| return BBDMUNet(
|
| in_channels=in_channels,
|
| base_channels=base_channels,
|
| t_dim=t_dim,
|
| image_size=image_size,
|
| channel_mults=mults,
|
| attn_resolutions=attn_res,
|
| dual_output=dual_output,
|
| use_x1_cond=use_x1_cond,
|
| )
|
|
|
|
|
| def OriginalBBDMUNet(in_channels=3, base_channels=128, t_dim=256, image_size=64):
|
| """
|
| UNet matching the original BBDM paper (xuekt98/BBDM).
|
|
|
| Designed for latent-space diffusion on 64x64x3 VQGAN-f4 latents.
|
| Architecture: 64->32->16->8 bottleneck (3 stages, 2 downsamples)
|
| Channel mults: (1, 4, 8) -> [128, 512, 1024]
|
| Attention at 32, 16, and 8 — covers nearly the whole network.
|
| 8 heads (64 dims/head at 512ch, matching LDM convention).
|
| """
|
| return BBDMUNet(
|
| in_channels=in_channels,
|
| base_channels=base_channels,
|
| t_dim=t_dim,
|
| image_size=image_size,
|
| channel_mults=(1, 4, 8),
|
| attn_resolutions=(32, 16, 8),
|
| num_heads=8,
|
| dropout=0.0,
|
| )
|
|
|
|
|
| def DualTrunkUNet(in_channels=3, base_channels=128, t_dim=256, image_size=256):
|
| """Dual-trunk UNet with cross-attention conditioning from y-trunk."""
|
| if image_size <= 64:
|
| attn_res = (16,)
|
| mults = (1, 2, 4)
|
| else:
|
| attn_res = (16, 32)
|
| mults = (1, 2, 4, 4)
|
|
|
| return DualTrunkBBDMUNet(
|
| in_channels=in_channels,
|
| base_channels=base_channels,
|
| t_dim=t_dim,
|
| image_size=image_size,
|
| channel_mults=mults,
|
| attn_resolutions=attn_res,
|
| )
|
|
|
|
|
| def DeepDualTrunkUNet(in_channels=3, base_channels=128, t_dim=256, image_size=256):
|
| """Deep dual-trunk UNet: 5 levels, cross-attention at 16x16 and 32x32."""
|
| return DualTrunkBBDMUNet(
|
| in_channels=in_channels,
|
| base_channels=base_channels,
|
| t_dim=t_dim,
|
| image_size=image_size,
|
| channel_mults=(1, 2, 4, 4, 4),
|
| attn_resolutions=(16, 32),
|
| num_heads=8,
|
| dropout=0.0,
|
| )
|
|
|
|
|
| def DeepUNet(in_channels=3, base_channels=128, t_dim=256, image_size=256):
|
| """
|
| Deeper UNet that reaches 16x16 resolution with attention.
|
|
|
| Architecture: 256->128->64->32->16 (4 downsamples)
|
| Channel mults: (1, 2, 4, 4, 4) -> [128, 256, 512, 512, 512]
|
| Attention at 16x16 and 32x32 resolutions.
|
| Capped at 512 channels for fp16 stability.
|
| """
|
| return BBDMUNet(
|
| in_channels=in_channels,
|
| base_channels=base_channels,
|
| t_dim=t_dim,
|
| image_size=image_size,
|
| channel_mults=(1, 2, 4, 4, 4),
|
| attn_resolutions=(16, 32),
|
| num_heads=8,
|
| dropout=0.0,
|
| )
|
|
|