tinyllm-cpu-char / train.py
Vishwas1's picture
Add CPU-trained tiny character LLM
170658b verified
#!/usr/bin/env python3
"""Train a tiny character-level GPT on CPU.
This is intentionally small and educational, not production-grade.
"""
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
import torch
from model import TinyGPT, TinyGPTConfig
def build_vocab(text: str):
chars = sorted(set(text))
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {i: ch for ch, i in stoi.items()}
return chars, stoi, itos
def encode(text: str, stoi: dict[str, int]):
return [stoi[ch] for ch in text]
def get_batch(data: torch.Tensor, block_size: int, batch_size: int, device: str):
ix = torch.randint(len(data) - block_size - 1, (batch_size,))
x = torch.stack([data[i : i + block_size] for i in ix]).to(device)
y = torch.stack([data[i + 1 : i + block_size + 1] for i in ix]).to(device)
return x, y
@torch.no_grad()
def estimate_loss(model, train_data, val_data, block_size, batch_size, eval_iters, device):
out = {}
model.eval()
for split, data in [("train", train_data), ("val", val_data)]:
losses = torch.zeros(eval_iters)
for k in range(eval_iters):
xb, yb = get_batch(data, block_size, batch_size, device)
_, loss = model(xb, yb)
losses[k] = loss.item()
out[split] = losses.mean().item()
model.train()
return out
def main():
p = argparse.ArgumentParser()
p.add_argument("--data", default="data/tiny_corpus.txt")
p.add_argument("--out", default="checkpoints/tinyllm.pt")
p.add_argument("--steps", type=int, default=500)
p.add_argument("--batch-size", type=int, default=16)
p.add_argument("--block-size", type=int, default=64)
p.add_argument("--n-layer", type=int, default=2)
p.add_argument("--n-head", type=int, default=2)
p.add_argument("--n-embd", type=int, default=64)
p.add_argument("--lr", type=float, default=3e-4)
p.add_argument("--eval-interval", type=int, default=100)
p.add_argument("--eval-iters", type=int, default=10)
p.add_argument("--seed", type=int, default=1337)
args = p.parse_args()
torch.manual_seed(args.seed)
device = "cpu"
data_path = Path(args.data)
text = data_path.read_text(encoding="utf-8")
if len(text) < args.block_size + 2:
raise SystemExit("Dataset is too small for the chosen block size.")
chars, stoi, itos = build_vocab(text)
encoded = torch.tensor(encode(text, stoi), dtype=torch.long)
n = int(0.9 * len(encoded))
train_data = encoded[:n]
val_data = encoded[n:] if len(encoded[n:]) > args.block_size + 1 else encoded[:n]
cfg = TinyGPTConfig(
vocab_size=len(chars),
block_size=args.block_size,
n_layer=args.n_layer,
n_head=args.n_head,
n_embd=args.n_embd,
dropout=0.1,
)
model = TinyGPT(cfg).to(device)
params = sum(p.numel() for p in model.parameters())
print(f"chars={len(chars)} tokens={len(encoded)} params={params:,} device={device}")
optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr)
start = time.time()
last_loss = None
for step in range(args.steps + 1):
if step % args.eval_interval == 0 or step == args.steps:
losses = estimate_loss(model, train_data, val_data, args.block_size, args.batch_size, args.eval_iters, device)
print(f"step {step:5d}: train {losses['train']:.4f}, val {losses['val']:.4f}")
last_loss = losses
xb, yb = get_batch(train_data, args.block_size, args.batch_size, device)
_, loss = model(xb, yb)
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
ckpt = {
"model_state": model.state_dict(),
"config": cfg.__dict__,
"stoi": stoi,
"itos": {str(k): v for k, v in itos.items()},
"train_args": vars(args),
"last_loss": last_loss,
}
torch.save(ckpt, out_path)
meta_path = out_path.with_suffix(".json")
meta_path.write_text(json.dumps({"params": params, "chars": chars, "last_loss": last_loss}, indent=2), encoding="utf-8")
print(f"saved {out_path} and {meta_path} in {time.time() - start:.1f}s")
if __name__ == "__main__":
main()