Cyronius's picture
Release category-conditioned sprite EMA weights and browser ONNX exports
1540c35 verified
Raw History Blame Contribute Delete
5.25 kB
# Klein-style single-stream DiT for 32x32 RGBA sprites, shared by train.py and prof_train.py; 2026-10-17.
import math
import torch, torch.nn as nn, torch.nn.functional as F
HEAD = 128
NCAT, NFP = 8, 3
# prof_train.py flips these to measure them; 'off' settings exist only for timing.
FAST = {'norm': True, 'rope': 'stack', 'qknorm': True, 'joint_qk': False, 'addcmul': False}
class RMSNorm(nn.Module):
def __init__(s, d):
super().__init__(); s.w = nn.Parameter(torch.ones(d))
def forward(s, x):
if FAST['norm']:
return F.rms_norm(x, (x.shape[-1],), s.w, 1e-6)
return x * torch.rsqrt(x.float().pow(2).mean(-1, keepdim=True) + 1e-6).to(x.dtype) * s.w
def rope_table(side):
# Half of each head encodes the row, half the column (2D RoPE, as in klein).
f = 1.0 / (10000 ** (torch.arange(0, HEAD // 4) / (HEAD // 4)))
ys, xs = torch.meshgrid(torch.arange(side), torch.arange(side), indexing='ij')
ang = torch.cat([ys.reshape(-1, 1) * f, xs.reshape(-1, 1) * f], 1)
return torch.cos(ang), torch.sin(ang)
def rope(x, cos, sin):
if FAST['rope'] == 'off':
return x
if FAST['rope'] == 'half':
# Rotate-half pairing (dim i with i+64): fewer passes over q/k than the interleaved form.
c, s_ = torch.cat([cos, cos], -1).to(x.dtype), torch.cat([sin, sin], -1).to(x.dtype)
x1, x2 = x.chunk(2, -1)
return x * c + torch.cat([-x2, x1], -1) * s_
if FAST['rope'] == 'stack':
a, b = x.unflatten(-1, (-1, 2)).unbind(-1)
cos, sin = cos.to(x.dtype), sin.to(x.dtype)
return torch.stack([a * cos - b * sin, a * sin + b * cos], -1).flatten(-2)
a, b = x.float().unflatten(-1, (-1, 2)).unbind(-1)
return torch.stack([a * cos - b * sin, a * sin + b * cos], -1).flatten(-2).to(x.dtype)
class Block(nn.Module):
def __init__(s, h):
super().__init__()
s.h, s.nh, s.mlp = h, h // HEAD, 3 * h
s.norm = nn.LayerNorm(h, elementwise_affine=False, eps=1e-6)
s.mod = nn.Linear(h, 3 * h)
s.lin1 = nn.Linear(h, 3 * h + 2 * s.mlp)
s.qn, s.kn = RMSNorm(HEAD), RMSNorm(HEAD)
s.lin2 = nn.Linear(h + s.mlp, h)
nn.init.zeros_(s.mod.weight); nn.init.zeros_(s.mod.bias)
def forward(s, x, c, cos, sin):
B, T, _ = x.shape
sh, sc, g = s.mod(F.silu(c)).unsqueeze(1).chunk(3, -1)
xm = torch.addcmul(sh, s.norm(x), 1 + sc) if FAST['addcmul'] else s.norm(x) * (1 + sc) + sh
qkv, a, b = s.lin1(xm).split([3 * s.h, s.mlp, s.mlp], -1)
qkv = qkv.view(B, T, 3, s.nh, HEAD).permute(2, 0, 3, 1, 4)
v = qkv[2]
if FAST['joint_qk']:
qk = qkv[:2]
if FAST['qknorm']:
qk = F.rms_norm(qk, (HEAD,), eps=1e-6) * torch.stack([s.qn.w, s.kn.w]).view(2, 1, 1, 1, HEAD).to(qk.dtype)
q, k = rope(qk, cos, sin).unbind(0)
else:
q, k = qkv[0], qkv[1]
if FAST['qknorm']:
q, k = s.qn(q), s.kn(k)
q, k = rope(q, cos, sin), rope(k, cos, sin)
o = F.scaled_dot_product_attention(q, k, v).transpose(1, 2).reshape(B, T, s.h)
out = s.lin2(torch.cat([o, F.silu(a) * b], -1))
return torch.addcmul(x, g, out) if FAST['addcmul'] else x + g * out
class DiT(nn.Module):
def __init__(s, h, depth, patch, vocab=0):
super().__init__()
s.p, s.side = patch, 32 // patch
s.inp = nn.Linear(4 * patch * patch, h)
s.t1, s.t2 = nn.Linear(256, h), nn.Linear(h, h)
s.cat = nn.Embedding(NCAT + 1, h) # last index = no label
s.fp = nn.Embedding(NFP + 1, h)
if vocab: # caption words: learned embeddings, mean-pooled, 0 = padding/none
s.wemb = nn.Embedding(vocab, h, padding_idx=0)
s.wproj = nn.Sequential(nn.Linear(h, h), nn.SiLU(), nn.Linear(h, h))
s.blocks = nn.ModuleList([Block(h) for _ in range(depth)])
s.fnorm = nn.LayerNorm(h, elementwise_affine=False, eps=1e-6)
s.fmod = nn.Linear(h, 2 * h)
s.outp = nn.Linear(h, 4 * patch * patch)
for m in (s.fmod, s.outp):
nn.init.zeros_(m.weight); nn.init.zeros_(m.bias)
cos, sin = rope_table(s.side)
s.register_buffer('cos', cos, persistent=False); s.register_buffer('sin', sin, persistent=False)
def forward(s, x, t, cat, fp, txt=None):
B, p, n = x.shape[0], s.p, s.side
x = x.unfold(2, p, p).unfold(3, p, p).permute(0, 2, 3, 1, 4, 5).reshape(B, n * n, 4 * p * p)
half = torch.exp(-math.log(10000) * torch.arange(128, device=x.device) / 128)
te = torch.cat([torch.cos(t[:, None] * 1000 * half), torch.sin(t[:, None] * 1000 * half)], -1)
c = s.t2(F.silu(s.t1(te))) + s.cat(cat) + s.fp(fp)
if txt is not None:
m = (txt > 0).to(c.dtype)
e = (s.wemb(txt) * m[..., None]).sum(1) / m.sum(1, keepdim=True).clamp(min=1)
c = c + s.wproj(e) * (m.sum(1, keepdim=True) > 0)
x = s.inp(x)
for blk in s.blocks:
x = blk(x, c, s.cos, s.sin)
sh, sc = s.fmod(F.silu(c)).unsqueeze(1).chunk(2, -1)
x = s.outp(s.fnorm(x) * (1 + sc) + sh)
return x.view(B, n, n, 4, p, p).permute(0, 3, 1, 4, 2, 5).reshape(B, 4, 32, 32)