// End-to-end acceptance for the Phase T5 tiny transformer: overfit one fixed // char-level batch with a hand-written gradient-descent step and assert the loss // collapses toward 0. This is THE signal that the whole fwd+bwd graph (embedding, // RMSNorm, RoPE, multi-head attention, SwiGLU, LM head, cross-entropy) is wired // correctly — a single buggy backward would stall the loss. // // The optimizer here is deliberately minimal (`p ← p − lr·grad`); AdamW / LR // schedule / real data are T6. Gated behind `not(no_cuda)` (runs on dash5). #![cfg(not(no_cuda))] use xtrain_autodiff::tape::Var; use xtrain_cuda::device; use xtrain_model::{Config, TinyTransformer, ids_tensor}; use xtrain_tensor::Device; // Deterministic LCG fill in [-scale, scale). fn fill(n: usize, seed: u64, scale: f32) -> Vec { let mut state = seed .wrapping_mul(2862933555777941757) .wrapping_add(3037000493); (0..n) .map(|_| { state = state .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); (((state >> 33) as f32 / (1u64 << 31) as f32) - 0.5) * 2.0 * scale }) .collect() } fn require_gpu() { assert!( device::device_count().expect("device count") > 0, "no CUDA device" ); device::set_device(0).unwrap(); } // One GD step over every parameter: p ← p − lr·grad, then zero the grad. fn gd_step(params: &[Var], lr: f32) { for p in params { if let Some(g) = p.grad() { let updated = p.value().add(&g.scale(-lr)); p.set_value(updated); } p.zero_grad(); } } #[test] fn overfit_tiny_batch() { require_gpu(); let device = Device::Cuda(0); // --- Char-level bring-up: tiny embedded text → vocab → (input, target). --- let text = "hello tiny transformer world"; let mut vocab_chars: Vec = text.chars().collect(); vocab_chars.sort_unstable(); vocab_chars.dedup(); let vocab = vocab_chars.len(); let stoi = |c: char| vocab_chars.iter().position(|&x| x == c).unwrap() as i32; let tokens: Vec = text.chars().map(stoi).collect(); // Next-token prediction: input = tokens[..n-1], target = tokens[1..]. let input: Vec = tokens[..tokens.len() - 1].to_vec(); let target: Vec = tokens[1..].to_vec(); let ids = ids_tensor(&input, device); let targets = ids_tensor(&target, device); // --- Tiny model with small-scale deterministic init. --- let mut cfg = Config::tiny(); cfg.vocab = vocab; let mut seed = 1u64; let model = TinyTransformer::new(cfg, device, |shape| { seed = seed.wrapping_add(1); let n: usize = shape.iter().product(); // RMSNorm gammas ([dim]) init to ~1; everything else small random. if shape.len() == 1 { fill(n, seed, 0.02).iter().map(|v| v + 1.0).collect() } else { fill(n, seed, 0.08) } }); let params = model.params(); println!( "overfit: vocab={vocab} seq={} params={}", input.len(), cfg.num_params() ); let read_loss = |l: &Var| -> f32 { l.value().to_device(Device::Cpu).as_slice::()[0] }; let lr = 0.3f32; let steps = 200; let start = read_loss(&model.loss(&ids, &targets)); let mut last = start; for step in 0..steps { let loss = model.loss(&ids, &targets); last = read_loss(&loss); if step % 20 == 0 || step == steps - 1 { println!("step {step:3}: loss = {last:.6}"); } loss.backward(); gd_step(¶ms, lr); } println!("overfit: start loss = {start:.6} → final loss = {last:.6} ({steps} steps)"); // A correct fwd+bwd memorises this tiny fixed batch: loss → ~0. assert!( last < 0.05, "overfit failed to drive loss to ~0: start {start:.4} final {last:.4}" ); assert!(last < start, "loss did not decrease"); // Sanity: greedy argmax should reproduce the target sequence after overfit. let logits = model.forward(&ids).value().to_device(Device::Cpu); let lg = logits.as_slice::(); let mut correct = 0; for (r, &t) in target.iter().enumerate() { let row = &lg[r * vocab..(r + 1) * vocab]; let argmax = row .iter() .enumerate() .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()) .unwrap() .0 as i32; if argmax == t { correct += 1; } } println!("overfit: greedy match {correct}/{}", target.len()); assert_eq!(correct, target.len() as i32, "did not memorise the batch"); }