Quazim0t0 commited on
Commit
dd6c22f
·
verified ·
1 Parent(s): 54f8951

Upload chat_sft.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. chat_sft.py +135 -0
chat_sft.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ chat_sft.py -- interactive multi-turn chat REPL for an SFT'd Quazimoto-LM.
3
+
4
+ Renders the running conversation in the student's ChatML and generates the
5
+ assistant reply. Two decode paths:
6
+
7
+ * SPECULATIVE (default) -- DeepSpec/DSpark-style self-speculative decoding: the
8
+ MTP heads draft the next few tokens, the main head verifies them in one pass,
9
+ accepting the longest correct prefix (see model.forward_drafts / generate.py
10
+ generate_speculative). Greedy, so output is deterministic.
11
+ * SAMPLED (--temperature > 0) -- the KV-cache sampler (generate.py generate):
12
+ the conversation is prefilled once, then each new token is a single-position
13
+ cached forward. Supports temperature / top-k / top-p.
14
+
15
+ Both reuse the model's KV cache; speculative additionally drafts multiple tokens
16
+ per verify. Usage:
17
+ python chat_sft.py # auto-find newest chkpt, speculative
18
+ python chat_sft.py --temperature 0.7 # sampled instead
19
+ python chat_sft.py --ckpt chkpt/quazimoto_sft.pt --system "You are concise."
20
+ """
21
+ import argparse, os, sys
22
+ # Force UTF-8 console so byte-merge tokens with non-cp1252 chars don't crash print().
23
+ for _s in (sys.stdout, sys.stderr):
24
+ try:
25
+ _s.reconfigure(encoding="utf-8", errors="replace")
26
+ except (AttributeError, ValueError):
27
+ pass
28
+ import torch
29
+
30
+ from model import QuazimotoLM, QuazimotoConfig
31
+ from generate import generate, generate_speculative, resolve_stop_ids
32
+
33
+ PKG_DIR = os.path.dirname(os.path.abspath(__file__))
34
+ ROLE_TOK = {"system": "<|system|>", "user": "<|user|>", "assistant": "<|assistant|>"}
35
+
36
+
37
+ def find_ckpt(path):
38
+ if path and os.path.isfile(path):
39
+ return path
40
+ folder = (os.path.dirname(path) if path else os.path.join(PKG_DIR, "chkpt")) or "."
41
+ if not os.path.isdir(folder):
42
+ return None
43
+ # prefer an SFT checkpoint, else newest *.pt
44
+ pts = [os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(".pt")]
45
+ sft = [p for p in pts if "sft" in os.path.basename(p).lower()]
46
+ pool = sft or pts
47
+ return max(pool, key=os.path.getmtime) if pool else None
48
+
49
+
50
+ def load_tokenizer(tok_dir):
51
+ sys.path.insert(0, tok_dir)
52
+ from spike_tokenizer import SpikeTokenizer
53
+ return SpikeTokenizer(vocab_file=os.path.join(tok_dir, "tokenizer.json"))
54
+
55
+
56
+ def render(history, tok):
57
+ """history: list of {role,content} -> token ids ending with the assistant header
58
+ so the model continues the assistant turn."""
59
+ parts = [f"<|im_start|>{ROLE_TOK.get(m['role'],'<|user|>')}\n{m['content']}<|im_end|>\n"
60
+ for m in history]
61
+ parts.append("<|im_start|><|assistant|>\n")
62
+ return tok.encode("".join(parts), add_special_tokens=False)
63
+
64
+
65
+ def main():
66
+ p = argparse.ArgumentParser(description="Quazimoto-LM SFT chat (KV cache + speculative)")
67
+ p.add_argument("--ckpt", default="", help="SFT checkpoint (default: newest *sft*.pt in ./chkpt)")
68
+ p.add_argument("--tok_dir", default=PKG_DIR)
69
+ p.add_argument("--system", default="", help="optional system prompt")
70
+ p.add_argument("--max_new_tokens", type=int, default=256)
71
+ p.add_argument("--temperature", type=float, default=0.0, help="0 = speculative/greedy; >0 = sampled")
72
+ p.add_argument("--top_k", type=int, default=40)
73
+ p.add_argument("--top_p", type=float, default=0.95)
74
+ p.add_argument("--repetition_penalty", type=float, default=1.1)
75
+ p.add_argument("--no_speculative", action="store_true", help="force the cached sampler even at temp 0")
76
+ p.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
77
+ args = p.parse_args()
78
+
79
+ path = find_ckpt(args.ckpt)
80
+ if path is None:
81
+ print("No checkpoint found; pass --ckpt or train one."); return
82
+ ckpt = torch.load(path, map_location=args.device, weights_only=False)
83
+ cfg = QuazimotoConfig(**ckpt["family_config"])
84
+ model = QuazimotoLM(cfg); model.load_state_dict(ckpt["model"], strict=False)
85
+ model.to(args.device).eval()
86
+ tok = load_tokenizer(args.tok_dir)
87
+ stop = resolve_stop_ids(tok, chat=True) # <|im_end|>, <eos>, <|endoftext|>
88
+ imend = tok.get_vocab().get("<|im_end|>")
89
+
90
+ spec = (args.temperature <= 1e-4 and not args.no_speculative and model.mtp_heads is not None)
91
+ mode = "speculative (DSpark draft+verify)" if spec else f"sampled (temp {args.temperature}, KV cache)"
92
+ print(f"loaded {os.path.basename(path)} (step {ckpt.get('step')}) | decode: {mode}")
93
+ print("type your message; /reset clears history, /quit exits.\n")
94
+
95
+ history = []
96
+ if args.system:
97
+ history.append({"role": "system", "content": args.system})
98
+
99
+ while True:
100
+ try:
101
+ user = input("you> ").strip()
102
+ except (EOFError, KeyboardInterrupt):
103
+ print(); break
104
+ if not user:
105
+ continue
106
+ if user in ("/quit", "/exit"):
107
+ break
108
+ if user == "/reset":
109
+ history = ([{"role": "system", "content": args.system}] if args.system else [])
110
+ print("(history cleared)\n"); continue
111
+
112
+ history.append({"role": "user", "content": user})
113
+ ids = render(history, tok)
114
+ x = torch.tensor([ids], device=args.device)
115
+ with torch.no_grad():
116
+ if spec:
117
+ out = generate_speculative(model, cfg, x, args.max_new_tokens, stop_ids=stop)
118
+ else:
119
+ out = generate(model, cfg, x, args.max_new_tokens, temperature=max(args.temperature, 1e-6),
120
+ top_k=args.top_k or None, top_p=args.top_p,
121
+ repetition_penalty=args.repetition_penalty, stop_ids=stop,
122
+ device=args.device, use_cache=True)
123
+ gen = out[0, len(ids):].tolist()
124
+ if imend in gen: # trim at the assistant turn end
125
+ gen = gen[:gen.index(imend)]
126
+ reply = tok.decode(gen, skip_special_tokens=True).strip()
127
+ try:
128
+ print(f"bot> {reply}\n")
129
+ except UnicodeEncodeError:
130
+ print("bot> " + reply.encode("ascii", "replace").decode() + "\n")
131
+ history.append({"role": "assistant", "content": reply})
132
+
133
+
134
+ if __name__ == "__main__":
135
+ main()