Download diffuthink/model.py from OthmaneW/StoryPatch-context-v3: direct link, hf CLI and curl.
- Browser
- Download file 7.28 kB
-
https://huggingface.co/OthmaneW/StoryPatch-context-v3/resolve/main/diffuthink/model.py
- Command line
-
hf download hf://OthmaneW/StoryPatch-context-v3/diffuthink/model.py
-
curl -L -o model.py https://huggingface.co/OthmaneW/StoryPatch-context-v3/resolve/main/diffuthink/model.py
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") | |
| 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]) | |
| 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") | |