""" Derived from Andrej Karpathy's nanochat project. MIT License Copyright (c) 2025 Andrej Karpathy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. """ from __future__ import annotations import argparse import random import statistics import time from typing import Callable import numpy as np import torch from dropout_decay.experiments.artifacts import write_jsonl_row from dropout_decay.models import DropoutGPT, GPTConfig from dropout_decay.specs import DropoutCondition, ModelSpec def set_seed(seed: int) -> None: random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) def batch_seed( seed: int, model: ModelSpec, dropout_code: int, stage: int | None ) -> int: return ( seed * 1_000_003 + model.n_layer * 100_003 + model.n_head * 10_007 + model.n_embd * 101 + dropout_code * 37 + (stage or 0) * 997 ) def make_batch( tokens: np.ndarray, token_limit: int, batch_size: int, block_size: int, rng: np.random.Generator, device: torch.device, ) -> tuple[torch.Tensor, torch.Tensor]: limit = min(token_limit, len(tokens)) max_start = limit - block_size - 1 if max_start <= 0: raise ValueError("token_limit is too small for the requested block_size") starts = rng.integers(0, max_start, size=batch_size) x_np = np.stack([tokens[start : start + block_size] for start in starts]).astype( np.int64 ) y_np = np.stack( [tokens[start + 1 : start + 1 + block_size] for start in starts] ).astype(np.int64) return ( torch.tensor(x_np, dtype=torch.long, device=device), torch.tensor(y_np, dtype=torch.long, device=device), ) @torch.no_grad() def estimate_loss( model: DropoutGPT, tokens: np.ndarray, token_limit: int, batches: int, args: argparse.Namespace, device: torch.device, rng_seed: int, ) -> float: if batches <= 0: return float("nan") model.eval() rng = np.random.default_rng(rng_seed) losses: list[float] = [] for _ in range(batches): x, y = make_batch( tokens, token_limit, args.batch_size, args.block_size, rng, device ) _, loss = model(x, y) losses.append(float(loss.item())) model.train() return float(statistics.fmean(losses)) def train_segment( *, run_mode: str, condition: DropoutCondition, model_spec: ModelSpec, config: GPTConfig, train_tokens: np.ndarray, val_tokens: np.ndarray, token_limit: int, steps: int, seed: int, args: argparse.Namespace, device: torch.device, dropout_fn: Callable[[int], float], metrics_file, trace_file, stage: int | None = None, model: DropoutGPT | None = None, optimizer: torch.optim.Optimizer | None = None, tokens_seen_start: int = 0, ) -> tuple[DropoutGPT, torch.optim.Optimizer, int, dict]: if model is None: set_seed(seed) model = DropoutGPT(config).to(device) optimizer = torch.optim.AdamW( model.parameters(), lr=args.lr, betas=(0.9, 0.95), weight_decay=args.weight_decay, ) else: torch.manual_seed(seed + 10_000 + (stage or 0)) if optimizer is None: raise ValueError("optimizer is required when reusing a model") model.train() dropout_code = int(round(condition.initial * 10_000)) rng = np.random.default_rng(batch_seed(seed, model_spec, dropout_code, stage)) tokens_seen = tokens_seen_start last_loss = float("nan") active_dropout = condition.initial t0 = time.time() for step in range(1, steps + 1): active_dropout = dropout_fn(tokens_seen) model.set_dropout(active_dropout) x, y = make_batch( train_tokens, token_limit, args.batch_size, args.block_size, rng, device ) _, loss = model(x, y) optimizer.zero_grad(set_to_none=True) loss.backward() if args.grad_clip > 0: torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) optimizer.step() tokens_seen += args.batch_size * args.block_size last_loss = float(loss.item()) if args.log_every > 0 and step % args.log_every == 0: write_jsonl_row( trace_file, { "event": "train_step", "run_mode": run_mode, "condition": condition.name, "model_name": model_spec.name, "seed": seed, "stage": stage, "step": step, "steps": steps, "token_limit": int(token_limit), "tokens_seen": int(tokens_seen), "dropout": float(active_dropout), "train_batch_loss": last_loss, }, ) if args.eval_every > 0 and step % args.eval_every == 0: train_eval = estimate_loss( model, train_tokens, token_limit, args.trace_eval_batches, args, device, rng_seed=seed + 20_000 + step, ) val_eval = estimate_loss( model, val_tokens, len(val_tokens), args.trace_eval_batches, args, device, rng_seed=seed + 30_000 + step, ) write_jsonl_row( trace_file, { "event": "eval_step", "run_mode": run_mode, "condition": condition.name, "model_name": model_spec.name, "seed": seed, "stage": stage, "step": step, "steps": steps, "token_limit": int(token_limit), "tokens_seen": int(tokens_seen), "dropout": float(active_dropout), "train_eval_loss": train_eval, "val_eval_loss": val_eval, "generalization_gap": val_eval - train_eval, }, ) train_eval = estimate_loss( model, train_tokens, token_limit, args.train_eval_batches, args, device, rng_seed=seed + 40_000 + (stage or 0), ) val_eval = estimate_loss( model, val_tokens, len(val_tokens), args.eval_batches, args, device, rng_seed=seed + 50_000 + (stage or 0), ) row = { "run_mode": run_mode, "condition": condition.name, "condition_kind": condition.kind, "seed": seed, "stage": stage, "token_limit": int(token_limit), "steps": int(steps), "tokens_seen": int(tokens_seen), "dropout_initial": float(condition.initial), "dropout_final": float(condition.final), "dropout_schedule": condition.schedule, "dropout_active_final": float(active_dropout), "train_loss_last": last_loss, "train_eval_loss": train_eval, "val_eval_loss": val_eval, "eval_loss": val_eval, "generalization_gap": val_eval - train_eval, "elapsed_sec": time.time() - t0, "parameters": model.num_parameters(), "model_config": config.to_dict(), **model_spec.to_dict(), } write_jsonl_row(metrics_file, row) return model, optimizer, tokens_seen, row