bci-gate-warm / code /model.py
augustander's picture
Upload code/model.py with huggingface_hub
7a33586 verified
Raw
History Blame Contribute Delete
31 kB
"""
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
# ============================================================
# Building blocks
# ============================================================
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) # [B, heads, HW, dim]
k = k.permute(0, 1, 2, 3) # [B, heads, dim, HW]
v = v.permute(0, 1, 3, 2) # [B, heads, HW, dim]
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) # [B, heads, HW, dim]
k = k.permute(0, 1, 2, 3) # [B, heads, dim, HW]
v = v.permute(0, 1, 3, 2) # [B, heads, HW, dim]
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)
# ============================================================
# Main UNet with attention
# ============================================================
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
# Timestep embedding
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
# Input projection: 6-ch (x_t + x_1) when use_x1_cond=True, else 3-ch (x_t alone)
n_in = in_channels * (2 if use_x1_cond else 1)
self.input_conv = nn.Conv2d(n_in, ch, 3, padding=1)
# ---- Encoder ----
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())
# ---- Bottleneck ----
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)
# ---- Decoder ----
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())
# ---- Output ----
self.out_norm = nn.GroupNorm(min(32, cur_ch), cur_ch)
if dual_output:
# Two parallel heads: predicts noise z and clean image x_0
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 # x_1 NOT given as explicit input; only enters via the bridge state x_t
h = self.input_conv(x)
# Encoder with skip connections
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)
# Bottleneck
h = self.mid1(h, t_emb)
h = self.mid_attn(h)
h = self.mid2(h, t_emb)
# Decoder
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)
# ============================================================
# Dual-Trunk UNet (cross-attention conditioning)
# ============================================================
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 # flag for bridge/training code to detect
ch = base_channels
# ---- Timestep embedding (only for x_t trunk) ----
self.time_mlp = nn.Sequential(
SinusoidalPosEmb(t_dim),
nn.Linear(t_dim, t_dim * 4),
nn.SiLU(),
nn.Linear(t_dim * 4, t_dim),
)
# ================================================================
# Y-TRUNK (encoder only, no timestep, extracts conditioning features)
# ================================================================
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
# Track y-trunk output channels at each level for cross-attention
self._y_channels = [cur_ch] # after input conv
for i, mult in enumerate(channel_mults):
out_ch = ch * mult
# y-trunk uses ResBlock but with a dummy t_dim — we'll pass zeros
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())
# Y-trunk bottleneck
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)
# ================================================================
# X_T TRUNK (full UNet with cross-attention to y-trunk features)
# ================================================================
cur_ch = ch
cur_res = image_size
self.input_conv = nn.Conv2d(in_channels, ch, 3, padding=1) # 3ch, NOT 6ch
# ---- Encoder ----
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) # placeholder
if i < len(channel_mults) - 1:
self.enc_downs.append(Downsample(cur_ch))
cur_res //= 2
else:
self.enc_downs.append(nn.Identity())
# Use ModuleList for proper registration of cross-attn (replace None with Identity)
# We handle None in forward manually
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)
# Re-register as ModuleList (only non-None)
self.enc_cross_attns = nn.ModuleList(
[ca if ca is not None else nn.Identity() for ca in self.enc_cross_attns]
)
# ---- Bottleneck ----
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)
# ---- Decoder ----
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())
# ---- Output ----
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)
# Zero timestep embedding for y-trunk (doesn't depend on t)
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)
# Bottleneck
h = self.y_mid1(h, zero_t)
h = self.y_mid_attn(h)
h = self.y_mid2(h, zero_t)
features.append(h) # bottleneck features
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.
"""
# Get y-trunk features
if y_features is None:
y_features = self.encode_y(x_T)
# Apply CFG conditioning dropout (zero out y features)
if cond_mask is not None:
y_features = [f * cond_mask for f in y_features]
t_emb = self.time_mlp(t)
# y_features layout: [input, level0, level1, ..., levelN, bottleneck]
# index 0 = after input conv (for skip-level matching)
# index 1..N = after each encoder level
# index -1 = bottleneck
# ---- x_t Encoder ----
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)
# Cross-attend to y features at matching resolution
if i in self._enc_cross_attn_indices:
h = cross_attn(h, y_features[i + 1]) # +1 because features[0] is input conv
skips.append(h)
h = down(h)
# ---- Bottleneck ----
h = self.mid1(h, t_emb)
h = self.mid_self_attn(h)
h = self.mid_cross_attn(h, y_features[-1]) # bottleneck y features
h = self.mid2(h, t_emb)
# ---- Decoder ----
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:
# Match to encoder-level y features (decoder mirrors encoder)
# Decoder level i corresponds to encoder level (N-1-i)
n_levels = len(self.enc_blocks)
enc_idx = n_levels - i # features index
if enc_idx < len(y_features) - 1: # -1 to exclude bottleneck
h = cross_attn(h, y_features[enc_idx])
h = up(h)
return self.out_conv(F.silu(self.out_norm(h)))
# ============================================================
# VQGAN wrapper for latent-space BBDM
# ============================================================
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
# UNet operates in latent space
# For vq-f4 64x64x3 latents with (1,2,4,4):
# Stage 0: 64x64 x 128ch (attention at 32)
# Stage 1: 32x32 x 256ch (attention at 16)
# Stage 2: 16x16 x 512ch (attention at 16)
# Stage 3: 8x8 x 512ch (no downsample)
# Bottleneck: 8x8 x 512ch with attention
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
# Snap to nearest codebook entry (as in original BBDM paper)
# This acts as free error correction for noisy predictions
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)
# ============================================================
# PatchGAN Discriminator
# ============================================================
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
# Second-to-last layer: stride 1
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),
]
# Final layer: 1-channel prediction map
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))
# ============================================================
# Factory functions
# ============================================================
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,
)