ContextReq commited on
Commit
3420071
·
verified ·
1 Parent(s): 309546c

reuploaded after fixing max output bug for samplers

Browse files
Files changed (2) hide show
  1. complete.py +43 -0
  2. sampler.py +85 -0
complete.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Completion CLI: continue a prompt with a trained Notio checkpoint.
3
+
4
+ Usage:
5
+ src/complete.py "Once upon a time" --checkpoint out/notio.pt --n 2
6
+ src/complete.py "The little cat" --temp 0.8 --top-k 40 --max-tokens 200
7
+ """
8
+ import argparse
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
13
+ import sampler
14
+
15
+
16
+ def main():
17
+ ap = argparse.ArgumentParser()
18
+ ap.add_argument("prompt", nargs="?", default="", help="text to complete (default: story start)")
19
+ ap.add_argument("--checkpoint", default="out/notio.pt")
20
+ ap.add_argument("--temp", type=float, default=0.9)
21
+ ap.add_argument("--top-k", type=int, default=0, help="0 = off")
22
+ ap.add_argument("--max-tokens", type=int, default=900)
23
+ ap.add_argument("--n", type=int, default=1, help="number of completions")
24
+ ap.add_argument("--device", default=None)
25
+ a = ap.parse_args()
26
+
27
+ m, device, ck = sampler.load_model(a.checkpoint, a.device)
28
+ print(f"checkpoint: {a.checkpoint} | step {ck['step']} | "
29
+ f"train {ck.get('loss', float('nan')):.3f} | val {ck.get('val_loss', float('nan')):.3f} | {device}")
30
+
31
+ prompt = a.prompt or "Once upon a time"
32
+ prompt_ids = sampler.encode_prompt(prompt)
33
+ block = m.cfg.layer1.block_size
34
+ if len(prompt_ids) >= block:
35
+ sys.exit(f"prompt too long: {len(prompt_ids)} ids, block_size {block}")
36
+ print(f"prompt: {prompt!r} ({len(prompt_ids)} ids) | temp {a.temp} | top-k {a.top_k}")
37
+ for i in range(a.n):
38
+ ids = sampler.generate(m, device, prompt_ids, a.max_tokens, a.temp, a.top_k)
39
+ print(f"\n=== completion {i} ({len(ids)} ids) ===\n{sampler.ids_to_display(ids)}")
40
+
41
+
42
+ if __name__ == "__main__":
43
+ main()
sampler.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sampler: load a Notio checkpoint and generate completions statefully.
2
+
3
+ Prompt is fed once (positions 0..P-1), then generation continues token by
4
+ token carrying the RNN state with advancing position offsets. Decodes via
5
+ the runtime decoder: tags -> effects, display text for humans.
6
+ """
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ import torch
12
+ import torch.nn.functional as F
13
+
14
+ PROJ = Path(__file__).resolve().parents[1]
15
+ sys.path.insert(0, str(PROJ))
16
+ sys.path.insert(0, str(PROJ / "scripts"))
17
+ import tokenizer as tk
18
+ from model import Notio
19
+
20
+ tk.load_vocab()
21
+
22
+
23
+ def detach_states(states):
24
+ out = []
25
+ for st in states:
26
+ out.append(tuple(s.detach() for s in st) if isinstance(st, tuple) else st.detach())
27
+ return out
28
+
29
+
30
+ def load_model(checkpoint, device=None):
31
+ """Load checkpoint -> (model, device, ck_dict). Weights-only disabled:
32
+ our checkpoints embed NotioConfig."""
33
+ device = device or ("cuda" if torch.cuda.is_available() else "cpu")
34
+ ck = torch.load(checkpoint, map_location=device, weights_only=False)
35
+ m = Notio(ck["cfg"])
36
+ m.load_state_dict(ck["state_dict"])
37
+ m.to(device)
38
+ m.eval()
39
+ return m, device, ck
40
+
41
+
42
+ def encode_prompt(text):
43
+ """Text -> id list, prefixed with <bos>. Accepts literal whitespace."""
44
+ if isinstance(text, str):
45
+ text = text.encode()
46
+ return [1] + list(tk.encode_bytes(text)) # <bos>
47
+
48
+
49
+ def ids_to_display(ids):
50
+ """id list -> human text via the runtime decoder (tags mapped/dropped)."""
51
+ out = bytes(np.asarray(ids, dtype=np.uint8)).translate(tk.TABLE_DEC)
52
+ for sent, tok in tk.SENT_DEC:
53
+ out = out.replace(sent, tok)
54
+ return tk.display(out).decode()
55
+
56
+
57
+ @torch.no_grad()
58
+ def generate(m, device, prompt_ids, max_tokens=900, temperature=0.9, top_k=0):
59
+ # NOTE: learned story length is ~700-900 ids; too-small max_tokens chops
60
+ # stories off before the model's natural <eos> (see src/diag_eos.py)
61
+ """Stateful completion. Stops at <eos> or max_tokens. Returns id list."""
62
+ block = m.cfg.layer1.block_size
63
+ if len(prompt_ids) >= block:
64
+ return list(prompt_ids) # nothing left to generate
65
+ if len(prompt_ids) + max_tokens > block:
66
+ max_tokens = block - len(prompt_ids) # wpe capacity is the ceiling
67
+ ids = torch.tensor([prompt_ids], dtype=torch.long, device=device)
68
+ logits, states = m(ids) # one pass over the prompt, pos 0..P-1
69
+ states = detach_states(states)
70
+ pos = len(prompt_ids)
71
+ for _ in range(max_tokens):
72
+ logits, states = m(ids[:, -1:], states, pos_offset=pos)
73
+ states = detach_states(states)
74
+ pos += 1
75
+ logits = logits[:, -1, :] / max(temperature, 1e-6)
76
+ if top_k > 0:
77
+ v, _ = torch.topk(logits, top_k)
78
+ logits = torch.where(logits < v[:, -1:],
79
+ torch.full_like(logits, -float("inf")), logits)
80
+ probs = F.softmax(logits, dim=-1)
81
+ nxt = torch.multinomial(probs, 1)
82
+ ids = torch.cat([ids, nxt], dim=1)
83
+ if nxt.item() == 2: # <eos>
84
+ break
85
+ return ids[0].tolist()