Download diffuthink/v2/inference.py from OthmaneW/DiffuThink-Story-512: direct link, hf CLI and curl.
- Browser
- Download file 5.31 kB
-
https://huggingface.co/OthmaneW/DiffuThink-Story-512/resolve/main/diffuthink/v2/inference.py
- Command line
-
hf download hf://OthmaneW/DiffuThink-Story-512/diffuthink/v2/inference.py
-
curl -L -o inference.py https://huggingface.co/OthmaneW/DiffuThink-Story-512/resolve/main/diffuthink/v2/inference.py
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")) | |
| 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} | |