nandutt's picture
Upload mini-llm models, code and results
a2b8b4a verified
Raw History Blame Contribute Delete
5.73 kB
"""Shared utilities: device, seed, prompt format, reward, checkpoint load/save."""
from __future__ import annotations
import os
import random
import re
from contextlib import nullcontext
import numpy as np
import torch
from tokenizer import ASSISTANT, EOS, USER
# ------------------------------------------------------------------ runtime
def get_device() -> str:
if torch.cuda.is_available():
return "cuda"
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
return "mps"
return "cpu"
def autocast_ctx(device: str):
"""Mixed precision: bf16 where the GPU supports it (A100/L4), fp16 on T4, fp32 on CPU."""
if device != "cuda":
return nullcontext(), None
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
return torch.autocast("cuda", dtype=dtype), dtype
def set_seed(seed: int) -> None:
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
def ensure_dir(path: str) -> None:
d = os.path.dirname(path)
if d:
os.makedirs(d, exist_ok=True)
# ------------------------------------------------------------ chat format
PROMPT_TEMPLATE = "Write a short story that uses the words: {words}."
def build_prompt(words: list[str]) -> str:
return f"{USER}{PROMPT_TEMPLATE.format(words=', '.join(words))}{ASSISTANT}"
def build_example(words: list[str], story: str) -> tuple[str, str]:
"""Return (prompt, response). The SFT loss is computed on the response only."""
return build_prompt(words), story.strip() + EOS
_STOP = set("""
a an the and or but so if then than that this these those there their they them he she his her him it its
was were is are be been being am do does did done have has had having to of in on at for with from by as
into onto over under up down out off very too also just not no yes all any some one two three every each
said says say went go goes going came come comes get got make made can could would should will shall may
might must when where what which who whom why how because while after before again once day time little
big lot lots very much many more most other another then now away back here there with
upon named lived felt wanted liked loved looked found know knew think thought told asked started saw
""".split())
def pick_keywords(story: str, k: int, rng: random.Random) -> list[str] | None:
"""Pick k content words from a story — used as the SFT/DPO instruction."""
cands = sorted({w for w in re.findall(r"[a-z]+", story.lower()) if len(w) >= 4 and w not in _STOP})
if len(cands) < k:
return None
return rng.sample(cands, k)
# ----------------------------------------------------------------- reward
def keyword_reward(text: str, words: list[str], finished: bool) -> float:
"""
Rule-based, verifiable reward (in the spirit of 'verifiable rewards'):
- fraction of required words that appear (0..1)
- +0.2 if the model closes the story with EOS (not truncated)
"""
found = set(re.findall(r"[a-z]+", text.lower()))
hit = sum(1 for w in words if w in found) / max(len(words), 1)
return hit + (0.2 if finished else 0.0)
def keyword_hit_rate(text: str, words: list[str]) -> float:
found = set(re.findall(r"[a-z]+", text.lower()))
return sum(1 for w in words if w in found) / max(len(words), 1)
def keyword_mentions(text: str, words: list[str]) -> float:
"""Average number of times each required word appears. ~1-2 is natural; much higher = stuffing."""
toks = re.findall(r"[a-z]+", text.lower())
return sum(toks.count(w) for w in words) / max(len(words), 1)
def repeated_trigram_rate(text: str) -> float:
"""Fraction of word trigrams that already appeared earlier in the text (degenerate repetition)."""
toks = re.findall(r"[a-z']+", text.lower())
grams = list(zip(toks, toks[1:], toks[2:]))
if not grams:
return 0.0
return 1.0 - len(set(grams)) / len(grams)
def keyword_reward_v2(text: str, words: list[str], finished: bool) -> float:
"""
Anti-stuffing reward: v1 plus penalties for the failure modes seen in the first run.
- penalty 0.1 per mention beyond 2 per required word (keyword stuffing)
- penalty 1.0 x repeated-trigram rate (looping text)
"""
toks = re.findall(r"[a-z]+", text.lower())
excess = sum(max(0, toks.count(w) - 2) for w in words)
return keyword_reward(text, words, finished) - 0.1 * excess - repeated_trigram_rate(text)
REWARDS = {"v1": keyword_reward, "v2": keyword_reward_v2}
# ------------------------------------------------------------ checkpoints
def save_checkpoint(path: str, model, model_cfg, extra: dict | None = None) -> None:
ensure_dir(path)
raw = getattr(model, "_orig_mod", model) # unwrap torch.compile
torch.save({"model": raw.state_dict(), "model_cfg": model_cfg.__dict__, **(extra or {})}, path)
def load_model(path: str, device: str):
from config import ModelConfig
from model import GPT
ckpt = torch.load(path, map_location=device, weights_only=False)
cfg = ModelConfig(**ckpt["model_cfg"])
model = GPT(cfg).to(device)
model.load_state_dict(ckpt["model"])
return model, cfg
def load_model_dir(path: str, device: str):
"""Load an exported model folder (config.json + model.safetensors), e.g. from the Hugging Face Hub."""
import json
from safetensors.torch import load_model as st_load
from config import ModelConfig
from model import GPT
with open(os.path.join(path, "config.json")) as f:
cfg = ModelConfig(**json.load(f))
model = GPT(cfg)
st_load(model, os.path.join(path, "model.safetensors"))
return model.to(device).eval(), cfg