Training Loop¶
Philosophy¶
KAMUI's training loop is explicit Python. There is no model.fit(),
no callback system, no framework magic. The loop is a for loop you can
read, modify, and understand in full.
This is intentional. Understanding how a model is trained is as important as understanding its architecture. The training loop teaches: - How gradients accumulate and apply - Why learning rate warmup matters - What gradient clipping does and why - How to diagnose training instability from loss curves
The Loop (pseudocode)¶
for step in range(max_steps):
# ── 1. Get a batch ───────────────────────────────────────────────────
input_ids, target_ids = next(dataloader) # (B, S) each
# ── 2. Forward pass + loss ───────────────────────────────────────────
loss = model(input_ids, targets=target_ids)
loss = loss / grad_accum_steps # scale for accumulation
# ── 3. Backward pass ─────────────────────────────────────────────────
loss.backward()
# ── 4. Gradient update (every grad_accum_steps micro-batches) ────────
if (step + 1) % grad_accum_steps == 0:
grad_norm_pre = compute_grad_norm(model)
clip_grad_norm_(model.parameters(), max_norm=grad_clip)
grad_norm_post = compute_grad_norm(model)
lr = scheduler.get_lr(step)
set_lr(optimiser, lr)
optimiser.step()
optimiser.zero_grad()
# ── 5. Logging ───────────────────────────────────────────────────────
if step % log_interval == 0:
log(step, loss, lr, grad_norm_pre, grad_norm_post, tokens_per_sec)
# ── 6. Evaluation ────────────────────────────────────────────────────
if step % eval_interval == 0:
val_loss = evaluate(model, val_dataloader)
log(step, val_loss=val_loss)
# ── 7. Checkpoint ────────────────────────────────────────────────────
if step % checkpoint_interval == 0:
save_checkpoint(model, optimiser, scheduler, step)
Learning Rate Schedule¶
Linear warmup → cosine decay → floor.
if step < warmup_steps:
lr = max_lr * (step / warmup_steps)
elif step < max_steps:
progress = (step - warmup_steps) / (max_steps - warmup_steps)
lr = min_lr + 0.5 * (max_lr - min_lr) * (1 + cos(π * progress))
else:
lr = min_lr
Why warmup? At step 0, gradients are large and noisy. Starting at
max_lr causes loss spikes or divergence. Warmup lets the model reach a
stable region before the full learning rate is applied.
Why cosine decay? Smooth transitions, no sudden LR drops that cause loss spikes. The model gently anneals into a low-loss minimum.
Gradient Accumulation¶
Gradient accumulation simulates a larger effective batch size without requiring additional GPU memory.
With batch_size=32 and grad_accum_steps=4, the effective batch is 128 —
equivalent to loading 128 sequences at once but using only 32 sequences
worth of GPU memory per step.
Correctness requirement: the loss must be divided by grad_accum_steps
before .backward(). Otherwise gradients are scaled by the number of
accumulation steps relative to a true large-batch run.
Gradient Clipping¶
Limits the L2 norm of the gradient vector. Prevents occasional large gradient updates (from difficult training examples) from destabilising the model.
Diagnostics: KAMUI logs grad_norm_pre_clip at every update step.
If this value is consistently above 5–10, the learning rate is likely
too high or the model is unstable.
AdamW — Weight Decay Separation¶
AdamW applies L2 regularisation directly to weights. KAMUI uses two parameter groups:
| Group | Parameters | Weight decay |
|---|---|---|
| Decayed | All weight matrices (ndim ≥ 2) | 0.1 |
| Not decayed | Biases, LayerNorm γ/β (ndim < 2) | 0.0 |
Applying weight decay to biases and LayerNorm parameters is incorrect — they should be unconstrained. This separation is subtle but measurably improves training stability.
Diagnosing Training Problems¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Loss doesn't decrease at all | Causal mask broken (future leakage) or zero LR | Check mask uses -inf; check warmup |
| Loss decreases then explodes | LR too high | Lower learning_rate |
| Loss decreases very slowly | LR too low or gradient vanishing | Raise LR; check grad_norm_pre_clip |
grad_norm_pre_clip > 10 consistently |
LR too high | Lower LR; tighten grad_clip |
| Val loss diverges from train loss | Overfitting | Add dropout; use larger dataset |
Loss is exactly log(vocab_size) after 200 steps |
Gradient not flowing | Check .backward() is called |