OthmaneW's picture
Publish DiffuThink Story 512 model bundle
c6f3061 verified
Raw History Blame Contribute Delete
7.28 kB
"""Bidirectional Transformer: RMSNorm, RoPE, SwiGLU and noise conditioning.
These are packaged versions of the concepts explored in experiments/.
No pretrained weights or high-level Transformer implementation are used.
"""
from dataclasses import dataclass
import math
import torch
from torch import nn
from torch.nn import functional as F
PAD, MASK = 256, 257
VOCAB_SIZE = 258
def encode(text: str) -> list[int]:
return list(text.encode("utf-8"))
def decode(tokens) -> str:
return bytes(int(t) for t in tokens if 0 <= int(t) < 256).decode("utf-8", errors="replace")
@dataclass
class ModelConfig:
width: int = 64
heads: int = 4
layers: int = 2
max_length: int = 64
def __post_init__(self):
if min(self.width, self.heads, self.layers, self.max_length) < 1:
raise ValueError("Model dimensions must be positive")
if self.width % self.heads or (self.width // self.heads) % 2:
raise ValueError("width / heads must be an even integer for RoPE")
class RMSNorm(nn.Module):
def __init__(self, width):
super().__init__()
self.weight = nn.Parameter(torch.ones(width))
def forward(self, x):
return x * torch.rsqrt(x.float().square().mean(-1, keepdim=True) + 1e-6).to(x.dtype) * self.weight
def rope(x):
length, dim = x.shape[-2:]
freq = 10000 ** (-torch.arange(0, dim, 2, device=x.device).float() / dim)
angles = torch.outer(torch.arange(length, device=x.device), freq)
c, s = angles.cos().to(x.dtype), angles.sin().to(x.dtype)
a, b = x[..., ::2], x[..., 1::2]
return torch.stack((a * c - b * s, a * s + b * c), -1).flatten(-2)
class Block(nn.Module):
def __init__(self, cfg):
super().__init__()
w = cfg.width
self.heads = cfg.heads
self.norm1, self.norm2 = RMSNorm(w), RMSNorm(w)
self.time = nn.Linear(w, w, bias=False)
self.qkv = nn.Linear(w, 3 * w, bias=False)
self.out = nn.Linear(w, w, bias=False)
self.gate = nn.Linear(w, 4 * w, bias=False)
self.up = nn.Linear(w, 4 * w, bias=False)
self.down = nn.Linear(4 * w, w, bias=False)
def forward(self, x, time, valid, causal=False):
x = x + self.time(time)[:, None]
b, n, w = x.shape
q, k, v = self.qkv(self.norm1(x)).chunk(3, -1)
q, k, v = [z.reshape(b, n, self.heads, w // self.heads).transpose(1, 2) for z in (q, k, v)]
# Bidirectional: visible bytes on both sides can inform a missing byte.
if causal and valid is None:
# Caller guarantees right padding; supervised tokens cannot see future PADs.
y = F.scaled_dot_product_attention(rope(q), rope(k), v, is_causal=True)
else:
attention_mask = valid[:, None, None, :]
if causal:
attention_mask = attention_mask & torch.ones(n, n, device=x.device, dtype=torch.bool).tril()
y = F.scaled_dot_product_attention(rope(q), rope(k), v, attn_mask=attention_mask)
x = x + self.out(y.transpose(1, 2).reshape(b, n, w))
z = self.norm2(x)
return x + self.down(F.silu(self.gate(z)) * self.up(z))
class DiffuThink(nn.Module):
def __init__(self, config: ModelConfig):
super().__init__()
self.config = config
self.embedding = nn.Embedding(VOCAB_SIZE, config.width, padding_idx=PAD)
self.time_mlp = nn.Sequential(nn.Linear(config.width, config.width), nn.SiLU(), nn.Linear(config.width, config.width))
self.blocks = nn.ModuleList(Block(config) for _ in range(config.layers))
self.norm = RMSNorm(config.width)
self.head = nn.Linear(config.width, 256, bias=False)
def forward(self, tokens, noise):
if tokens.ndim != 2 or tokens.shape[1] > self.config.max_length:
raise ValueError("Expected [batch, length] within max_length")
half = self.config.width // 2
freq = torch.exp(-math.log(10000) * torch.arange(half, device=tokens.device) / max(half - 1, 1))
angles = noise[:, None] * 1000 * freq
time = self.time_mlp(torch.cat((angles.sin(), angles.cos()), -1))
x = self.embedding(tokens)
for block in self.blocks:
x = block(x, time, tokens.ne(PAD))
return self.head(self.norm(x))
def corrupt(clean, noise, generator=None):
"""Bernoulli masking, with at least one target per nonempty example."""
valid = clean.ne(PAD)
if not valid.any(-1).all():
raise ValueError("Empty examples cannot be corrupted")
selected = (torch.rand(clean.shape, device=clean.device, generator=generator) < noise[:, None]) & valid
missing = ~selected.any(-1)
# Choose a valid position uniformly, without assuming padding placement.
fallback = torch.rand(clean.shape, device=clean.device, generator=generator).masked_fill(~valid, -1).argmax(-1)
selected[missing, fallback[missing]] = True
return clean.masked_fill(selected, MASK), selected
def masked_loss(logits, clean, selected):
return F.cross_entropy(logits[selected], clean[selected])
@torch.inference_mode()
def generate(model, template: str, steps=12, temperature=0.8, seed=42):
"""Each ~ represents ONE missing byte. Commit most confident bytes first.
The fixed context is never overwritten. This is a heuristic iterative
unmasking sampler, not an exact reverse-diffusion likelihood sampler.
"""
if steps < 1 or temperature < 0 or not math.isfinite(temperature):
raise ValueError("steps must be positive and temperature finite and nonnegative")
ids = [MASK if b == ord("~") else b for b in encode(template)]
if not ids or len(ids) > model.config.max_length:
raise ValueError(f"Template must contain 1..{model.config.max_length} UTF-8 bytes")
device = next(model.parameters()).device
rng = torch.Generator(device=device).manual_seed(seed)
tokens = torch.tensor([ids], device=device)
total = int(tokens.eq(MASK).sum())
trace = []
model.eval()
for step in range(min(steps, total)):
unknown = tokens.eq(MASK)
remaining = int(unknown.sum())
noise = torch.tensor([remaining / tokens.shape[1]], device=device)
logits = model(tokens, noise)[0]
probs = (logits / max(temperature, 1e-6)).softmax(-1)
candidates = logits.argmax(-1) if temperature == 0 else torch.multinomial(probs, 1, generator=rng).squeeze(-1)
confidence = probs.gather(-1, candidates[:, None]).squeeze(-1).masked_fill(~unknown[0], -1)
# Integer arithmetic avoids rounding a remaining count of 1 down to 0.
iterations = min(steps, total)
target_remaining = total * (iterations - step - 1) // iterations
positions = confidence.topk(remaining - target_remaining).indices
tokens[0, positions] = candidates[positions]
trace.append({"step": step + 1, "remaining": int(tokens.eq(MASK).sum()), "text": display(tokens[0].tolist())})
return {"text": decode(tokens[0].tolist()), "tokens": tokens[0].tolist(), "trace": trace, "seed": seed}
def display(ids):
# Decode runs together so a valid UTF-8 character is not split for display.
return b"".join(b"~" if t == MASK else bytes([t]) if t < 256 else b"" for t in ids).decode("utf-8", errors="replace")