File size: 16,746 Bytes
dd1362a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 | # bora_dit.py -- Bora V1: a flow-matching video DiT over Wan2.1-VAE latents, plus the loss and the block sampler.
#
# Input: a block of F=4 latent frames [B,16,4,32,32] (normalized latents from bora_dataset.BoraLatents).
# Conditioning: T5 token embeddings [B,32,768] + mask (cross-attention), timestep t in [0,1] (adaLN),
# and optionally the clean first frame of the block (channel-concat + mask channel) so that block k
# can continue from the last frame of block k-1 at generation time.
# Objective: rectified flow. x_t = (1-t) x + t eps, model predicts v = eps - x, MSE on the frames it has to generate.
# Sampling: Euler, 8 steps by default, classifier-free guidance on the text, blocks chained with 1 frame of overlap.
#
# Sizes (dim/depth/heads): v1 = 1024/20/16 -> 340M params, 1024 tokens per block with patch (1,2,2).
import math
from dataclasses import dataclass, asdict
import torch
import torch.nn as nn
import torch.nn.functional as F
# ----------------------------------------------------------------------------- config
@dataclass
class DiTConfig:
in_channels: int = 16
frames: int = 4
height: int = 32
width: int = 32
patch: tuple = (1, 2, 2) # (t, h, w) -> 4 x 16 x 16 = 1024 tokens
dim: int = 1024
depth: int = 20
heads: int = 16
mlp_ratio: float = 4.0
text_dim: int = 768 # T5 v1.1 base
attn: str = "softmax" # "softmax" (3D RoPE) or "linear" (ReLU linear attention, learned positions)
qk_norm: bool = True
frame_cond: bool = True # accept a clean first frame (adds 16 + 1 input channels)
time_shift: float = 3.0 # SD3-style timestep shift toward noise, used in training and sampling
@property
def tokens(self):
return (self.frames // self.patch[0]) * (self.height // self.patch[1]) * (self.width // self.patch[2])
@property
def grid(self):
return (self.frames // self.patch[0], self.height // self.patch[1], self.width // self.patch[2])
PRESETS = {
"tiny": dict(dim=128, depth=2, heads=4), # smoke tests
"small": dict(dim=512, depth=12, heads=8), # ~50M, quick experiments
"v1": dict(dim=1024, depth=20, heads=16), # ~340M, the plan
}
# ----------------------------------------------------------------------------- pieces
def timestep_embedding(t, dim, max_period=10000):
"""t in [0,1] -> sinusoidal [B, dim]. Scaled by 1000 so the frequencies match the usual 0..1000 diffusion range."""
half = dim // 2
freqs = torch.exp(-math.log(max_period) * torch.arange(half, device=t.device, dtype=torch.float32) / half)
args = (t.float() * 1000.0)[:, None] * freqs[None]
return torch.cat([args.cos(), args.sin()], dim=-1)
class RMSNorm(nn.Module):
def __init__(self, dim, eps=1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x):
xf = x.float()
return (xf * torch.rsqrt(xf.pow(2).mean(-1, keepdim=True) + self.eps)).to(x.dtype) * self.weight
def rope_3d(grid, head_dim, theta=10000.0):
"""cos/sin tables [L, head_dim/2] for a (T,H,W) token grid. head_dim/2 angles split 1:1.5:1.5 between t, h, w."""
T, H, W = grid
n = head_dim // 2
nt = (n // 4) if n >= 4 else 1
nh = (n - nt) // 2
nw = n - nt - nh
def axis(num, pos):
freqs = 1.0 / (theta ** (torch.arange(num, dtype=torch.float32) / num))
return pos.float()[:, None] * freqs[None] # [P, num]
t_idx, h_idx, w_idx = torch.meshgrid(torch.arange(T), torch.arange(H), torch.arange(W), indexing="ij")
ang = torch.cat([axis(nt, t_idx.flatten()), axis(nh, h_idx.flatten()), axis(nw, w_idx.flatten())], dim=-1) # [L, n]
return ang.cos(), ang.sin()
def apply_rope(x, cos, sin):
"""x [B, heads, L, d]; pairs (i, i + d/2) are rotated by the angle i."""
x1, x2 = x.float().chunk(2, dim=-1)
out = torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
return out.to(x.dtype)
class Attention(nn.Module):
def __init__(self, dim, heads, qk_norm, kind="softmax", ctx_dim=None):
super().__init__()
self.heads, self.hd, self.kind = heads, dim // heads, kind
ctx_dim = ctx_dim or dim
self.q = nn.Linear(dim, dim)
self.kv = nn.Linear(ctx_dim, 2 * dim)
self.o = nn.Linear(dim, dim)
self.qn = RMSNorm(self.hd) if qk_norm else nn.Identity()
self.kn = RMSNorm(self.hd) if qk_norm else nn.Identity()
def forward(self, x, ctx=None, ctx_mask=None, rope=None):
B, L, _ = x.shape
ctx = x if ctx is None else ctx
q = self.q(x).view(B, L, self.heads, self.hd).transpose(1, 2) # [B,H,L,d]
k, v = self.kv(ctx).view(B, ctx.shape[1], 2, self.heads, self.hd).permute(2, 0, 3, 1, 4)
q, k = self.qn(q), self.kn(k)
if rope is not None:
q, k = apply_rope(q, *rope), apply_rope(k, *rope)
if self.kind == "linear":
out = self._linear(q, k, v)
else:
mask = None
if ctx_mask is not None:
mask = ctx_mask[:, None, None, :] # True = attend
out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
return self.o(out.transpose(1, 2).reshape(B, L, -1))
@staticmethod
def _linear(q, k, v, eps=1e-6):
"""ReLU linear attention (SANA). fp32 for the sums; O(L d^2) instead of O(L^2 d)."""
q, k, v = F.relu(q.float()), F.relu(k.float()), v.float()
kv = torch.einsum("bhld,bhle->bhde", k, v) # [B,H,d,d]
z = 1.0 / (torch.einsum("bhld,bhd->bhl", q, k.sum(2)) + eps) # [B,H,L]
out = torch.einsum("bhld,bhde->bhle", q, kv) * z[..., None]
return out.to(v.dtype) if v.dtype != torch.float32 else out
class Block(nn.Module):
"""adaLN-zero DiT block with a shared modulation table (PixArt-alpha): the 6 x dim modulation comes from one global
MLP on the timestep plus a per-block learned offset, instead of a 6 x dim^2 Linear per block."""
def __init__(self, cfg):
super().__init__()
d = cfg.dim
self.n1 = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
self.attn = Attention(d, cfg.heads, cfg.qk_norm, cfg.attn)
self.n2 = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
self.cross = Attention(d, cfg.heads, cfg.qk_norm, "softmax", ctx_dim=cfg.text_dim)
self.n3 = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
hidden = int(d * cfg.mlp_ratio)
self.mlp = nn.Sequential(nn.Linear(d, hidden), nn.GELU(approximate="tanh"), nn.Linear(hidden, d))
self.table = nn.Parameter(torch.randn(6, d) / d ** 0.5)
def forward(self, x, t6, text, text_mask, rope):
# t6: [B, 6, dim] from the shared timestep MLP
shift1, scale1, gate1, shift2, scale2, gate2 = (self.table[None] + t6).unbind(1)
h = self.n1(x) * (1 + scale1[:, None]) + shift1[:, None]
x = x + gate1[:, None] * self.attn(h, rope=rope)
x = x + self.cross(self.n2(x), ctx=text, ctx_mask=text_mask)
h = self.n3(x) * (1 + scale2[:, None]) + shift2[:, None]
x = x + gate2[:, None] * self.mlp(h)
return x
# ----------------------------------------------------------------------------- model
class BoraDiT(nn.Module):
def __init__(self, cfg: DiTConfig):
super().__init__()
self.cfg = cfg
self.grad_checkpoint = False # set True to trade ~2x block compute for ~5x less activation memory
d, p = cfg.dim, cfg.patch
in_ch = cfg.in_channels + (cfg.in_channels + 1 if cfg.frame_cond else 0)
self.patch_embed = nn.Conv3d(in_ch, d, kernel_size=p, stride=p)
self.t_embed = nn.Sequential(nn.Linear(256, d), nn.SiLU(), nn.Linear(d, d))
self.t_mod = nn.Sequential(nn.SiLU(), nn.Linear(d, 6 * d)) # shared adaLN
self.text_proj = nn.Sequential(nn.Linear(cfg.text_dim, cfg.text_dim), nn.GELU(approximate="tanh"), nn.Linear(cfg.text_dim, cfg.text_dim))
self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.depth)])
self.final_norm = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
self.final_mod = nn.Sequential(nn.SiLU(), nn.Linear(d, 2 * d))
self.final = nn.Linear(d, p[0] * p[1] * p[2] * cfg.in_channels)
if cfg.attn == "linear": # no RoPE for linear attention
self.pos = nn.Parameter(torch.randn(1, cfg.tokens, d) * 0.02)
else:
cos, sin = rope_3d(cfg.grid, d // cfg.heads)
self.register_buffer("rope_cos", cos, persistent=False)
self.register_buffer("rope_sin", sin, persistent=False)
self._init()
def _init(self):
def basic(m):
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None:
nn.init.zeros_(m.bias)
self.apply(basic)
w = self.patch_embed.weight.data
nn.init.xavier_uniform_(w.view(w.shape[0], -1))
nn.init.zeros_(self.patch_embed.bias)
nn.init.normal_(self.t_embed[0].weight, std=0.02)
nn.init.normal_(self.t_embed[2].weight, std=0.02)
nn.init.zeros_(self.t_mod[1].weight); nn.init.zeros_(self.t_mod[1].bias) # adaLN-zero: gates start at 0
nn.init.zeros_(self.final_mod[1].weight); nn.init.zeros_(self.final_mod[1].bias)
nn.init.zeros_(self.final.weight); nn.init.zeros_(self.final.bias) # output starts at 0
for b in self.blocks:
nn.init.zeros_(b.cross.o.weight); nn.init.zeros_(b.cross.o.bias) # cross-attn starts silent
def unpatchify(self, x):
B = x.shape[0]
T, H, W = self.cfg.grid
pt, ph, pw = self.cfg.patch
c = self.cfg.in_channels
x = x.view(B, T, H, W, pt, ph, pw, c)
x = x.permute(0, 7, 1, 4, 2, 5, 3, 6) # B c T pt H ph W pw
return x.reshape(B, c, T * pt, H * ph, W * pw)
def forward(self, x_t, t, text, text_mask, cond=None, cond_mask=None):
"""x_t [B,16,F,H,W], t [B] in [0,1], text [B,L,768], text_mask [B,L] bool,
cond [B,16,F,H,W] clean frames (zeros where unknown), cond_mask [B,1,F,1,1] in {0,1}. Returns v [B,16,F,H,W]."""
B = x_t.shape[0]
if self.cfg.frame_cond:
if cond is None:
cond = torch.zeros_like(x_t)
cond_mask = torch.zeros(B, 1, x_t.shape[2], 1, 1, device=x_t.device, dtype=x_t.dtype)
m = cond_mask.expand(B, 1, *x_t.shape[2:]).to(x_t.dtype)
x_in = torch.cat([x_t, cond * m, m], dim=1)
else:
x_in = x_t
x = self.patch_embed(x_in).flatten(2).transpose(1, 2) # [B,L,dim]
if self.cfg.attn == "linear":
x = x + self.pos
rope = None
else:
rope = (self.rope_cos, self.rope_sin)
te = self.t_embed(timestep_embedding(t, 256).to(x.dtype)) # [B,dim]
t6 = self.t_mod(te).view(B, 6, -1)
text = self.text_proj(text.to(x.dtype))
for blk in self.blocks:
if self.grad_checkpoint and self.training:
x = torch.utils.checkpoint.checkpoint(blk, x, t6, text, text_mask, rope, use_reentrant=False)
else:
x = blk(x, t6, text, text_mask, rope)
shift, scale = self.final_mod(te).chunk(2, dim=-1)
x = self.final_norm(x) * (1 + scale[:, None]) + shift[:, None]
return self.unpatchify(self.final(x))
# ----------------------------------------------------------------------------- flow matching
def shift_t(u, shift):
"""SD3 timestep shift: pushes samples toward t=1 (more noise). Identity for shift=1."""
return shift * u / (1 + (shift - 1) * u)
def sample_timesteps(B, shift, device):
u = torch.sigmoid(torch.randn(B, device=device)) # logit-normal(0,1)
return shift_t(u, shift)
def make_frame_cond(x, p_cond, generator=None):
"""With probability p_cond per sample, give the clean first frame of the block as conditioning.
Returns cond [B,16,F,H,W], cond_mask [B,1,F,1,1], loss_mask [B,1,F,1,1] (0 on the given frame)."""
B, C, Fr, H, W = x.shape
use = (torch.rand(B, device=x.device, generator=generator) < p_cond).to(x.dtype) # [B]
cond_mask = torch.zeros(B, 1, Fr, 1, 1, device=x.device, dtype=x.dtype)
cond_mask[:, :, 0] = use.view(B, 1, 1, 1)
cond = torch.zeros_like(x)
cond[:, :, 0] = x[:, :, 0]
return cond, cond_mask, 1 - cond_mask
def flow_loss(model, x, text, text_mask, p_cond=0.5, shift=3.0, t=None, noise=None):
"""Rectified-flow MSE. x: clean block [B,16,F,H,W]. Returns (loss, dict of parts)."""
B = x.shape[0]
t = sample_timesteps(B, shift, x.device) if t is None else t
noise = torch.randn_like(x) if noise is None else noise
tt = t.view(B, 1, 1, 1, 1).to(x.dtype)
x_t = (1 - tt) * x + tt * noise
cond, cond_mask, loss_mask = make_frame_cond(x, p_cond)
v = model(x_t, t, text, text_mask, cond, cond_mask)
target = noise - x
err = (v.float() - target.float()) ** 2 * loss_mask
loss = err.sum() / (loss_mask.sum() * x.shape[1] * x.shape[3] * x.shape[4])
return loss, {"t_mean": float(t.mean()), "cond_frac": float(cond_mask[:, :, 0].mean())}
@torch.no_grad()
def sample_block(model, text, text_mask, null_text, null_mask, steps=8, cfg_scale=4.0, shift=3.0,
first_frame=None, generator=None, dtype=torch.bfloat16):
"""Euler sampler for one block. first_frame [B,16,H,W] (clean) continues a previous block; None starts fresh.
Returns clean latents [B,16,F,H,W] (float32)."""
c = model.cfg
B = text.shape[0]
dev = text.device
x = torch.randn(B, c.in_channels, c.frames, c.height, c.width, device=dev, generator=generator)
cond = cond_mask = None
eps0 = None
if first_frame is not None:
cond = torch.zeros_like(x)
cond[:, :, 0] = first_frame
cond_mask = torch.zeros(B, 1, c.frames, 1, 1, device=dev)
cond_mask[:, :, 0] = 1
eps0 = torch.randn(B, c.in_channels, c.height, c.width, device=dev, generator=generator)
ts = shift_t(torch.linspace(1, 0, steps + 1, device=dev), shift)
for i in range(steps):
t, t_next = ts[i], ts[i + 1]
if first_frame is not None: # keep the given frame on its own noising path
x[:, :, 0] = (1 - t) * first_frame + t * eps0
tb = t.expand(B)
with torch.autocast(device_type=dev.type, dtype=dtype, enabled=dev.type == "cuda"):
if cfg_scale != 1.0:
xx = torch.cat([x, x]); tt = torch.cat([tb, tb])
te = torch.cat([text, null_text]); tm = torch.cat([text_mask, null_mask])
cc = None if cond is None else torch.cat([cond, cond])
cm = None if cond_mask is None else torch.cat([cond_mask, cond_mask])
v_c, v_u = model(xx.to(dtype), tt, te, tm, cc if cc is None else cc.to(dtype), cm if cm is None else cm.to(dtype)).float().chunk(2)
v = v_u + cfg_scale * (v_c - v_u)
else:
v = model(x.to(dtype), tb, text, text_mask, None if cond is None else cond.to(dtype), None if cond_mask is None else cond_mask.to(dtype)).float()
x = x + (t_next - t) * v
if first_frame is not None:
x[:, :, 0] = first_frame
return x
@torch.no_grad()
def sample_video(model, text, text_mask, null_text, null_mask, n_blocks=3, overlap=1, **kw):
"""Chain blocks with `overlap` shared frames: 4 + 3 + 3 = 10 latent frames for n_blocks=3 (one full 37-frame clip)."""
blocks = []
first = None
for _ in range(n_blocks):
blk = sample_block(model, text, text_mask, null_text, null_mask, first_frame=first, **kw)
blocks.append(blk if first is None else blk[:, :, overlap:])
first = blk[:, :, -1]
return torch.cat(blocks, dim=2)
# ----------------------------------------------------------------------------- helpers
def build(preset="v1", **overrides):
cfg = DiTConfig(**{**PRESETS[preset], **overrides})
return BoraDiT(cfg), cfg
def count_params(model):
return sum(p.numel() for p in model.parameters())
def config_dict(cfg):
d = asdict(cfg)
d["patch"] = list(d["patch"])
return d
|