Derive optimizer updates, then measure PyTorch schedules, clipping, and token budgets.
The pretraining loop from Chapter 05 exposed a gradient and a learning rate, but it treated the optimizer as one line. This chapter opens that line. The NumPy cells preserve update equations on a fixed loss surface; the reusable PyTorch path then applies warmup, cosine decay, global clipping, AdamW parameter groups, and token accounting to the ProofLM decoder.
The experiments are intentionally small. Their purpose is to make update equations and accounting rules testable before they are applied to a larger model.
Its minimizer is \theta^*=-A^{-1}b, so optimization error can be measured independently of the update rule. Plain gradient descent uses \theta_{t+1}=\theta_t-\eta g_t. Momentum keeps a velocity v_t=\beta v_{t-1}+g_t and updates with that velocity. AdamW keeps first and second moments, corrects their initialization bias, and applies weight decay directly to the parameter rather than adding it to the gradient.
Implement each state transition explicitly and compare the trajectories from one common starting point.
gd: loss 9.000 -> -1.142857, distance to optimum=0.00000
momentum: loss 9.000 -> -1.141568, distance to optimum=0.03134
adamw: loss 9.000 -> -1.141892, distance to optimum=0.03144
quadratic optimum: [ 0.71428571 -0.85714286]
The quadratic makes the comparison independent of data sampling and model architecture. Plain gradient descent follows the local slope, momentum smooths successive slopes through its velocity, and AdamW rescales coordinates using their estimated second moments while shrinking the parameter separately. Their final losses need not rank the same way for every learning rate; the useful invariant is that each stateful implementation is evaluated on the same known surface.
Learning-rate warmup
A warmup schedule increases the learning rate from a small value to a peak over W updates. Cosine decay then moves it toward a floor over the remaining S-W updates:
The schedule is indexed by optimizer updates, not epochs. This matters when batch size changes because one epoch then contains a different number of updates and a different number of processed tokens.
The first four rates rise to the configured maximum, then the cosine segment decreases smoothly to the floor. The step + 1 in warmup makes update zero a nonzero but reduced learning rate; the final cosine denominator maps the last valid update exactly to the minimum. A schedule saved in a checkpoint must retain its update position, just as the random state did in Chapter 05.
Gradient clipping
For a model with parameter gradients g_1,\ldots,g_m, define the global norm
Global clipping uses one scale for all arrays. It prevents an occasional large batch from producing an update larger than the configured trust threshold while preserving the relative direction among layers. The clipping operation should be logged separately from the raw norm so frequent clipping is visible as a training-dynamics change.
def global_norm(gradients):ifisinstance(gradients, dict): values = gradients.values()else: values = gradientsreturnfloat(np.sqrt(sum(np.sum(np.asarray(value) **2) for value in values)))def clip_by_global_norm(gradients, maximum_norm): norm = global_norm(gradients) scale =min(1.0, maximum_norm / (norm +1e-12))ifisinstance(gradients, dict): clipped = {name: np.asarray(value) * scale for name, value in gradients.items()}else: clipped = [np.asarray(value) * scale for value in gradients]return clipped, normgradients = {"embedding": np.array([[3.0, 4.0]]), "projection": np.array([12.0])}clipped_gradients, raw_norm = clip_by_global_norm(gradients, maximum_norm=5.0)print("raw norm:", raw_norm, "clipped norm:", global_norm(clipped_gradients))assert np.isclose(raw_norm, 13.0)assert np.isclose(global_norm(clipped_gradients), 5.0)ratio = clipped_gradients["projection"] / gradients["projection"]assert np.allclose(clipped_gradients["embedding"] / gradients["embedding"], ratio)
raw norm: 13.0 clipped norm: 4.999999999999615
The example has a raw norm of 13 and is scaled to 5; both arrays receive the same factor. That common factor is the behavior needed when a Transformer has layers with very different gradient magnitudes. Clipping does not repair an incorrect objective or a consistently unsuitable learning rate, so the unclipped norm and the clipping frequency remain experiment metrics.
Training counters
An epoch count hides the amount of computation when batch size changes. For a sequence task with L tokens per example, a run with batch size B and U optimizer updates processes approximately
\text{tokens}=B\times L\times U
before accounting for the final partial batch. Hold the update budget fixed in the sweep below, vary batch size and learning rate, and report the resulting token budget explicitly. The objective is a small linear regression only because its loss is quick to evaluate; the accounting rule is the same for language-model batches.
The fixed-surface derivations identify what each state variable means. The training path below applies the same ideas to a real ProofLM module: matrix weights receive decoupled decay, normalization and bias parameters do not, the schedule is indexed by optimizer update, and the raw gradient norm is recorded before clipping.
The sweep keeps updates fixed, so the batch-32 runs process sixteen times as many examples and tokens as batch-2 runs. A lower final loss at a larger batch may therefore reflect a larger token budget rather than a more efficient optimizer. Report both axes when comparing learning rates, and add wall-clock time or hardware throughput when the experiment is intended to make a scaling claim.
Summary
Plain gradient descent, momentum, and AdamW differ in the state they carry and in where weight decay enters the update.
Warmup and cosine decay are functions of optimizer updates; changing batch size changes the token count per scheduled step.
Global gradient clipping applies one scale to all parameter arrays and should be logged with the raw norm.
The fixed-surface and regression experiments make optimizer behavior and learning-rate sensitivity measurable without a deep-learning framework.
Batch size, updates, examples, and tokens are separate accounting fields; epochs alone cannot compare these runs.
Chapter 07 packages loss, calibration, memorization, overlap, and diversity measurements into composable evaluation functions.
Exercises
Use the exercises to test the chapter’s invariants and connect the derivations to the reusable implementation. Solutions are hidden in the notebook source and are available through the course tooling when needed.
[P6.1] AdamW update
AdamW derivation. Write the first-moment and second-moment recurrences, explain the bias-correction factors at step one, and state where decoupled weight decay appears in the parameter update.
[P6.2] Token-budget accounting
Budget accounting. A run uses batch size 16, sequence length 128, and 250 optimizer updates. Compute processed tokens and explain why comparing it with a batch-size-4 run by epoch count alone is misleading.
[P6.3]
Update-indexed schedules. A cosine schedule is a function of optimizer updates, not epochs. Given warmup_updates=2 and total_updates=6, explain why the first two learning-rate factors differ from the later factors, and state which token-accounting fields must be reported when batch size changes.
def schedule_report(batch_size, context_length, updates):# Return processed tokens and the schedule's final update index.pass