Download model/sft.py from MSkilll/Etra: direct link, hf CLI and curl.
- Browser
- Download file 7.92 kB
-
https://huggingface.co/MSkilll/Etra/resolve/main/model/sft.py
- Command line
-
hf download hf://MSkilll/Etra/model/sft.py
-
curl -L -o sft.py https://huggingface.co/MSkilll/Etra/resolve/main/model/sft.py
7.92 kB
| # sft.py | |
| import json, math, random, argparse | |
| from pathlib import Path | |
| import torch, torch.nn as nn | |
| from datasets import load_dataset | |
| from main import Transformer | |
| from bpe import load_bpe_tokenizer | |
| import os | |
| import re | |
| SCRIPT_DIR = Path(__file__).parent.absolute() | |
| ROOT = SCRIPT_DIR.parent # .../Alpha | |
| DATA = ROOT / "jsons" # search_ex, calc_ex, rag_*.json | |
| TOK = ROOT / "jsons" / "fineweb_bpe.json" # confirm with the find command | |
| CKPT = ROOT / "checkpoints" | |
| MAX_LEN, MICRO, ACCUM = 2048, 16, 2 | |
| LR, WARMUP, EPOCHS, EVAL_EVERY = 3e-5, 50, 3, 200 | |
| def fmt(ex): | |
| if ex.get("input"): | |
| return (f"### Instruction:\n{ex['instruction']}\n\n" | |
| f"### Input:\n{ex['input']}\n\n### Response:\n") | |
| return f"### Instruction:\n{ex['instruction']}\n\n### Response:\n" | |
| def build(ds, tok): | |
| bos, eos = tok.word_to_idx["<bos>"], tok.word_to_idx["<eos>"] | |
| enc = lambda s: tok.tok.encode(s, add_special_tokens=False).ids | |
| out = [] | |
| for ex in ds: | |
| if "messages" in ex: | |
| item = encode_chat(ex, bos, eos, enc) | |
| if item: out.append(item) | |
| continue | |
| p = [bos] + enc(fmt(ex)) | |
| a = enc(ex["output"].strip()) + [eos] | |
| if len(p) + len(a) > MAX_LEN or len(a) < 2: | |
| continue # drop, never truncate: a cut answer would lose its <eos> | |
| out.append((p + a, [-100] * len(p) + a)) | |
| return out | |
| def encode_chat(ex, bos, eos, enc): | |
| msgs, sysm = ex["messages"], "" | |
| if msgs[0]["role"] == "system": | |
| sysm, msgs = msgs[0]["content"], msgs[1:] | |
| ids, lab = [bos], [-100] | |
| for i in range(0, len(msgs) - 1, 2): | |
| head = f"### System:\n{sysm}\n\n" if (i == 0 and sysm) else "" | |
| p = enc(f"{head}### Instruction:\n{msgs[i]['content']}\n\n### Response:\n") | |
| r = enc(msgs[i + 1]["content"]) + [eos] | |
| if len(ids) + len(p) + len(r) > MAX_LEN: | |
| break # stop at a turn boundary, never mid-answer | |
| ids += p + r | |
| lab += [-100] * len(p) + r # loss only on assistant text + <eos> | |
| return (ids, lab) if len(ids) > 1 else None | |
| def collate(batch, pad): | |
| n = max(len(x) for x, _ in batch) | |
| ids = torch.full((len(batch), n), pad) | |
| lab = torch.full((len(batch), n), -100) | |
| for i, (x, y) in enumerate(batch): | |
| ids[i, :len(x)] = torch.tensor(x) | |
| lab[i, :len(y)] = torch.tensor(y) | |
| return ids[:, :-1], lab[:, 1:] # right-padding is safe: causal attention | |
| def lr_at(s, total): | |
| if s < WARMUP: return LR * (s + 1) / WARMUP | |
| p = (s - WARMUP) / max(1, total - WARMUP) | |
| return 0.1 * LR + 0.5 * 0.9 * LR * (1 + math.cos(math.pi * p)) | |
| def evaluate(model, data, pad, dev): | |
| model.eval(); tot, cnt = 0.0, 0 | |
| for i in range(0, len(data), MICRO): | |
| x, y = collate(data[i:i + MICRO], pad) | |
| x, y = x.to(dev), y.to(dev) | |
| with torch.autocast("cuda", dtype=torch.bfloat16, enabled=dev.type == "cuda"): | |
| lg = model(x) | |
| tot += nn.functional.cross_entropy(lg.float().reshape(-1, lg.size(-1)), y.reshape(-1), | |
| ignore_index=-100, reduction="sum").item() | |
| cnt += (y != -100).sum().item() | |
| model.train() | |
| return tot / cnt | |
| if __name__ == "__main__": | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--base", default=str(CKPT / "checkpoint_latest.pt")) | |
| ap.add_argument("--out", default=str(CKPT / "checkpoint_sftv4.pt")) | |
| a = ap.parse_args() | |
| Path(a.out).parent.mkdir(parents=True, exist_ok=True) | |
| random.seed(42); torch.manual_seed(42) | |
| dev = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| tok = load_bpe_tokenizer(TOK) | |
| pad = tok.word_to_idx["<pad>"] | |
| alpaca = list(load_dataset("yahma/alpaca-cleaned")["train"]) | |
| MATH = re.compile(r"^(calculate|compute|evaluate|solve|add|subtract|multiply|divide|" | |
| r"find the (sum|product|difference|quotient|result)|" | |
| r"what is the (sum|product|difference|quotient|result)|" | |
| r"what('s| is) [\d\.\-\+\*/%\s]+)", re.I) | |
| alpaca = [ex for ex in alpaca if ex["input"] or not MATH.match(ex["instruction"].strip())] | |
| FACT = re.compile(r"^(who|when|where|which|what is|what was|what are|how many|how much|in which|name the)\b", re.I) | |
| alpaca = [ex for ex in alpaca | |
| if ex["input"] or not FACT.match(ex["instruction"].strip())] | |
| with open(DATA / "search_ex.json", encoding="utf-8") as f: | |
| search = json.load(f) | |
| with open(DATA / "calc_ex.json", encoding="utf-8") as f: | |
| calc = json.load(f) | |
| with open(DATA / "rag_ex.json", encoding="utf-8") as f: | |
| rag = json.load(f) | |
| with open(DATA / "rag_long.json", encoding="utf-8") as f: | |
| rag_long = json.load(f) | |
| with open(DATA / "rag_asqa.json", encoding="utf-8") as f: | |
| rag_asqa = json.load(f) | |
| with open(DATA / "rag_wiki.json", encoding="utf-8") as f: | |
| rag_wiki = json.load(f) | |
| with open(DATA / "chat.json", encoding="utf-8") as f: | |
| chat = json.load(f) | |
| with open(DATA / "identity.json", encoding="utf-8") as f: | |
| identity = json.load(f) | |
| def first_user(ex): | |
| return next(m["content"] for m in ex["messages"] if m["role"] == "user").strip() | |
| chat = [ex for ex in chat if not (MATH.match(first_user(ex)) or FACT.match(first_user(ex)))] | |
| raw = alpaca + search + calc + rag + rag_long * 2 + rag_asqa + rag_wiki + chat + identity * 3 | |
| random.shuffle(raw) | |
| data = build(raw, tok) | |
| val, train = data[:1000], data[1000:] | |
| print(f"train {len(train)} | val {len(val)} examples") | |
| ck = torch.load(a.base, map_location="cpu"); arch = ck["arch"] | |
| model = Transformer(arch["d_model"], arch["num_heads"], arch["vocab_size"], | |
| arch["d_ff"], arch["num_kv_heads"], arch["num_decoder_layers"]) | |
| model.load_state_dict(ck["model_state_dict"]); model.to(dev).train() | |
| opt = torch.optim.AdamW(model.parameters(), lr=LR, betas=(0.9, 0.95), | |
| weight_decay=0.0, fused=dev.type == "cuda") | |
| per_step = MICRO * ACCUM | |
| total = EPOCHS * (len(train) // per_step) | |
| print(f"base val (answer tokens): {evaluate(model, val, pad, dev):.4f}") | |
| step, best = 0, float("inf") | |
| for ep in range(EPOCHS): | |
| random.shuffle(train) | |
| for b in range(0, len(train) - per_step + 1, per_step): | |
| for g in opt.param_groups: g["lr"] = lr_at(step, total) | |
| opt.zero_grad(set_to_none=True); tl = 0.0 | |
| for m in range(ACCUM): | |
| x, y = collate(train[b + m * MICRO: b + (m + 1) * MICRO], pad) | |
| x, y = x.to(dev), y.to(dev) | |
| with torch.autocast("cuda", dtype=torch.bfloat16, enabled=dev.type == "cuda"): | |
| lg = model(x) | |
| loss = nn.functional.cross_entropy(lg.float().reshape(-1, lg.size(-1)), | |
| y.reshape(-1), ignore_index=-100) | |
| (loss / ACCUM).backward(); tl += loss.item() / ACCUM | |
| gn = nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| opt.step(); step += 1 | |
| if step % 20 == 0: | |
| print(f"ep {ep+1} step {step}/{total} | loss {tl:.4f} | gnorm {gn:.2f}") | |
| if step % EVAL_EVERY == 0 or step == total: | |
| v = evaluate(model, val, pad, dev) | |
| print(f"[Eval] step {step} | val {v:.4f}") | |
| if v < best: | |
| best = v | |
| torch.save({"model_state_dict": model.state_dict(), "arch": arch, | |
| "step": step, "val_loss": v}, a.out) | |
| print(" saved best") |