Download tiny_video/model.py from Kompaktion/Movetoy: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/Kompaktion/Movetoy/resolve/main/tiny_video/model.py
- Command line
-
hf download hf://Kompaktion/Movetoy/tiny_video/model.py
-
curl -L -o model.py https://huggingface.co/Kompaktion/Movetoy/resolve/main/tiny_video/model.py
16 kB
| """A tiny text-to-video diffusion transformer, trained from random init with rectified flow. | |
| Video [B,3,T,H,W] is cut into (1 x p x p) patches. Each block runs spatial attention | |
| within a frame, temporal attention across frames at the same patch position, | |
| cross-attention to the caption, and an MLP, all modulated (adaLN-Zero) by the | |
| diffusion time and pooled caption. The caption encoder is a 2-layer transformer | |
| over the small word vocabulary in data.py. | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import math | |
| from dataclasses import asdict, dataclass | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from torch.utils.checkpoint import checkpoint | |
| from .data import NULL, PAD, VOCAB, MAX_TOKENS | |
| from .world import BYTE_VOCAB, HYBRID_VOCAB, MAX_BYTES, MAX_HYBRID, NULL as BYTE_NULL, PAD as BYTE_PAD | |
| def specials(text: str) -> tuple[int, int]: | |
| """(pad id, null id) for the caption tokenizer a model was trained with.""" | |
| return (BYTE_PAD, BYTE_NULL) if text in ("bytes", "hybrid") else (PAD, NULL) | |
| class ModelConfig: | |
| frames: int = 16 | |
| size: int = 48 | |
| patch: int = 4 | |
| dim: int = 384 | |
| depth: int = 8 | |
| heads: int = 6 | |
| text_depth: int = 2 | |
| vocab_size: int = len(VOCAB) | |
| max_tokens: int = MAX_TOKENS | |
| mlp_ratio: float = 4.0 | |
| attn: str = "factorized" | |
| rope: bool = False | |
| text: str = "words" # "bytes": raw UTF-8 byte tokens (world.ByteTokenizer), no vocabulary at all # 3D rotary positions (t, y, x) in self-attention: motion becomes a relative pattern # "joint": the first attention in each block spans all frames, not just one | |
| grad_checkpoint: bool = False # recompute block activations in backward: far less VRAM, ~30% slower | |
| # ---------------------------------------------------------------- building blocks | |
| def modulate(x, shift, scale): | |
| return x * (1 + scale) + shift | |
| class Attention(nn.Module): | |
| def __init__(self, dim: int, heads: int, cross: bool = False): | |
| super().__init__() | |
| self.heads = heads | |
| self.q = nn.Linear(dim, dim) | |
| self.kv = nn.Linear(dim, 2 * dim) | |
| self.out = nn.Linear(dim, dim) | |
| hd = dim // heads | |
| self.q_norm = nn.RMSNorm(hd) | |
| self.k_norm = nn.RMSNorm(hd) | |
| def forward(self, x, ctx=None, mask=None, rope=None): | |
| B, N, D = x.shape | |
| ctx = x if ctx is None else ctx | |
| q = self.q(x).view(B, N, self.heads, -1).transpose(1, 2) | |
| k, v = self.kv(ctx).view(B, ctx.shape[1], 2, self.heads, -1).permute(2, 0, 3, 1, 4) | |
| q, k = self.q_norm(q), self.k_norm(k) | |
| if rope is not None: | |
| q, k = apply_rope(q, rope), apply_rope(k, rope) | |
| o = F.scaled_dot_product_attention(q, k, v, attn_mask=mask) | |
| return self.out(o.transpose(1, 2).reshape(B, N, D)) | |
| def rope_table(t, y, x, head_dim: int, base: float = 100.0): | |
| """cos/sin [L, head_dim/2] for integer positions t, y, x (each [L]). Head dims split 1/4 t, 3/8 y, 3/8 x.""" | |
| pairs = head_dim // 2 | |
| pt = pairs // 4 | |
| py = (pairs - pt) // 2 | |
| angles = [] | |
| for pos, n in ((t, pt), (y, py), (x, pairs - pt - py)): | |
| freqs = base ** (-torch.arange(n, device=pos.device, dtype=torch.float32) / max(n, 1)) | |
| angles.append(pos.float()[:, None] * freqs[None]) | |
| a = torch.cat(angles, dim=1) | |
| return a.cos(), a.sin() | |
| def apply_rope(x, rope): | |
| """Rotate consecutive channel pairs of x [B,H,L,hd] by the angles in rope.""" | |
| cos, sin = rope | |
| x1, x2 = x.float().unflatten(-1, (-1, 2)).unbind(-1) | |
| out = torch.stack([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1).flatten(-2) | |
| return out.to(x.dtype) | |
| class Mlp(nn.Sequential): | |
| def __init__(self, dim: int, ratio: float): | |
| hidden = int(dim * ratio) | |
| super().__init__(nn.Linear(dim, hidden), nn.GELU(approximate="tanh"), nn.Linear(hidden, dim)) | |
| class Block(nn.Module): | |
| def __init__(self, cfg: ModelConfig): | |
| super().__init__() | |
| D = cfg.dim | |
| self.norms = nn.ModuleList([nn.LayerNorm(D, elementwise_affine=False, eps=1e-6) for _ in range(4)]) | |
| self.spatial = Attention(D, cfg.heads) | |
| self.temporal = Attention(D, cfg.heads) | |
| self.cross = Attention(D, cfg.heads, cross=True) | |
| self.mlp = Mlp(D, cfg.mlp_ratio) | |
| self.ada = nn.Sequential(nn.SiLU(), nn.Linear(D, 11 * D)) | |
| assert cfg.attn in ("factorized", "joint"), cfg.attn | |
| self.joint = cfg.attn == "joint" | |
| def forward(self, x, c, ctx, ctx_mask, ropes=None): | |
| # x [B,T,N,D], c [B,D], ctx [B,L,D], ctx_mask [B,1,1,L] bool (True = attend) | |
| B, T, N, D = x.shape | |
| (s1, sc1, g1, s2, sc2, g2, g3, s4, sc4, g4, sc3) = self.ada(c)[:, None, None].chunk(11, dim=-1) | |
| r_joint, r_space, r_time = ropes or (None, None, None) | |
| if self.joint: | |
| h = modulate(self.norms[0](x), s1, sc1).reshape(B, T * N, D) | |
| x = x + g1 * self.spatial(h, rope=r_joint).view(B, T, N, D) | |
| else: | |
| h = modulate(self.norms[0](x), s1, sc1).reshape(B * T, N, D) | |
| x = x + g1 * self.spatial(h, rope=r_space).view(B, T, N, D) | |
| h = modulate(self.norms[1](x), s2, sc2).transpose(1, 2).reshape(B * N, T, D) | |
| x = x + g2 * self.temporal(h, rope=r_time).view(B, N, T, D).transpose(1, 2) | |
| h = (self.norms[2](x) * (1 + sc3)).reshape(B, T * N, D) | |
| x = x + g3 * self.cross(h, ctx, ctx_mask).view(B, T, N, D) | |
| h = modulate(self.norms[3](x), s4, sc4) | |
| return x + g4 * self.mlp(h) | |
| class TextEncoder(nn.Module): | |
| def __init__(self, cfg: ModelConfig): | |
| super().__init__() | |
| self.pad = specials(cfg.text)[0] | |
| self.embed = nn.Embedding(cfg.vocab_size, cfg.dim) | |
| self.pos = nn.Parameter(torch.randn(cfg.max_tokens, cfg.dim) * 0.02) | |
| layer = nn.TransformerEncoderLayer(cfg.dim, cfg.heads, int(cfg.dim * cfg.mlp_ratio), dropout=0.0, | |
| activation="gelu", batch_first=True, norm_first=True) | |
| self.layers = nn.TransformerEncoder(layer, cfg.text_depth, enable_nested_tensor=False) | |
| self.norm = nn.LayerNorm(cfg.dim) | |
| def forward(self, tokens): | |
| pad = tokens == self.pad | |
| h = self.embed(tokens) + self.pos[: tokens.shape[1]] | |
| h = self.norm(self.layers(h, src_key_padding_mask=pad)) | |
| keep = (~pad).unsqueeze(-1).to(h.dtype) | |
| pooled = (h * keep).sum(1) / keep.sum(1).clamp(min=1) | |
| return h, pooled, (~pad)[:, None, None, :] | |
| def timestep_embedding(t, dim: int): | |
| half = dim // 2 | |
| freqs = torch.exp(-math.log(10000) * torch.arange(half, device=t.device, dtype=torch.float32) / half) | |
| args = (t.float() * 1000)[:, None] * freqs[None] | |
| return torch.cat([torch.cos(args), torch.sin(args)], dim=-1) | |
| # ---------------------------------------------------------------- the model | |
| class VideoDiT(nn.Module): | |
| def __init__(self, cfg: ModelConfig | None = None): | |
| super().__init__() | |
| self.cfg = cfg = cfg or ModelConfig() | |
| assert cfg.size % cfg.patch == 0 and cfg.dim % cfg.heads == 0 and cfg.attn in ("factorized", "joint") | |
| D, p = cfg.dim, cfg.patch | |
| self.grid = cfg.size // p | |
| self.patch_embed = nn.Conv3d(3, D, kernel_size=(1, p, p), stride=(1, p, p)) | |
| self.pos_space = nn.Parameter(torch.randn(self.grid * self.grid, D) * 0.02) | |
| self.pos_time = nn.Parameter(torch.randn(cfg.frames, D) * 0.02) | |
| self.t_embed = nn.Sequential(nn.Linear(256, D), nn.SiLU(), nn.Linear(D, D)) | |
| self.text = TextEncoder(cfg) | |
| self.text_proj = nn.Linear(D, D) | |
| 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_ada = nn.Sequential(nn.SiLU(), nn.Linear(D, 2 * D)) | |
| self.final = nn.Linear(D, p * p * 3) | |
| 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 | |
| nn.init.xavier_uniform_(w.view(w.shape[0], -1)) | |
| nn.init.normal_(self.text.embed.weight, std=0.02) | |
| for b in self.blocks: # adaLN-Zero: every residual branch starts switched off | |
| nn.init.zeros_(b.ada[-1].weight) | |
| nn.init.zeros_(b.ada[-1].bias) | |
| for m in (self.final_ada[-1], self.final): | |
| nn.init.zeros_(m.weight) | |
| nn.init.zeros_(m.bias) | |
| def _ropes(self, T: int, device): | |
| g, hd = self.grid, self.cfg.dim // self.cfg.heads | |
| tt, yy, xx = torch.meshgrid(torch.arange(T, device=device), torch.arange(g, device=device), | |
| torch.arange(g, device=device), indexing="ij") | |
| joint = rope_table(tt.flatten(), yy.flatten(), xx.flatten(), hd) | |
| zeros = torch.zeros(g * g, device=device, dtype=torch.long) | |
| space = rope_table(zeros, yy[0].flatten(), xx[0].flatten(), hd) | |
| time = rope_table(torch.arange(T, device=device), zeros[:T], zeros[:T], hd) | |
| return joint, space, time | |
| def forward(self, x, t, tokens): | |
| """x [B,3,T,H,W], t [B] in [0,1], tokens [B,L] -> predicted velocity [B,3,T,H,W].""" | |
| B, _, T, H, W = x.shape | |
| h = self.patch_embed(x) # [B,D,T,g,g] | |
| h = h.flatten(3).permute(0, 2, 3, 1) # [B,T,N,D] | |
| h = h + self.pos_space[None, None] + self.pos_time[None, :T, None] | |
| ctx, pooled, ctx_mask = self.text(tokens) | |
| c = self.t_embed(timestep_embedding(t, 256)) + self.text_proj(pooled) | |
| ropes = self._ropes(T, x.device) if self.cfg.rope else None | |
| for blk in self.blocks: | |
| if self.cfg.grad_checkpoint and self.training and torch.is_grad_enabled(): | |
| h = checkpoint(blk, h, c, ctx, ctx_mask, ropes, use_reentrant=False) | |
| else: | |
| h = blk(h, c, ctx, ctx_mask, ropes) | |
| shift, scale = self.final_ada(c)[:, None, None].chunk(2, dim=-1) | |
| h = self.final(modulate(self.final_norm(h), shift, scale)) # [B,T,N,p*p*3] | |
| p, g = self.cfg.patch, self.grid | |
| h = h.view(B, T, g, g, p, p, 3).permute(0, 6, 1, 2, 4, 3, 5) | |
| return h.reshape(B, 3, T, H, W) | |
| # ---------------------------------------------------------------- training / sampling | |
| def null_tokens(tokens: torch.Tensor, text: str = "words") -> torch.Tensor: | |
| pad, null = specials(text) | |
| out = torch.full_like(tokens, pad) | |
| out[:, 0] = null | |
| return out | |
| def flow_loss(model: VideoDiT, x0: torch.Tensor, tokens: torch.Tensor, p_uncond: float = 0.1) -> torch.Tensor: | |
| """Rectified flow: x_t = (1-t) x0 + t eps, regress v = eps - x0. Logit-normal t.""" | |
| B = x0.shape[0] | |
| t = torch.sigmoid(torch.randn(B, device=x0.device)) | |
| eps = torch.randn_like(x0) | |
| tt = t.view(B, 1, 1, 1, 1) | |
| xt = (1 - tt) * x0 + tt * eps | |
| drop = torch.rand(B, device=x0.device) < p_uncond | |
| tokens = torch.where(drop[:, None], null_tokens(tokens, model.cfg.text), tokens) | |
| v = model(xt, t, tokens) | |
| return F.mse_loss(v.float(), (eps - x0).float()) | |
| def sample(model: VideoDiT, tokens: torch.Tensor, steps: int = 40, cfg_scale: float = 4.0, | |
| generator: torch.Generator | None = None, shift: float = 2.0) -> torch.Tensor: | |
| """Euler integration of the flow from noise (t=1) to data (t=0). Returns [B,3,T,H,W] in [-1,1]. | |
| `shift` > 1 spends more steps at high noise, where the layout and motion are decided. | |
| """ | |
| cfg = model.cfg | |
| device = next(model.parameters()).device | |
| tokens = tokens.to(device) | |
| B = tokens.shape[0] | |
| x = torch.randn(B, 3, cfg.frames, cfg.size, cfg.size, device=device, generator=generator) | |
| u = torch.linspace(1, 0, steps + 1, device=device) | |
| ts = shift * u / (1 + (shift - 1) * u) | |
| cond = torch.cat([tokens, null_tokens(tokens, cfg.text)]) if cfg_scale != 1 else tokens | |
| for i in range(steps): | |
| t = ts[i].expand(cond.shape[0]) | |
| xin = torch.cat([x, x]) if cfg_scale != 1 else x | |
| with torch.autocast(device.type, dtype=torch.bfloat16, enabled=device.type == "cuda"): | |
| v = model(xin, t, cond).float() | |
| if cfg_scale != 1: | |
| vc, vu = v.chunk(2) | |
| v = vu + cfg_scale * (vc - vu) | |
| x = x + (ts[i + 1] - ts[i]) * v | |
| return x.clamp(-1, 1) | |
| def to_uint8(x: torch.Tensor) -> torch.Tensor: | |
| """[B,3,T,H,W] in [-1,1] -> uint8 [B,T,H,W,3] on CPU.""" | |
| return ((x.float().clamp(-1, 1) + 1) * 127.5).round().to(torch.uint8).permute(0, 2, 3, 4, 1).cpu() | |
| class EMA: | |
| def __init__(self, model: nn.Module, decay: float = 0.999): | |
| self.decay = decay | |
| self.model = copy.deepcopy(model).eval().requires_grad_(False) | |
| def update(self, model: nn.Module): | |
| for e, p in zip(self.model.parameters(), model.parameters()): | |
| e.lerp_(p.detach(), 1 - self.decay) | |
| def save_checkpoint(path, model: VideoDiT, opt=None, step: int = 0, extra: dict | None = None, ema: EMA | None = None): | |
| torch.save({ | |
| "config": asdict(model.cfg), | |
| "model": model.state_dict(), | |
| "ema": ema.model.state_dict() if ema else None, | |
| "opt": opt.state_dict() if opt else None, | |
| "step": step, | |
| "extra": extra or {}, | |
| }, path) | |
| def load_checkpoint(path, device="cpu", use_ema: bool = True): | |
| ckpt = torch.load(path, map_location=device, weights_only=False) | |
| model = VideoDiT(ModelConfig(**ckpt["config"])) | |
| state = ckpt["ema"] if use_ema and ckpt.get("ema") else ckpt["model"] | |
| model.load_state_dict(state) | |
| return model.to(device).eval(), ckpt | |
| def tokenizer_for(cfg: ModelConfig): | |
| """The caption tokenizer matching a model's config (v1 word vocabulary or v2 raw bytes).""" | |
| if cfg.text == "bytes": | |
| from .world import ByteTokenizer | |
| return ByteTokenizer(cfg.max_tokens) | |
| if cfg.text == "hybrid": | |
| from .world import HybridTokenizer | |
| return HybridTokenizer(cfg.max_tokens) | |
| from .data import Tokenizer | |
| return Tokenizer(cfg.max_tokens) | |
| def byte_model_config(**kw) -> ModelConfig: | |
| """Defaults for world-v2 models: byte captions, joint space-time attention, 3D RoPE.""" | |
| base = dict(frames=16, size=32, patch=4, dim=384, depth=8, heads=6, text_depth=4, vocab_size=BYTE_VOCAB, | |
| max_tokens=MAX_BYTES, attn="joint", rope=True, text="bytes") | |
| return ModelConfig(**{**base, **kw}) | |
| def hybrid_model_config(**kw) -> ModelConfig: | |
| """byte_model_config with hashed-word tokens added to the caption (text="hybrid").""" | |
| return byte_model_config(**{"text": "hybrid", "vocab_size": HYBRID_VOCAB, "max_tokens": MAX_HYBRID, **kw}) | |
| def load_matching(model: nn.Module, state: dict) -> dict: | |
| """Copy every tensor whose name and shape match; for embedding tables that grew, copy the overlapping rows. | |
| Returns {"loaded": [...], "partial": [...], "skipped": [...]} for provenance.""" | |
| own = model.state_dict() | |
| report = {"loaded": [], "partial": [], "skipped": []} | |
| with torch.no_grad(): | |
| for k, v in state.items(): | |
| if k not in own: | |
| report["skipped"].append(k) | |
| elif own[k].shape == v.shape: | |
| own[k].copy_(v) | |
| report["loaded"].append(k) | |
| elif own[k].ndim == v.ndim and all(a >= b for a, b in zip(own[k].shape, v.shape)): | |
| own[k][tuple(slice(0, n) for n in v.shape)].copy_(v) | |
| report["partial"].append(k) | |
| else: | |
| report["skipped"].append(k) | |
| return report | |