Yazhiniyan's picture
Upload 190 files
5ef3875 verified
Raw History Blame Contribute Delete
24.5 kB
"""``ycode-lm``: build, train and talk to your own from-scratch coding model.
ycode-lm prepare --source ~/code --out data/ # corpus + tokenizer + datasets
ycode-lm train --data data/ --out models/base # pretrain on code
ycode-lm sft --data data/ --init-from models/base --out models/chat
ycode-lm chat --model models/chat # ask it questions
ycode-lm sample --model models/base --prompt "def quicksort("
ycode-lm info --model models/chat
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from ycode.config import default_local_model_dir
from ycode.config import resolve_local_model as default_model_dir
def _require_torch() -> None:
import importlib.util
if importlib.util.find_spec("torch") is None or importlib.util.find_spec("numpy") is None:
print("ycode-lm needs PyTorch and NumPy. Install them with:\n\n pip install -e \".[local]\"\n",
file=sys.stderr)
raise SystemExit(2)
def _train_args(p: argparse.ArgumentParser, *, sft: bool) -> None:
p.add_argument("--data", required=True, type=Path, help="Directory produced by `ycode-lm prepare`.")
p.add_argument("--out", type=Path, default=None, help="Where to write the model.")
if sft:
p.add_argument("--init-from", required=True, type=Path, help="Pretrained model directory.")
p.add_argument("--resume", action="store_true",
help="Continue an interrupted SFT run in --out (falls back to --init-from).")
else:
p.add_argument("--preset", default="v2-small",
help="Model size: v3-40m, v2-tiny, v2-small (default), v2-base, v2-medium, v2-large, or v1 presets.")
p.add_argument("--layers", type=int, help="Override number of layers.")
p.add_argument("--heads", type=int, help="Override number of attention heads.")
p.add_argument("--embd", type=int, help="Override embedding width.")
p.add_argument("--context", type=int, help="Override context length (tokens).")
p.add_argument("--resume", action="store_true", help="Continue training the model in --out.")
p.add_argument("--init-from", type=Path, default=None,
help="Start from this trained/grown model instead of random weights "
"(continued pretraining, e.g. after `ycode-lm grow`).")
p.add_argument("--steps", type=int, default=None, help="Optimizer steps (the LR schedule spans all of them).")
p.add_argument("--until-step", type=int, default=None,
help="Stop at this step and save; continue later with --resume (segmented training).")
p.add_argument("--minutes", type=float, default=None, help="Stop after this many minutes.")
p.add_argument("--batch-size", type=int, default=16)
p.add_argument("--grad-accum", type=int, default=1)
p.add_argument("--lr", type=float, default=None)
p.add_argument("--eval-interval", type=int, default=100)
p.add_argument("--eval-iters", type=int, default=20, help="Batches per evaluation (per split).")
p.add_argument("--save-interval", type=int, default=None,
help="Also checkpoint every N steps without evaluating (cheap protection against crashes).")
p.add_argument("--device", default="auto", help="auto, cpu, cuda or mps.")
p.add_argument("--precision", default="auto", choices=("auto", "fp32", "bf16", "fp16"))
p.add_argument("--grad-checkpoint", action="store_true",
help="Recompute activations in backward: far less GPU memory, ~30%% slower.")
p.add_argument("--shard", action="store_true",
help="With several GPUs (torchrun): split weights and optimizer state across them (FSDP) "
"so models too big for one GPU can train.")
p.add_argument("--schedule", default="wsd", choices=("wsd", "cosine"),
help="LR schedule: warmup-stable-decay (default) or cosine.")
p.add_argument("--compile", action="store_true", help="Use torch.compile (faster on many machines).")
p.add_argument("--optimizer", default=None, choices=("adamw", "muon", "lion"),
help="adamw, or muon (Muon for hidden matrices + AdamW for the rest). "
"Default: muon for v3 presets, adamw otherwise.")
p.add_argument("--muon-lr", type=float, default=0.02)
if sft:
p.add_argument("--no-pack", action="store_true", help="One example per row instead of packing.")
else:
p.add_argument("--anneal-mix", type=float, default=0.2,
help="Share of instruction data mixed in during the LR decay phase (0 disables).")
p.add_argument("--seed", type=int, default=1337)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="ycode-lm", description="Train your own coding LLM from scratch.")
sub = parser.add_subparsers(dest="command", required=True)
p = sub.add_parser("prepare", help="Collect code, train the tokenizer and encode datasets.")
p.add_argument("--source", action="append", type=Path, default=[],
help="File or directory of code (repeatable). Default: the Python standard library.")
p.add_argument("--sft-data", action="append", type=Path, default=[],
help="Extra instruction data: JSONL with {\"prompt\", \"response\"} per line (repeatable).")
p.add_argument("--out", required=True, type=Path, help="Output data directory.")
p.add_argument("--vocab-size", type=int, default=8192)
p.add_argument("--exclude", action="append", default=[],
help="Glob of paths to leave out, e.g. '*/tests/*' (repeatable). Use it to hold out eval data.")
p.add_argument("--workers", type=int, default=None, help="Tokenizer processes (default: all CPU cores).")
p.add_argument("--sft-only", action="store_true",
help="Only rebuild the instruction data, reusing --tokenizer (for re-tuning a pretrained model).")
p.add_argument("--tokenizer", type=Path, default=None,
help="Reuse this tokenizer.json (needed to continue training an existing model).")
p.add_argument("--max-mb", type=float, default=None, help="Cap the code corpus at this many MB.")
_train_args(sub.add_parser("train", help="Pretrain a model on the code corpus."), sft=False)
_train_args(sub.add_parser("sft", help="Instruction-tune a pretrained model so it answers requests."),
sft=True)
p = sub.add_parser("grow", help="Deepen a trained model; the grown model starts out computing the same function.")
p.add_argument("--model", required=True, type=Path, help="Trained model directory.")
p.add_argument("--out", required=True, type=Path, help="Output directory (use as --init-from for train).")
p.add_argument("--layers", type=int, default=None, help="New number of layers.")
p.add_argument("--width", type=int, default=None, help="New width (embedding size); heads scale with it.")
p.add_argument("--version", type=int, default=None, help="Architecture version to record (e.g. 4).")
p = sub.add_parser("export", help="Write a slim inference-only copy of a model (no optimizer state).")
p.add_argument("--model", required=True, type=Path, help="Trained model directory.")
p.add_argument("--out", required=True, type=Path, help="Output directory.")
p.add_argument("--dtype", default="bf16", choices=("bf16", "fp32"), help="bf16 halves the size (default).")
p.add_argument("--max-shard-mb", type=float, default=None,
help="Split weights into files of at most this size (e.g. 45 to stay under GitHub's limits).")
p = sub.add_parser("autotrain", help="Grow, train, tune, evaluate and publish a model across GPU sessions "
"(Kaggle/Colab), syncing checkpoints to Hugging Face.")
p.add_argument("--base-model", required=True, type=Path, help="Model to grow from, e.g. models/ycode-lm-v4.")
p.add_argument("--preset", default="v5-385m", help="Target architecture (default v5-385m).")
p.add_argument("--work", type=Path, default=Path("ycode-autotrain"), help="Local working directory.")
p.add_argument("--hf-repo", default=None, help="Hugging Face repo for the finished model, e.g. you/ycode-lm-v5.")
p.add_argument("--ckpt-repo", default=None, help="Private repo for checkpoints (default: <hf-repo>-checkpoints).")
p.add_argument("--public", action="store_true", help="Make the finished-model repo public.")
p.add_argument("--local-hub", type=Path, default=None, help="Use a local folder instead of Hugging Face.")
p.add_argument("--pretrain-steps", type=int, default=6000)
p.add_argument("--sft-steps", type=int, default=800)
p.add_argument("--batch-size", type=int, default=8)
p.add_argument("--grad-accum", type=int, default=1)
p.add_argument("--nproc", type=int, default=None, help="GPUs to use (default: all visible).")
p.add_argument("--hours", type=float, default=11.0, help="Session time budget (Kaggle sessions last 12 h).")
p.add_argument("--segment-minutes", type=float, default=90.0, help="Checkpoint upload interval.")
p.add_argument("--max-mb", type=float, default=1000.0, help="Cap on the code corpus size.")
p.add_argument("--source", action="append", type=Path, default=[], help="Code to train on (default: "
"the Python standard library + installed packages).")
p.add_argument("--no-grad-checkpoint", action="store_true", help="Faster but needs much more GPU memory.")
p.add_argument("--shard", default="auto", choices=("auto", "yes", "no"),
help="Split the model across GPUs (FSDP). auto: for models over 600M parameters.")
p.add_argument("--lr", type=float, default=None, help="Pretraining LR for AdamW-trained weights.")
p.add_argument("--muon-lr", type=float, default=None, help="Pretraining LR for Muon-trained matrices.")
p.add_argument("--compile", action="store_true")
p.add_argument("--device", default="auto")
p.add_argument("--precision", default="auto", choices=("auto", "fp32", "bf16", "fp16"))
p.add_argument("--max-segments", type=int, default=None, help=argparse.SUPPRESS)
p.add_argument("--eval-interval", type=int, default=250)
p = sub.add_parser("push-hf", help="Upload an exported model folder to a Hugging Face model repo.")
p.add_argument("--model", required=True, type=Path, help="Folder from `ycode-lm export`.")
p.add_argument("--repo", required=True, help="Repo id, e.g. you/ycode-lm-v5.")
p.add_argument("--public", action="store_true")
p = sub.add_parser("eval", help="Score models: bits/byte on held-out code + pass rate on coding problems.")
p.add_argument("--model", action="append", type=Path, default=[],
help="Model directory (repeat to compare several, e.g. v1 and v2).")
p.add_argument("--heldout", action="append", type=Path, default=[],
help="Code the models never trained on, for bits-per-byte (repeatable).")
p.add_argument("--samples", type=int, default=1, help="Samples per problem for pass@k (default 1 = greedy).")
p.add_argument("--no-functional", action="store_true", help="Skip the coding-problem benchmark.")
p.add_argument("--verbose", action="store_true", help="Show each problem's result.")
p.add_argument("--device", default="auto")
for name, help_text in (("chat", "Interactive Q&A with your model."),
("sample", "Continue a code prompt."), ("info", "Show model details.")):
p = sub.add_parser(name, help=help_text)
p.add_argument("--model", type=Path, default=None, help=f"Model directory (default {default_model_dir()}).")
p.add_argument("--device", default="auto")
if name != "info":
p.add_argument("--temperature", type=float, default=0.7)
p.add_argument("--max-tokens", type=int, default=300)
if name == "sample":
p.add_argument("--prompt", required=True)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
_require_torch()
if args.command == "prepare":
from ycode.lm.data import default_sources, prepare_dataset
sources = args.source or default_sources()
if args.sft_only:
from ycode.lm.data import prepare_sft_dataset
if args.tokenizer is None:
print("error: --sft-only needs --tokenizer path/to/tokenizer.json", file=sys.stderr)
return 2
try:
prepare_sft_dataset(sources, args.out, args.tokenizer, extra_sft=args.sft_data,
exclude=args.exclude)
except (ValueError, OSError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
return 0
try:
prepare_dataset(sources, args.out, vocab_size=args.vocab_size, extra_sft=args.sft_data,
exclude=args.exclude, workers=args.workers, tokenizer_path=args.tokenizer,
max_mb=args.max_mb)
except (ValueError, OSError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
return 0
if args.command in ("train", "sft"):
from ycode.lm.train import TrainConfig, train
overrides = {}
if args.command == "train":
for key, attr in (("n_layer", "layers"), ("n_head", "heads"), ("n_embd", "embd"),
("block_size", "context")):
if getattr(args, attr) is not None:
overrides[key] = getattr(args, attr)
sft = args.command == "sft"
cfg = TrainConfig(
stage="sft" if sft else "pretrain",
preset=getattr(args, "preset", "v2-small"),
model_overrides=overrides,
batch_size=args.batch_size,
grad_accum=args.grad_accum,
lr=args.lr or (3e-4 if sft else 1e-3),
min_lr=(args.lr or (3e-4 if sft else 1e-3)) / 10,
warmup_steps=50 if sft else 100,
max_steps=args.steps or (1000 if sft else 5000),
max_minutes=args.minutes,
eval_interval=args.eval_interval,
eval_iters=args.eval_iters,
device=args.device,
precision=args.precision,
schedule=args.schedule,
compile=args.compile,
grad_checkpoint=args.grad_checkpoint,
shard=args.shard,
optimizer=args.optimizer or ("muon" if str(getattr(args, "preset", "")).startswith("v3") else "adamw"),
muon_lr=args.muon_lr,
pack=not getattr(args, "no_pack", False),
anneal_mix=getattr(args, "anneal_mix", 0.0),
seed=args.seed,
init_from=str(args.init_from) if args.init_from else None,
resume=getattr(args, "resume", False),
until_step=args.until_step,
save_interval=args.save_interval,
)
out = args.out or default_local_model_dir()
try:
summary = train(args.data, out, cfg)
except (ValueError, FileNotFoundError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
except KeyboardInterrupt:
print("\nInterrupted. The last checkpoint is in", out)
return 130
if summary: # empty on non-zero ranks of a multi-GPU run; rank 0 reports
print(f"Saved model to {out} (val loss {summary['final']['val']:.3f})")
return 0
if args.command == "autotrain":
return _autotrain(args)
if args.command == "push-hf":
from ycode.lm.hub import HFHub
try:
hub = HFHub(args.repo, private=not args.public)
hub.upload_folder(args.model, "", f"Upload {args.model.name}")
except Exception as exc: # noqa: BLE001 - network/auth errors become a clear message
print(f"error: {exc}", file=sys.stderr)
return 1
print(f"Uploaded {args.model} to {hub.url()}")
return 0
if args.command == "grow":
from ycode.lm.grow import grow_depth, grow_width
from ycode.lm.train import load_checkpoint, save_checkpoint
try:
model, tok, payload = load_checkpoint(args.model)
if args.layers is None and args.width is None:
raise ValueError("pass --layers and/or --width")
grown = model
if args.width is not None and args.width != model.cfg.n_embd:
grown = grow_width(grown, args.width, arch_version=args.version)
if args.layers is not None and args.layers != grown.cfg.n_layer:
grown = grow_depth(grown, args.layers, arch_version=args.version)
except (FileNotFoundError, ValueError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
save_checkpoint(args.out, grown, tok, step=0, stage="grown", val_loss=None)
print(f"Grew {model.cfg.n_layer}x{model.cfg.n_embd} -> {grown.cfg.n_layer}x{grown.cfg.n_embd}: "
f"{model.num_params() / 1e6:.2f}M -> "
f"{grown.num_params() / 1e6:.2f}M parameters, saved to {args.out}")
return 0
if args.command == "export":
from ycode.lm.train import export_checkpoint
try:
export_checkpoint(args.model, args.out, dtype=args.dtype, max_shard_mb=args.max_shard_mb)
except (FileNotFoundError, ValueError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
files = sorted(args.out.glob("model*.pt"))
total = sum(f.stat().st_size for f in files) / 1e6
print(f"Exported {args.model} -> {args.out} ({len(files)} file(s), {total:.1f} MB, {args.dtype})")
return 0
from ycode.lm.generate import LocalLM
if args.command == "eval":
return _eval(args)
model_dir = args.model or default_model_dir()
try:
lm = LocalLM(model_dir, device=args.device)
except (FileNotFoundError, ValueError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
if args.command == "info":
cfg = lm.model.cfg
print(f"Model: {model_dir} (YCode-LM v{lm.version})")
print(f"Parameters: {lm.num_params / 1e6:.2f}M")
print(f"Layers: {cfg.n_layer}, heads {cfg.n_head}, width {cfg.n_embd}, context {cfg.block_size}")
print(f"Vocab: {cfg.vocab_size}")
print(f"Stage: {lm.stage} (step {lm.payload.get('step')}, val loss {lm.payload.get('val_loss')})")
return 0
def echo(chunk: str) -> None:
sys.stdout.write(chunk)
sys.stdout.flush()
if args.command == "sample":
sys.stdout.write(args.prompt)
lm.complete(args.prompt, max_new_tokens=args.max_tokens, temperature=args.temperature, on_text=echo)
print()
return 0
print(f"YCode local model ({lm.num_params / 1e6:.1f}M parameters, {lm.stage}). Ctrl+D to quit.")
history: list[tuple[str, str]] = []
while True:
try:
question = input("\nyou> ").strip()
except (EOFError, KeyboardInterrupt):
print()
return 0
if not question:
continue
history.append(("user", question))
sys.stdout.write("model> ")
try:
answer = lm.chat(history[-6:], max_new_tokens=args.max_tokens, temperature=args.temperature,
on_text=echo)
except KeyboardInterrupt:
print("\n(interrupted)")
history.pop()
continue
print()
history.append(("assistant", answer))
def _autotrain(args) -> int: # noqa: ANN001
from ycode.lm.autotrain import AutoTrainer, Plan, preset_params
from ycode.lm.hub import HFHub, LocalHub
import torch
nproc = args.nproc if args.nproc is not None else max(1, torch.cuda.device_count())
big = preset_params(args.preset) > 600e6
shard = nproc > 1 and (args.shard == "yes" or (args.shard == "auto" and big))
if big and not shard and torch.cuda.is_available():
gb = torch.cuda.get_device_properties(0).total_memory / 1e9
if gb < 40:
print(f"warning: {args.preset} needs about 18 GB of GPU memory for weights and optimizer state "
f"alone; with one {gb:.0f} GB GPU it will likely run out of memory. Use 2+ GPUs "
"(sharded) or a 40 GB+ GPU.", file=sys.stderr)
extra = {k: v for k, v in (("lr", args.lr), ("muon_lr", args.muon_lr)) if v is not None}
plan = Plan(shard=shard, release_shard_mb=1000.0 if big else 45.0, **extra,base_model=str(args.base_model), preset=args.preset, sources=[str(s) for s in args.source],
max_mb=args.max_mb, pretrain_steps=args.pretrain_steps, sft_steps=args.sft_steps,
batch_size=args.batch_size, grad_accum=args.grad_accum, nproc=nproc, precision=args.precision,
grad_checkpoint=not args.no_grad_checkpoint, compile=args.compile, session_hours=args.hours,
segment_minutes=args.segment_minutes, max_segments=args.max_segments, device=args.device,
eval_interval=args.eval_interval)
try:
if args.local_hub is not None:
ckpt, release = LocalHub(args.local_hub, "checkpoints"), LocalHub(args.local_hub, "release")
else:
if not args.hf_repo:
print("error: pass --hf-repo you/ycode-lm-v5 (or --local-hub DIR for a dry run)", file=sys.stderr)
return 2
ckpt = HFHub(args.ckpt_repo or f"{args.hf_repo}-checkpoints", private=True)
release = HFHub(args.hf_repo, private=not args.public)
state = AutoTrainer(plan, args.work, ckpt, release).run()
except (RuntimeError, ValueError, FileNotFoundError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print(f"Phase: {state.phase} | pretrain {state.pretrain_step}/{plan.pretrain_steps} | "
f"sft {state.sft_step}/{plan.sft_steps}")
return 0
def _eval(args) -> int: # noqa: ANN001
import json
from ycode.lm.evaluate import BUGGY, PROBLEMS, bits_per_byte, bugfix_eval, functional_eval, load_heldout_texts
from ycode.lm.generate import LocalLM
models = args.model or [default_model_dir()]
texts = load_heldout_texts(args.heldout) if args.heldout else []
if args.heldout and not texts:
print("error: no code found in --heldout", file=sys.stderr)
return 1
rows = []
for model_dir in models:
try:
lm = LocalLM(model_dir, device=args.device)
except (FileNotFoundError, ValueError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print(f"Evaluating {model_dir} (YCode-LM v{lm.version}, {lm.num_params / 1e6:.1f}M params) ...")
row = {"model": str(model_dir), "version": lm.version, "params_m": round(lm.num_params / 1e6, 2)}
if texts:
with lm.precision():
row["bits_per_byte"] = round(bits_per_byte(lm.model, lm.tokenizer, texts, device=lm.device), 4)
if not args.no_functional:
res = functional_eval(lm, samples=args.samples, log=print if args.verbose else None)
row["pass@1"] = round(res.pass_at_1, 4)
if res.pass_at_k is not None:
row[f"pass@{res.k}"] = round(res.pass_at_k, 4)
row["solved"] = res.solved
fix_rate, fixed = bugfix_eval(lm, log=print if args.verbose else None)
row["fix@1"] = round(fix_rate, 4)
row["fixed"] = fixed
rows.append(row)
print()
header = ["model", "version", "params_m", "bits_per_byte", "pass@1"] + sorted(
{k for r in rows for k in r if k.startswith("pass@") and k != "pass@1"}) + ["fix@1"]
print(" | ".join(h for h in header))
for r in rows:
print(" | ".join(str(r.get(h, "-")) for h in header))
if not args.no_functional:
print(f"\n({len(PROBLEMS)} problems, {len(BUGGY)} bug fixes; bits/byte: lower is better; "
"pass@k / fix@1: higher is better)")
for r in rows:
if r.get("solved"):
print(f"{r['model']} solved: {', '.join(r['solved'])}")
print(json.dumps(rows))
return 0
if __name__ == "__main__":
sys.exit(main())