Download common.py from nandutt/mini-llm-tinystories: direct link, hf CLI and curl.
- Browser
- Download file 5.73 kB
-
https://huggingface.co/nandutt/mini-llm-tinystories/resolve/main/common.py
- Command line
-
hf download hf://nandutt/mini-llm-tinystories/common.py
-
curl -L -o common.py https://huggingface.co/nandutt/mini-llm-tinystories/resolve/main/common.py
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 | |