OthmaneW's picture
Publish DiffuThink Story 512 model bundle
c6f3061 verified
Raw History Blame Contribute Delete
5.31 kB
"""Confidence-guided subword denoising and blockwise continuation."""
import math
from pathlib import Path
import time
import torch
from tokenizers import Tokenizer
from .model import Denoiser, PAD, BOS, EOS, MASK, UNK
from .train import amp
def load(directory, device="cpu"):
return Denoiser.from_pretrained(directory, device), Tokenizer.from_file(str(Path(directory) / "tokenizer.json"))
@torch.inference_mode()
def denoise(model, tokenizer, ids, *, steps=16, temperature=0.7, seed=42, top_k=40, precision="fp32", policy="confidence", threshold=0.85, allow_eos=True):
if not ids or len(ids) > model.config.max_length:
raise ValueError(f"Input exceeds {model.config.max_length} tokens")
if steps < 1 or not math.isfinite(temperature) or temperature < 0 or top_k < 1:
raise ValueError("Invalid sampling parameters")
if policy not in ("confidence", "left_to_right", "adaptive") or not 0 < threshold <= 1:
raise ValueError("Invalid policy or threshold")
if not 0 <= seed < 2**63:
raise ValueError("seed must be in 0..2^63-1")
device = str(next(model.parameters()).device).split(":")[0]
generator = torch.Generator(device=device).manual_seed(seed)
tokens = torch.tensor([ids], device=device)
fixed = tokens.ne(MASK)
count = int((~fixed).sum())
iterations = min(steps, count)
trace = []
model.eval()
started = time.perf_counter()
for step in range(iterations):
unknown = tokens.eq(MASK)
rate = unknown.sum(-1).float() / (tokens.ne(PAD) & tokens.ne(BOS)).sum(-1).clamp_min(1)
with amp(device, precision):
logits = model(tokens, rate)[0].float()
logits[:, [PAD, BOS, MASK, UNK]] = -torch.inf
if not allow_eos:
logits[:, EOS] = -torch.inf
# Ranking always uses the untempered distribution. Using T≈0 here
# would turn every argmax confidence into 1 and destroy its ordering.
confidence_probs = logits.softmax(-1)
if temperature == 0:
candidates = logits.argmax(-1)
else:
values, indices = logits.topk(min(top_k, logits.shape[-1]), -1)
choice = torch.multinomial((values / temperature).softmax(-1), 1, generator=generator)
candidates = indices.gather(-1, choice).squeeze(-1)
confidence = confidence_probs.gather(-1, candidates[:, None]).squeeze(-1).masked_fill(~unknown[0], -1)
# Cosine schedule keeps more holes early and commits all by the last pass.
remaining = int(unknown.sum())
target = min(remaining - 1, math.floor(count * math.cos((step + 1) / iterations * math.pi / 2)))
if step == iterations - 1:
target = 0
commit = remaining - max(0, target)
if policy == "adaptive":
commit = max(commit, int((confidence >= threshold).sum()))
ranking = confidence if policy != "left_to_right" else -torch.arange(len(ids), device=device).float().masked_fill(~unknown[0], float("inf"))
positions = ranking.topk(commit).indices
tokens[0, positions] = candidates[positions]
trace.append({"step": step + 1, "remaining": int(tokens.eq(MASK).sum()),
"text": tokenizer.decode(tokens[0].tolist(), skip_special_tokens=False)})
if not tokens.eq(MASK).any():
break
result = tokens[0].tolist()
return {"text": tokenizer.decode(result), "tokens": result, "trace": trace,
"forward_passes": len(trace), "latency_ms": (time.perf_counter() - started) * 1000,
"seed": seed}
def infill(model, tokenizer, prefix, suffix, missing_tokens=3, **kwargs):
if not 1 <= missing_tokens <= 64:
raise ValueError("missing_tokens must be 1..64")
ids = [BOS] + tokenizer.encode(prefix).ids + [MASK] * missing_tokens + tokenizer.encode(suffix).ids + [EOS]
return denoise(model, tokenizer, ids, **kwargs)
def continue_text(model, tokenizer, prompt, max_new_tokens=64, block_size=16, **kwargs):
if not 1 <= max_new_tokens <= 256 or not 1 <= block_size <= 64:
raise ValueError("max_new_tokens: 1..256; block_size: 1..64")
context = [BOS] + tokenizer.encode(prompt).ids
if len(context) + min(block_size, max_new_tokens) > model.config.max_length:
raise ValueError("Prompt too long for this model context")
output, trace, passes, latency = list(context), [], 0, 0.
base_seed = kwargs.pop("seed", 42)
for offset in range(0, max_new_tokens, block_size):
length = min(block_size, max_new_tokens - offset)
# Keep BOS as a boundary when a continuation exceeds the context window.
window = [BOS] + output[1:][-(model.config.max_length - length - 1):]
result = denoise(model, tokenizer, window + [MASK] * length, seed=base_seed + offset, **kwargs)
new = result["tokens"][-length:]
passes += result["forward_passes"]
latency += result["latency_ms"]
trace.extend({**row, "block": offset // block_size + 1} for row in result["trace"])
if EOS in new:
output.extend(new[:new.index(EOS)])
break
output.extend(new)
return {"text": tokenizer.decode(output), "tokens": output, "trace": trace, "forward_passes": passes,
"latency_ms": latency, "seed": base_seed}