pixeldit-space / diffusers_patch /src /diffusers /pipelines /pixeldit /modeling_pixeldit_controlnet.py
madtune's picture
FIX pipeline
878c361
Raw
History Blame Contribute Delete
16.7 kB
"""
PixelDiT ControlNet + IP-Adapter model.
Architecture follows SANA ControlNet — copied transformer blocks inject
reference-image features into the patch pathway (s tokens) via zero-init
skip projections. IP-Adapter adds cross-attention from SigLIP patches
into every Gemma text-token slot.
"""
from copy import deepcopy
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.checkpoint import checkpoint as grad_ckpt
_HIDDEN = 1536 # PixelDiT hidden_size
_SIGLIP_DIM = 1152 # google/siglip-so400m-patch14-384 hidden_size
class ControlNetBlock(nn.Module):
"""One copied transformer block + zero-init in/out projections."""
def __init__(self, base_block, block_index: int):
super().__init__()
self.copied_block = deepcopy(base_block)
self.block_index = block_index
if block_index == 0:
self.before_proj = nn.Linear(_HIDDEN, _HIDDEN)
nn.init.zeros_(self.before_proj.weight)
nn.init.zeros_(self.before_proj.bias)
self.after_proj = nn.Linear(_HIDDEN, _HIDDEN)
nn.init.zeros_(self.after_proj.weight)
nn.init.zeros_(self.after_proj.bias)
def forward(self, s_main, y_emb, condition, pos, pos_txt, ctrl_s):
dt = self.after_proj.weight.dtype
use_ckpt = self.training and torch.is_grad_enabled()
if self.block_index == 0:
ctrl_s = ctrl_s + self.before_proj(ctrl_s.to(dt))
inp = (s_main + ctrl_s).to(dt)
else:
inp = ctrl_s.to(dt)
y_in = y_emb.detach().to(dt)
if use_ckpt:
ctrl_s, _ = grad_ckpt(
self.copied_block, inp, y_in, condition, pos, pos_txt, None,
use_reentrant=False,
)
else:
ctrl_s, _ = self.copied_block(inp, y_in, condition, pos, pos_txt, None)
skip = self.after_proj(ctrl_s.to(dt))
return ctrl_s, skip
class PixelDiTControlNet(nn.Module):
"""
Wraps a frozen PixelDiT inner transformer with a trainable ControlNet branch
and IP-Adapter cross-attention layers.
The ``transformer`` argument is the raw ``.model`` attribute of
``PixelDiTModel`` (the inner ``SanaMS`` or equivalent nn.Module), NOT
the ``PixelDiTModel`` wrapper itself. Use :func:`unwrap_transformer` to
extract it from a loaded ``PixelDiTModel``.
Training: ref_x = same clean image as the diffusion target (self-reconstruction).
Inference: ref_x = style / reference image to transfer from.
"""
def __init__(self, transformer, copy_blocks_num: int = 7):
super().__init__()
self.transformer = transformer
self.copy_blocks_num = copy_blocks_num
self.controlnet_blocks = nn.ModuleList([
ControlNetBlock(transformer.patch_blocks[i], i)
for i in range(copy_blocks_num)
])
# SigLIP → text-space conditioning (adds to y_emb in ControlNet branch)
self.siglip_y_proj = nn.Sequential(
nn.LayerNorm(_SIGLIP_DIM),
nn.Linear(_SIGLIP_DIM, _HIDDEN),
)
nn.init.zeros_(self.siglip_y_proj[1].weight)
nn.init.zeros_(self.siglip_y_proj[1].bias)
# IP-Adapter cross-attention path (keys match ip_adapter_v2.pt)
self.ip_proj = nn.Linear(_SIGLIP_DIM, _HIDDEN, bias=True)
nn.init.normal_(self.ip_proj.weight, std=0.02)
nn.init.zeros_(self.ip_proj.bias)
self.ip_k = nn.ModuleList([nn.Linear(_HIDDEN, _HIDDEN, bias=False) for _ in range(transformer.patch_depth)])
self.ip_v = nn.ModuleList([nn.Linear(_HIDDEN, _HIDDEN, bias=False) for _ in range(transformer.patch_depth)])
for layer in self.ip_v:
nn.init.normal_(layer.weight, std=0.02)
self.ip_scale = nn.Parameter(torch.full((transformer.patch_depth,), 0.1))
self._freeze_transformer()
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
@property
def dtype(self):
return next(self.parameters()).dtype
@property
def device(self):
return next(self.parameters()).device
def _freeze_transformer(self):
for p in self.transformer.parameters():
p.requires_grad_(False)
def trainable_params(self):
return (
list(self.controlnet_blocks.parameters())
+ list(self.siglip_y_proj.parameters())
+ self.ip_params()
)
def ip_params(self):
return (
list(self.ip_proj.parameters())
+ list(self.ip_k.parameters())
+ list(self.ip_v.parameters())
+ [self.ip_scale]
)
def encode_siglip(self, patches: torch.Tensor, n_ip: int = 256) -> torch.Tensor:
"""Pool SigLIP patch tokens to n_ip and project to hidden space."""
resampled = F.adaptive_avg_pool1d(patches.permute(0, 2, 1), n_ip).permute(0, 2, 1)
return self.ip_proj(resampled.to(self.ip_proj.weight.dtype))
def _ip_cross_attn(self, y: torch.Tensor, ip: torch.Tensor, block_idx: int) -> torch.Tensor:
B, Ny, H = y.shape
n_heads = self.transformer.num_groups
hd = H // n_heads
wdtype = self.ip_k[block_idx].weight.dtype
q = y.to(wdtype).reshape(B, Ny, n_heads, hd).transpose(1, 2)
ip = ip.to(wdtype)
k = self.ip_k[block_idx](ip).reshape(B, ip.shape[1], n_heads, hd).transpose(1, 2)
v = self.ip_v[block_idx](ip).reshape(B, ip.shape[1], n_heads, hd).transpose(1, 2)
out = F.scaled_dot_product_attention(q, k, v)
return out.transpose(1, 2).reshape(B, Ny, H).to(y.dtype)
# ------------------------------------------------------------------
# Forward
# ------------------------------------------------------------------
def forward(
self,
x: torch.Tensor, # noisy image [B, 3, H, W]
t: torch.Tensor, # sigma×1000 [B]
y: torch.Tensor, # Gemma embed [B, T, 2304]
ref_x: torch.Tensor, # reference [B, 3, H, W]
siglip_y: torch.Tensor = None, # SigLIP tokens [B, T, 1152]
ctrl_drop: bool = False,
ctrl_scale = 1.0,
siglip_scale = 1.0,
ip_features: torch.Tensor = None, # [B, 256, 1536]
ip_strength = 1.0,
) -> torch.Tensor:
tr = self.transformer
tr_dev = next(tr.parameters()).device
cn_dev = next(self.controlnet_blocks.parameters()).device
split = tr_dev != cn_dev
B, _, H, W = x.shape
Hs, Ws = H // tr.patch_size, W // tr.patch_size
L = Hs * Ws
x = x.to(tr_dev)
t = t.to(tr_dev)
y = y.to(tr_dev)
ref_x = ref_x.to(tr_dev)
pos = tr.fetch_pos(Hs, Ws, tr_dev)
patches = F.unfold(x, kernel_size=tr.patch_size, stride=tr.patch_size).transpose(1, 2)
ref_patches = F.unfold(ref_x, kernel_size=tr.patch_size, stride=tr.patch_size).transpose(1, 2)
t_emb = tr.t_embedder(t.view(-1)).view(B, -1, tr.hidden_size)
Ltxt = min(y.shape[1], tr.txt_max_length)
y = y[:, :Ltxt, :]
y_emb = tr.y_embedder(y).view(B, Ltxt, tr.hidden_size)
y_emb = y_emb + tr.y_pos_embedding[:, :Ltxt, :].to(y_emb.dtype)
condition = F.silu(t_emb)
pos_txt = tr.fetch_pos_text(Ltxt, tr_dev) if tr.use_text_rope else None
siglip_ctrl = None
if siglip_y is not None:
proj_param = next(self.siglip_y_proj.parameters())
sig = siglip_y[:, :Ltxt, :].to(proj_param.device, dtype=proj_param.dtype)
siglip_ctrl = self.siglip_y_proj(sig)
if torch.is_tensor(siglip_scale):
sig_scale = siglip_scale.to(siglip_ctrl.device, dtype=siglip_ctrl.dtype).view(-1, 1, 1)
else:
sig_scale = siglip_ctrl.new_tensor(float(siglip_scale))
siglip_ctrl = siglip_ctrl * sig_scale
ip_for_attn = None
if ip_features is not None:
if ip_features.shape[0] != B:
raise ValueError(f"ip_features batch {ip_features.shape[0]} != latent batch {B}")
ip_for_attn = ip_features.to(self.ip_proj.weight.device, dtype=self.ip_proj.weight.dtype)
def apply_ip(y_tokens, block_idx):
if ip_for_attn is None:
return y_tokens
ip_dev = self.ip_proj.weight.device
y_ip = y_tokens.to(ip_dev) if y_tokens.device != ip_dev else y_tokens
delta = self._ip_cross_attn(y_ip, ip_for_attn, block_idx)
if torch.is_tensor(ip_strength):
strength = ip_strength.to(delta.device, dtype=delta.dtype).view(-1, 1, 1)
else:
strength = delta.new_tensor(float(ip_strength))
block_scale = self.ip_scale[block_idx].to(delta.device, dtype=delta.dtype)
return y_tokens + (block_scale * strength * delta).to(y_tokens.device, dtype=y_tokens.dtype)
s = tr.s_embedder(patches)
ctrl_s = tr.s_embedder(ref_patches)
use_ckpt = self.training and torch.is_grad_enabled()
# Block 0 — no ControlNet skip yet (matches SANA pattern)
y_emb = apply_ip(y_emb, 0)
if use_ckpt:
s, y_emb = grad_ckpt(tr.patch_blocks[0], s, y_emb, condition, pos, pos_txt, None, use_reentrant=False)
else:
s, y_emb = tr.patch_blocks[0](s, y_emb, condition, pos, pos_txt, None)
# ControlNet blocks inject skips into main blocks 1 … copy_blocks_num
for i in range(self.copy_blocks_num):
s_cn = s.to(cn_dev) if split else s
ctrl_s_ = ctrl_s.to(cn_dev) if split else ctrl_s
y_cn = y_emb.to(cn_dev) if split else y_emb
if siglip_ctrl is not None:
y_cn = y_cn + siglip_ctrl.to(cn_dev, dtype=y_cn.dtype)
cond_cn = condition.to(cn_dev) if split else condition
pos_cn = pos.to(cn_dev) if split else pos
pos_t_cn = pos_txt.to(cn_dev) if (split and pos_txt is not None) else pos_txt
ctrl_s_, skip = self.controlnet_blocks[i](s_cn, y_cn, cond_cn, pos_cn, pos_t_cn, ctrl_s_)
ctrl_s = ctrl_s_.to(tr_dev) if split else ctrl_s_
if ctrl_drop:
skip_eff = skip * 0.0
else:
if torch.is_tensor(ctrl_scale):
scale = ctrl_scale.to(skip.device, dtype=skip.dtype).view(-1, 1, 1)
else:
scale = skip.new_tensor(float(ctrl_scale))
skip_eff = skip * scale
s = s + skip_eff.to(tr_dev, dtype=s.dtype)
y_emb = apply_ip(y_emb, i + 1)
if use_ckpt:
s, y_emb = grad_ckpt(tr.patch_blocks[i + 1], s, y_emb, condition, pos, pos_txt, None, use_reentrant=False)
else:
s, y_emb = tr.patch_blocks[i + 1](s, y_emb, condition, pos, pos_txt, None)
# Remaining blocks
for i in range(self.copy_blocks_num + 1, tr.patch_depth):
y_emb = apply_ip(y_emb, i)
if use_ckpt:
s, y_emb = grad_ckpt(tr.patch_blocks[i], s, y_emb, condition, pos, pos_txt, None, use_reentrant=False)
else:
s, y_emb = tr.patch_blocks[i](s, y_emb, condition, pos, pos_txt, None)
# Pixel pathway (unchanged from base model)
s = F.silu(t_emb + s)
s_cond = s.view(B * L, tr.hidden_size)
x_pixels = tr.pixel_embedder(x, img_height=H, img_width=W, patch_size=tr.patch_size)
for blk in tr.pixel_blocks:
x_pixels = blk(x_pixels, s_cond, H, W, tr.patch_size, None)
x_pixels = tr.final_layer(x_pixels)
P2 = tr.patch_size ** 2
x_pixels = x_pixels.view(B, L, P2, tr.out_channels).permute(0, 3, 2, 1).contiguous()
x_pixels = x_pixels.view(B, tr.out_channels * P2, L)
return F.fold(x_pixels, (H, W), kernel_size=tr.patch_size, stride=tr.patch_size)
# ------------------------------------------------------------------
# Utilities
# ------------------------------------------------------------------
def unwrap_transformer(pixeldit_model):
"""Extract the inner nn.Module from a PixelDiTModel wrapper."""
return pixeldit_model.model
def _load_tensors(path: str):
"""Load a flat tensor dict from either .safetensors or .pt/.pth."""
if path.endswith(".safetensors"):
from safetensors.torch import load_file
return load_file(path, device="cpu"), {}
ckpt = torch.load(path, map_location="cpu", weights_only=True)
# legacy nested format — extract metadata separately
meta = {k: v for k, v in ckpt.items() if not isinstance(v, (dict, list, torch.Tensor))}
return ckpt, meta
def _unflatten_to_state_dict(flat: dict, prefix: str) -> dict:
"""Extract a sub-state-dict from a flat safetensors dict."""
p = prefix + "."
return {k[len(p):]: v for k, v in flat.items() if k.startswith(p)}
def _unflatten_list_of_state_dicts(flat: dict, prefix: str, n: int):
"""Rebuild a list of state dicts from flat safetensors keys."""
result = []
for i in range(n):
p = f"{prefix}.{i}."
sd = {k[len(p):]: v for k, v in flat.items() if k.startswith(p)}
result.append(sd)
return result
def load_checkpoint(model: PixelDiTControlNet, path: str) -> int:
"""Load a combined ControlNet + IP-Adapter checkpoint (.safetensors or .pt)."""
flat, meta = _load_tensors(path)
if path.endswith(".safetensors"):
model.controlnet_blocks.load_state_dict(_unflatten_to_state_dict(flat, "controlnet_blocks"))
siglip_sd = _unflatten_to_state_dict(flat, "siglip_y_proj")
if siglip_sd:
model.siglip_y_proj.load_state_dict(siglip_sd)
ip_proj_sd = _unflatten_to_state_dict(flat, "ip_proj")
if ip_proj_sd:
model.ip_proj.load_state_dict(ip_proj_sd)
n = len(model.ip_k)
for layer, sd in zip(model.ip_k, _unflatten_list_of_state_dicts(flat, "ip_k", n)):
layer.load_state_dict(sd)
for layer, sd in zip(model.ip_v, _unflatten_list_of_state_dicts(flat, "ip_v", n)):
layer.load_state_dict(sd)
model.ip_scale.data.copy_(flat["ip_scale"].to(model.ip_scale.device, dtype=model.ip_scale.dtype))
step = int(meta.get("step", 0))
else:
model.controlnet_blocks.load_state_dict(flat["controlnet_blocks"])
if "siglip_y_proj" in flat:
model.siglip_y_proj.load_state_dict(flat["siglip_y_proj"])
if "ip_proj" in flat:
model.ip_proj.load_state_dict(flat["ip_proj"])
for layer, state in zip(model.ip_k, flat["ip_k"]):
layer.load_state_dict(state)
for layer, state in zip(model.ip_v, flat["ip_v"]):
layer.load_state_dict(state)
model.ip_scale.data.copy_(flat["ip_scale"].to(model.ip_scale.device, dtype=model.ip_scale.dtype))
step = int(flat.get("step", 0))
return step
def load_ip_adapter_checkpoint(model: PixelDiTControlNet, path: str) -> int:
"""Load an IP-Adapter-only checkpoint on top of an existing ControlNet model."""
flat, meta = _load_tensors(path)
if path.endswith(".safetensors"):
model.ip_proj.load_state_dict(_unflatten_to_state_dict(flat, "ip_proj"))
n = len(model.ip_k)
for layer, sd in zip(model.ip_k, _unflatten_list_of_state_dicts(flat, "ip_k", n)):
layer.load_state_dict(sd)
for layer, sd in zip(model.ip_v, _unflatten_list_of_state_dicts(flat, "ip_v", n)):
layer.load_state_dict(sd)
model.ip_scale.data.copy_(flat["ip_scale"].to(model.ip_scale.device, dtype=model.ip_scale.dtype))
step = int(meta.get("step", 0))
else:
model.ip_proj.load_state_dict(flat["ip_proj"])
if len(flat["ip_k"]) != len(model.ip_k) or len(flat["ip_v"]) != len(model.ip_v):
raise ValueError("IP adapter checkpoint block count does not match PixelDiT patch depth")
for layer, state in zip(model.ip_k, flat["ip_k"]):
layer.load_state_dict(state)
for layer, state in zip(model.ip_v, flat["ip_v"]):
layer.load_state_dict(state)
model.ip_scale.data.copy_(flat["ip_scale"].to(model.ip_scale.device, dtype=model.ip_scale.dtype))
step = int(flat.get("step", 0))
return step