Wheeler-Chat / generate.py
Quazim0t0's picture
Upload generate.py with huggingface_hub
4c6091e verified
Raw
History Blame Contribute Delete
21 kB
"""
generate.py -- inference / generation harness for Quazimoto-LM.
Loads a checkpoint saved by train.py (save_ckpt writes
{"model", "family_config", "vocab_size", "step"})
rebuilds the exact model from the embedded family_config, restores weights, and
samples completions with the bundled SpikeWhale tokenizer.
This is tuned to the Quazimoto architecture's quirks:
* The model has NO KV cache (attention is training-only / full recompute), so
each new token re-runs the whole context. We respect cfg.block_size by
cropping the context window, exactly like model.generate().
* We reuse the model's NaN/inf-sanitised sampling path (the oscillator stack
can push logits to +/-inf over long rollouts), and add nucleus (top-p),
top-k, and repetition-penalty controls on top.
* Generation stops early on <eos> / <|im_end|> / <|endoftext|> when present.
Examples
--------
# plain completion
python generate.py --ckpt chkpt/quazimoto.pt --prompt "the quazimoto oscillator"
# chat-style turn (wraps prompt in ChatML and stops on <|im_end|>)
python generate.py --ckpt chkpt/quazimoto.pt --chat --prompt "Hello, who are you?"
# interactive REPL
python generate.py --ckpt chkpt/quazimoto.pt --interactive
"""
import argparse
import os
import sys
import torch
import torch.nn.functional as F
from model import QuazimotoLM, QuazimotoConfig
PKG_DIR = os.path.dirname(os.path.abspath(__file__))
# --------------------------------------------------------------------------- #
# loading
# --------------------------------------------------------------------------- #
def load_tokenizer(tok_dir):
sys.path.insert(0, tok_dir)
from spike_tokenizer import SpikeTokenizer
return SpikeTokenizer(vocab_file=os.path.join(tok_dir, "tokenizer.json"))
def load_model(ckpt_path, device):
ckpt = torch.load(ckpt_path, map_location=device, weights_only=False)
fam = ckpt.get("family_config")
if fam is None:
raise ValueError(f"{ckpt_path} has no family_config; can't rebuild the model.")
fam = dict(fam)
state = ckpt["model"]
# Morphable-hybrid detection. Older hybrid checkpoints saved family_config WITHOUT
# use_morphable (the attention weights live under `.attn.full.*` and there are linear
# twins under `.attn.lin.*`). If we rebuilt a plain model those weights would all be
# "missing" -> random attention -> gibberish. Force the morphable/triattn structure
# so the state dict maps correctly, then apply the discretized modes + compressor.
is_hybrid = ("modes" in ckpt) or any(".attn.lin." in k for k in state)
if is_hybrid:
fam["use_morphable"] = True
fam.setdefault("morph_k_full", (ckpt.get("morph") or {}).get("k_full", 4))
tri = ckpt.get("triattn") or {}
if ckpt.get("triattn_stats") is not None or tri:
fam["use_triattention"] = True
fam.setdefault("triattn_budget", tri.get("budget", 2048))
fam.setdefault("triattn_window", tri.get("window", 128))
cfg = QuazimotoConfig(**fam)
model = QuazimotoLM(cfg)
missing, unexpected = model.load_state_dict(state, strict=False)
if missing:
print(f" [warn] missing keys: {len(missing)} (e.g. {missing[:3]})")
if unexpected:
print(f" [warn] unexpected keys: {len(unexpected)} (e.g. {unexpected[:3]})")
if is_hybrid:
modes = ckpt.get("modes")
if modes is not None:
for layer, mode in zip(model.layers, modes):
layer.attn.mode = mode
if ckpt.get("triattn_stats") is not None and model.triattn is not None:
model.triattn.load_state_dict(ckpt["triattn_stats"])
model._is_hybrid = True
print(f" [hybrid] modes={ckpt.get('modes')} | kept full={ckpt.get('kept_full_layers')}")
model.to(device).eval()
step = ckpt.get("step", "?")
print(f"loaded {ckpt_path} | step {step} | vocab {cfg.vocab_size} | "
f"block_size {cfg.block_size}")
return model, cfg
# --------------------------------------------------------------------------- #
# sampling -- tuned for the oscillator stack (no KV cache, sanitised logits)
# --------------------------------------------------------------------------- #
def _filter_logits(logits, top_k, top_p):
"""Apply top-k then nucleus (top-p) filtering in place; returns logits."""
if top_k:
k = min(top_k, logits.size(-1))
kth = torch.topk(logits, k).values[..., -1, None]
logits = logits.masked_fill(logits < kth, float("-inf"))
if top_p and 0.0 < top_p < 1.0:
sorted_logits, sorted_idx = torch.sort(logits, descending=True, dim=-1)
cum = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
remove = cum > top_p
remove[..., 1:] = remove[..., :-1].clone() # keep the first over-threshold token
remove[..., 0] = False
scatter_remove = remove.scatter(-1, sorted_idx, remove)
logits = logits.masked_fill(scatter_remove, float("-inf"))
return logits
@torch.no_grad()
def generate(model, cfg, idx, n_new, temperature=0.9, top_k=40, top_p=0.95,
repetition_penalty=1.1, stop_ids=None, device="cpu", use_cache=True):
"""Autoregressive sampling. Mirrors model.generate()'s NaN/inf sanitisation
(long oscillator rollouts can emit +/-inf logits whose softmax -> NaN ->
multinomial device-side assert) and adds top-p + repetition penalty.
Uses the attention KV cache by default: the prompt is prefilled once, then
each new token is a single-position forward. Once the cache reaches
block_size we re-prime with a windowed recompute (absolute RoPE positions
must stay within max_position_embeddings)."""
model.eval()
stop_ids = set(stop_ids or [])
past = None
for _ in range(n_new):
if use_cache and past is not None and past[0][0].size(2) < cfg.block_size:
logits, _, _, past = model(idx[:, -1:], past_key_values=past, use_cache=True)
elif use_cache:
logits, _, _, past = model(idx[:, -cfg.block_size:], use_cache=True)
else:
logits, _, _ = model(idx[:, -cfg.block_size:])
lg = torch.nan_to_num(logits[:, -1, :].float(),
nan=0.0, posinf=1e4, neginf=-1e4)
# repetition penalty over tokens already in the context
if repetition_penalty and repetition_penalty != 1.0:
for b in range(lg.size(0)):
seen = torch.unique(idx[b])
vals = lg[b, seen]
lg[b, seen] = torch.where(vals > 0, vals / repetition_penalty,
vals * repetition_penalty)
lg = lg / max(temperature, 1e-6)
lg = _filter_logits(lg, top_k, top_p)
probs = torch.softmax(lg, dim=-1)
if not torch.isfinite(probs).all() or float(probs.sum()) <= 0.0:
nxt = torch.argmax(torch.nan_to_num(lg, neginf=-1e4), dim=-1, keepdim=True)
else:
nxt = torch.multinomial(probs, 1)
idx = torch.cat([idx, nxt], dim=1)
if stop_ids and int(nxt[0]) in stop_ids and idx.size(0) == 1:
break
return idx
@torch.no_grad()
def generate_anti_drift(model, cfg, idx, n_new, *, temperature=0.7, min_p=0.05,
top_k=0, no_repeat_ngram=3, cfg_gamma=1.35,
repetition_penalty=1.1, stop_ids=None, device="cpu"):
"""Anti-drift decoding for small models (tuned in decode_lab.py).
Combines three levers that each target a cause of coherence drift:
* CFG / context guidance (cfg_gamma): run a second UNCONDITIONAL stream that
withholds the prompt and push logits away from it,
logits = logits_uncond + gamma*(logits_cond - logits_uncond),
so the model commits harder to what the prompt implies. Keep gamma <=1.5;
above ~2 it over-amplifies and mangles word surface form.
* min_p: drop tokens below min_p * p(top) -- a dynamic tail cut that beats
top-p on the flat next-token distributions small models produce.
* no_repeat_ngram + repetition_penalty: kill the loop/degeneration mode.
Batch-1. Uses a KV cache for each stream; re-primes on block_size overflow."""
assert idx.size(0) == 1, "anti-drift decoder is batch-1"
model.eval()
stop_ids = set(stop_ids or [])
use_cfg = abs(cfg_gamma - 1.0) > 1e-6
B = cfg.block_size
def step(seq, past):
if past is not None and past[0][0].size(2) < B:
lo, _, _, past = model(seq[:, -1:], past_key_values=past, use_cache=True)
else:
lo, _, _, past = model(seq[:, -B:], use_cache=True)
return lo[:, -1, :].float(), past
past_c = past_u = None
useq = idx[:, -1:].clone() if use_cfg else None # uncond stream withholds prompt
seq = idx
for _ in range(n_new):
logits_c, past_c = step(seq, past_c) # 1st iter prefills the prompt
if use_cfg:
logits_u, past_u = step(useq, past_u)
lg = logits_u + cfg_gamma * (logits_c - logits_u)
else:
lg = logits_c
lg = torch.nan_to_num(lg, nan=0.0, posinf=1e4, neginf=-1e4)
if repetition_penalty and repetition_penalty != 1.0:
seen = torch.unique(seq[0])
vals = lg[0, seen]
lg[0, seen] = torch.where(vals > 0, vals / repetition_penalty,
vals * repetition_penalty)
if no_repeat_ngram and seq.size(1) >= no_repeat_ngram - 1:
n = no_repeat_ngram
ids_list = seq[0].tolist()
prefix = tuple(ids_list[-(n - 1):]) if n > 1 else ()
for i in range(len(ids_list) - n + 1):
if tuple(ids_list[i:i + n - 1]) == prefix:
lg[0, ids_list[i + n - 1]] = -float("inf")
lg = lg / max(temperature, 1e-6)
if min_p and min_p > 0:
p = torch.softmax(lg, dim=-1)
lg = lg.masked_fill(p < min_p * p.max(dim=-1, keepdim=True).values, -float("inf"))
lg = _filter_logits(lg, top_k, 1.0)
probs = torch.softmax(lg, dim=-1)
if not torch.isfinite(probs).all() or float(probs.sum()) <= 0.0:
nxt = torch.argmax(torch.nan_to_num(lg, neginf=-1e4), dim=-1, keepdim=True)
else:
nxt = torch.multinomial(probs, 1)
seq = torch.cat([seq, nxt], dim=1)
if use_cfg:
useq = torch.cat([useq, nxt], dim=1)
if stop_ids and int(nxt[0]) in stop_ids:
break
return seq
# --------------------------------------------------------------------------- #
# self-speculative decoding (DeepSpec draft+verify, drafted by the MTP heads)
# --------------------------------------------------------------------------- #
@torch.no_grad()
def generate_speculative(model, cfg, idx, n_new, stop_ids=None, verbose=False, trace=None):
"""Greedy self-speculative decoding (batch size 1).
DeepSpec-style draft+verify, but the model drafts on ITSELF: the `mtp_layers`
MTP heads propose the next `mtp_layers` future tokens from one hidden state,
and the main head verifies them in a single parallel forward, accepting the
longest correct prefix. Output is BIT-IDENTICAL to greedy generate() (top_k=1)
-- speculation only changes the number of forwards, never the tokens.
Per cycle (one forward over committed seq + the pending drafts):
base = index of the last committed token; main_logits[base+j] is the
verifier's genuine token AFTER draft j. We accept draft j while it matches,
commit one correction token at the first mismatch, then read the next round's
drafts from the MTP heads at the deepest still-valid position (base+a)."""
assert idx.size(0) == 1, "speculative decoder is batch-1"
model.eval()
if model.mtp_heads is None:
raise ValueError("speculative decoding needs MTP heads (use_mtp=True).")
stop_ids = set(stop_ids or [])
K = len(model.mtp_heads)
start_len = idx.size(1)
forwards = 0
def crop(seq): # honor block_size context window
return seq[:, -cfg.block_size:]
# bootstrap: one forward over the prompt -> genuine next token + K drafts
main_logits, mtp = model.forward_drafts(crop(idx)); forwards += 1
g = main_logits[:, -1].argmax(-1, keepdim=True)
idx = torch.cat([idx, g], dim=1)
drafts = [mtp[k][:, -1].argmax(-1, keepdim=True) for k in range(K)] # guesses after g
while idx.size(1) - start_len < n_new:
if stop_ids and int(idx[0, -1]) in stop_ids:
break
seq = torch.cat([idx] + drafts, dim=1) # committed + K speculative tokens
base = idx.size(1) - 1 # position of the last committed token
cseq = crop(seq)
off = cseq.size(1) - seq.size(1) # crop shift (<=0) to realign `base`
main_logits, mtp = model.forward_drafts(cseq); forwards += 1
b = base + off
# verify: accept draft[j] while it matches the verifier's token after it
a = 0
for j in range(K):
v = main_logits[:, b + j].argmax(-1, keepdim=True) # genuine token after draft j-1 / g
if int(v) == int(drafts[j]):
a += 1
else:
break
accepted = drafts[:a]
correction = main_logits[:, b + a].argmax(-1, keepdim=True) # genuine token at first miss
new_toks = accepted + [correction]
idx = torch.cat([idx] + new_toks, dim=1)
if trace is not None: # per-cycle record for the visualizer
trace.append({"drafts": [int(d) for d in drafts], "accepted": a,
"correction": int(correction)})
# next drafts from the deepest still-valid hidden (position b+a holds the last
# accepted token / g, whose MTP heads predict the tokens AFTER `correction`).
drafts = [mtp[k][:, b + a].argmax(-1, keepdim=True) for k in range(K)]
if stop_ids and any(int(t) in stop_ids for t in new_toks):
break
idx = idx[:, :start_len + n_new]
if verbose:
produced = idx.size(1) - start_len
print(f" [spec] {produced} tokens in {forwards} forwards "
f"({produced / max(forwards,1):.2f} tok/forward; "
f"vs 1.00 for plain greedy)")
return idx
# --------------------------------------------------------------------------- #
# prompt helpers
# --------------------------------------------------------------------------- #
def build_prompt(tok, text, chat, system):
"""Return token ids for the prompt. In chat mode, wrap in ChatML so the
model is cued to produce an assistant turn (only useful if it was trained
on that framing -- harmless otherwise)."""
if chat:
parts = []
if system:
parts.append(f"<|im_start|><|system|>\n{system}<|im_end|>\n")
parts.append(f"<|im_start|><|user|>\n{text}<|im_end|>\n")
parts.append("<|im_start|><|assistant|>\n")
text = "".join(parts)
ids = tok.encode(text, add_special_tokens=False)
return ids, text
def resolve_stop_ids(tok, chat):
"""Collect ids of end-of-turn / end-of-text markers that exist in the vocab."""
names = ["<eos>", "<|endoftext|>"]
if chat:
names.insert(0, "<|im_end|>")
vocab = tok.get_vocab()
return [vocab[n] for n in names if n in vocab]
# --------------------------------------------------------------------------- #
# main
# --------------------------------------------------------------------------- #
def run_once(model, cfg, tok, args, device, prompt_text):
stop_ids = resolve_stop_ids(tok, args.chat)
ids, shown = build_prompt(tok, prompt_text, args.chat, args.system)
x = torch.tensor([ids], dtype=torch.long, device=device)
if getattr(model, "_is_hybrid", False):
# morphable hybrid: linear layers carry O(1) state, full layers a KV cache the
# TriAttention compressor prunes. Must use the hybrid decode path.
out = model.generate_hybrid(x, args.max_new_tokens,
temperature=args.temperature, top_k=args.top_k)
elif args.speculative:
# greedy self-speculative decoding (MTP heads draft, main head verifies);
# output equals greedy decoding, only faster. Ignores sampling knobs.
out = generate_speculative(model, cfg, x, args.max_new_tokens,
stop_ids=stop_ids, verbose=args.spec_stats)
elif getattr(args, "anti_drift", False):
out = generate_anti_drift(model, cfg, x, args.max_new_tokens,
temperature=args.temperature, min_p=args.min_p,
top_k=args.top_k or 0, no_repeat_ngram=args.no_repeat_ngram,
cfg_gamma=args.cfg_gamma,
repetition_penalty=args.repetition_penalty,
stop_ids=stop_ids, device=device)
else:
out = generate(model, cfg, x, args.max_new_tokens,
temperature=args.temperature, top_k=args.top_k,
top_p=args.top_p, repetition_penalty=args.repetition_penalty,
stop_ids=stop_ids, device=device, use_cache=not args.no_cache)
gen_ids = out[0, len(ids):].tolist()
completion = tok.decode(gen_ids, skip_special_tokens=not args.show_special)
if args.echo_prompt:
print(shown, end="")
print(completion)
return completion
def main():
p = argparse.ArgumentParser(description="Quazimoto-LM generation harness")
p.add_argument("--ckpt", default=os.path.join(PKG_DIR, "chkpt", "quazimoto.pt"),
help="checkpoint .pt saved by train.py")
p.add_argument("--tok_dir", default=PKG_DIR, help="dir holding tokenizer.json")
p.add_argument("--prompt", default="the quazimoto oscillator")
p.add_argument("--max_new_tokens", type=int, default=200)
p.add_argument("--temperature", type=float, default=0.9)
p.add_argument("--top_k", type=int, default=40, help="0 to disable")
p.add_argument("--top_p", type=float, default=0.95, help="1.0 to disable")
p.add_argument("--repetition_penalty", type=float, default=1.1, help="1.0 to disable")
p.add_argument("--seed", type=int, default=None)
p.add_argument("--chat", action="store_true", help="wrap prompt in ChatML framing")
p.add_argument("--system", default="", help="system prompt (chat mode only)")
p.add_argument("--echo_prompt", action="store_true", help="print the prompt before output")
p.add_argument("--show_special", action="store_true", help="don't strip special tokens")
p.add_argument("--interactive", action="store_true", help="REPL: read prompts from stdin")
p.add_argument("--no_cache", action="store_true",
help="disable the KV cache (full recompute each step)")
p.add_argument("--anti-drift", dest="anti_drift", action="store_true",
help="anti-drift decoding for small models: CFG context guidance + "
"min_p + no-repeat-ngram (tuned in decode_lab.py)")
p.add_argument("--cfg_gamma", type=float, default=1.35,
help="anti-drift CFG strength (1=off; keep <=1.5, >2 mangles words)")
p.add_argument("--min_p", type=float, default=0.05, help="anti-drift min_p tail cut")
p.add_argument("--no_repeat_ngram", type=int, default=3, help="anti-drift n-gram block (0=off)")
p.add_argument("--speculative", action="store_true",
help="greedy self-speculative decoding via MTP draft heads (DeepSpec-style)")
p.add_argument("--spec_stats", action="store_true",
help="print tokens/forward acceptance stats for speculative decoding")
p.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
args = p.parse_args()
if args.top_k == 0:
args.top_k = None
if args.seed is not None:
torch.manual_seed(args.seed)
device = args.device
tok = load_tokenizer(args.tok_dir)
model, cfg = load_model(args.ckpt, device)
if cfg.vocab_size != tok.vocab_size:
print(f" [warn] model vocab {cfg.vocab_size} != tokenizer vocab "
f"{tok.vocab_size}; decode may be misaligned.")
if args.interactive:
print("interactive mode -- type a prompt, Ctrl-C / empty line + EOF to quit.\n")
try:
while True:
try:
line = input(">>> ").strip()
except EOFError:
break
if not line:
continue
run_once(model, cfg, tok, args, device, line)
print()
except KeyboardInterrupt:
print("\nbye.")
else:
run_once(model, cfg, tok, args, device, args.prompt)
if __name__ == "__main__":
main()