data: full TinyStories + tokenized-id cache, val loss, CLI arch
- Corpus::load_cached: tokenize the (large) corpus ONCE, cache the id stream to
<corpus>.u16.bin (gpt2 vocab 50257 < 65536 → exact u16), read cache on reruns.
- Corpus::split_tail: hold out a tail slice as a validation corpus.
- train(): take an optional valid corpus + eval_every/eval_batches; periodic
deterministic val-loss eval that checkpoints the BEST val model; returns
TrainResult{train_losses, evals, best_val}. T6 fixed-cadence path preserved.
- bin/train + bin/export_safetensors: read architecture (--heads/--head-dim/
--layers/--ffn) + opt knobs (--steps/--batch/--seq/--max-lr/--val-tokens/
--eval-every) from CLI flags; defaults reproduce the v0-baseline tiny config.
- gitignore the multi-GB corpus + *.u16.bin caches + *.ckpt (dash5-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,28 +31,47 @@ pub struct TrainConfig {
|
||||
pub max_grad_norm: f32,
|
||||
pub log_every: usize,
|
||||
/// Optional checkpoint path written every `ckpt_every` steps (and at the end).
|
||||
/// When `eval_every > 0`, the checkpoint instead tracks the BEST val loss.
|
||||
pub ckpt_path: Option<PathBuf>,
|
||||
pub ckpt_every: usize,
|
||||
/// Evaluate held-out val loss every `eval_every` steps (0 = never). Each eval
|
||||
/// averages cross-entropy over `eval_batches` fixed windows of the val corpus.
|
||||
pub eval_every: usize,
|
||||
pub eval_batches: usize,
|
||||
/// Seed for reproducible sequence sampling.
|
||||
pub seed: u64,
|
||||
}
|
||||
|
||||
/// Outcome of a run: per-step train losses and (step, val_loss) eval points.
|
||||
pub struct TrainResult {
|
||||
pub train_losses: Vec<f32>,
|
||||
pub evals: Vec<(usize, f32)>,
|
||||
pub best_val: Option<f32>,
|
||||
}
|
||||
|
||||
/// Train `model` on `corpus` for `cfg.steps` AdamW steps. Returns the per-step
|
||||
/// loss trace (one mean loss per step, read from the first sequence of the
|
||||
/// batch — cheap and representative). Logs progress and checkpoints as configured.
|
||||
/// train-loss trace plus any (step, val_loss) eval points. Logs progress, and —
|
||||
/// when `valid` is given and `cfg.eval_every > 0` — evaluates held-out val loss
|
||||
/// periodically and checkpoints the BEST val model (else checkpoints on a fixed
|
||||
/// cadence, as in T6). Logs progress.
|
||||
pub fn train(
|
||||
model: &TinyTransformer,
|
||||
device: Device,
|
||||
corpus: &Corpus,
|
||||
valid: Option<&Corpus>,
|
||||
cfg: &TrainConfig,
|
||||
) -> Vec<f32> {
|
||||
) -> TrainResult {
|
||||
let params = model.params();
|
||||
let mut opt = GpuAdamW::new(cfg.weight_decay);
|
||||
let mut rng = cfg.seed;
|
||||
let mut losses = Vec::with_capacity(cfg.steps);
|
||||
let mut evals = Vec::new();
|
||||
let mut best_val: Option<f32> = None;
|
||||
let inv_batch = 1.0 / cfg.batch_size as f32;
|
||||
let start = Instant::now();
|
||||
let mut tokens_seen: u64 = 0;
|
||||
// Best-val checkpointing only kicks in when we actually evaluate.
|
||||
let track_best = valid.is_some() && cfg.eval_every > 0;
|
||||
|
||||
for step in 0..cfg.steps {
|
||||
let lr = cfg.schedule.lr(step);
|
||||
@@ -88,18 +107,86 @@ pub fn train(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(path) = &cfg.ckpt_path {
|
||||
if cfg.ckpt_every > 0 && (step + 1) % cfg.ckpt_every == 0 {
|
||||
checkpoint::save(path, ¶ms).expect("checkpoint save");
|
||||
// Periodic held-out eval (deterministic windows, no grad).
|
||||
if let Some(v) = valid {
|
||||
if cfg.eval_every > 0 && ((step + 1) % cfg.eval_every == 0 || step == cfg.steps - 1) {
|
||||
let vl = eval_loss(model, device, v, cfg.seq_len, cfg.eval_batches);
|
||||
evals.push((step, vl));
|
||||
let improved = best_val.map(|b| vl < b).unwrap_or(true);
|
||||
println!(
|
||||
" eval @ step {step}: val loss {vl:.4}{}",
|
||||
if improved { " (best)" } else { "" }
|
||||
);
|
||||
if improved {
|
||||
best_val = Some(vl);
|
||||
if let Some(path) = &cfg.ckpt_path {
|
||||
checkpoint::save(path, ¶ms).expect("best checkpoint save");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed-cadence checkpointing (only when not tracking best val).
|
||||
if !track_best {
|
||||
if let Some(path) = &cfg.ckpt_path {
|
||||
if cfg.ckpt_every > 0 && (step + 1) % cfg.ckpt_every == 0 {
|
||||
checkpoint::save(path, ¶ms).expect("checkpoint save");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = &cfg.ckpt_path {
|
||||
checkpoint::save(path, ¶ms).expect("final checkpoint save");
|
||||
println!("saved checkpoint → {}", path.display());
|
||||
// Without periodic eval, still persist the final params (T6 behaviour). With
|
||||
// best-val tracking the checkpoint already holds the best model — don't clobber.
|
||||
if !track_best {
|
||||
if let Some(path) = &cfg.ckpt_path {
|
||||
checkpoint::save(path, ¶ms).expect("final checkpoint save");
|
||||
println!("saved checkpoint → {}", path.display());
|
||||
}
|
||||
}
|
||||
TrainResult {
|
||||
train_losses: losses,
|
||||
evals,
|
||||
best_val,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mean cross-entropy over `batches` deterministic, non-overlapping windows of
|
||||
/// the validation corpus (no backward — eval only). Deterministic so val loss is
|
||||
/// comparable across steps and runs.
|
||||
fn eval_loss(
|
||||
model: &TinyTransformer,
|
||||
device: Device,
|
||||
valid: &Corpus,
|
||||
seq: usize,
|
||||
batches: usize,
|
||||
) -> f32 {
|
||||
if valid.len() <= seq + 1 {
|
||||
return f32::NAN;
|
||||
}
|
||||
let n_win = (valid.len() - 1) / seq; // disjoint windows that fit
|
||||
let batches = batches.max(1).min(n_win.max(1));
|
||||
let stride = (n_win / batches).max(1);
|
||||
let mut sum = 0.0f32;
|
||||
let mut count = 0usize;
|
||||
for i in 0..batches {
|
||||
let s = (i * stride) * seq;
|
||||
if s + seq + 1 > valid.len() {
|
||||
break;
|
||||
}
|
||||
let input: Vec<i32> = valid.tokens[s..s + seq].to_vec();
|
||||
let target: Vec<i32> = valid.tokens[s + 1..s + seq + 1].to_vec();
|
||||
let ids = ids_tensor(&input, device);
|
||||
let targets = ids_tensor(&target, device);
|
||||
let loss = model.loss(&ids, &targets);
|
||||
sum += read_scalar(&loss);
|
||||
count += 1;
|
||||
}
|
||||
if count == 0 {
|
||||
f32::NAN
|
||||
} else {
|
||||
sum / count as f32
|
||||
}
|
||||
losses
|
||||
}
|
||||
|
||||
fn read_scalar(v: &xtrain_autodiff::tape::Var) -> f32 {
|
||||
|
||||
Reference in New Issue
Block a user