Upload chat_sft.py with huggingface_hub
Browse files- 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()
|