Writing the train_model_simple Function
A PyTorch training run repeats a core update cycle: reset gradients, calculate loss, perform a backward pass, and update weights.
Why Training Repeats Itself
A neural network starts out with weights that produce poor predictions. Training is the process of nudging those weights again and again until the predictions improve. No single nudge is enough, and the network never sees the whole dataset as one lump. Instead, the training code is a set of nested repetitions: an outer loop that goes over the entire training set, and an inner loop that works through that set in smaller chunks.
This article follows the approach of starting from the smallest unit of learning, one weight update, and building outward. Once you can explain what a single update needs, the outer repetitions and the monitoring steps become simple additions rather than items in a list you have to memorize. The function that ties all of this together is train_model_simple. Its listing is the one piece of code this section focuses on, and your goal is to notice its pattern: which work happens once, which work repeats for every batch, and which work happens only some of the time.
Predicting the Order Inside One Batch
Before reading the explanation, commit to a prediction. Consider only the work that happens for one single batch of training data. Three things must occur: the model's gradients need to be cleared, a loss must be computed from the model's output, and the weights must be changed using that loss. Decide which of these comes first and which comes last.
What do you think happens?
For one batch, what is the correct order of the core update steps?
Reveal answer
Reset gradients, calculate loss, backward pass, update weights
The source describes the cycle in exactly this order. Gradients left from the previous update are cleared first, because PyTorch accumulates gradients unless they are reset. The model then makes predictions and the loss is calculated. A backward pass computes loss gradients, and finally the weights are updated using those gradients.
The Four-Step Update Cycle
Every weight update in a PyTorch training run follows the same core cycle. Each step has a specific job, and the steps depend on one another in a fixed order.
- 1Reset gradients. Clear the gradients left over from the previous update, because PyTorch accumulates gradients unless they are reset.
- 2Calculate loss. The model makes predictions on the current batch of data, and the loss is calculated to measure how far those predictions are from the targets.
- 3Backward pass. Compute new loss gradients, which describe how each weight should change to reduce that loss.
- 4Update weights. Change the model weights using those gradients.
In train_model_simple these four operations appear as gradient reset, a call to calc_loss_batch, a backward call on the loss, and optimizer.step(). The optimizer is the object that actually changes the weights, so optimizer.step() is the step that makes the learning happen.
Splitting Data into Batches and Epochs
Rather than computing one update from the entire training set at once, the training loop processes the data in batches. Each batch is a group of training examples, and the loop iterates over the batches inside every epoch. An epoch is one complete pass over the training set, and it contains one batch iteration for each batch. Each pass through a batch produces one round of the core update cycle, so the number of batches determines how many weight updates happen during one complete pass through the data.
Here $N$ is the number of training examples and $B$ is the batch size, which is how many examples each batch holds. A larger batch size means fewer batches per pass, and a smaller batch size means more batches. Because the batch count comes from this division, you can work it out before training begins.
Training usually runs for more than one epoch. The epoch loop sets the number of full passes, and within each pass the batch loop performs the core update cycle once per batch. This is why iterating over epochs is the outermost repetition: everything else lives inside it.
Reading the Signature by Job
The signature of train_model_simple takes ten parameters: model, train_loader, val_loader, optimizer, device, num_epochs, eval_freq, eval_iter, start_context, and tokenizer. Grouping them by job makes the function easier to read.
| Group | Parameters | Role |
|---|---|---|
| Data flowing in | train_loader, val_loader | train_loader supplies batches for training; val_loader supplies batches used when the model is evaluated |
| Schedule | num_epochs, eval_freq, eval_iter | num_epochs sets how many full passes occur; eval_freq sets how often evaluation runs; eval_iter is passed on to the evaluation step |
| Where and how work happens | model, optimizer, device, start_context, tokenizer | model is the network being trained; optimizer changes its weights; device is the computation target; start_context and tokenizer are used only for printing a sample text |
The ten parameters of train_model_simple grouped by their job
Tracing One Batch Through train_model_simple
Each pass through the inner loop handles one pair of input and target batches. The function first clears the gradients left from the previous batch. It then calls calc_loss_batch, which takes the input batch, the target batch, the model, and the device, and returns a loss. Calling backward on that loss computes the loss gradients for the model's weights. Next, optimizer.step() uses those gradients to update the weights. After the update, tokens_seen and global_step are advanced, and the evaluation branch may run.
The evaluation branch comes after the update and the counter advance, so evaluation always sees the weights produced by the batch that just finished. The diagram below traces this path for a single batch, including the decision that either runs evaluation or skips straight back to the next batch.
How tokens_seen and global_step Advance
Before the loop starts, the function creates three empty lists for losses and tokens, sets tokens_seen to zero, and sets global_step to negative one. The starting value of negative one is deliberate: the counter is increased once per batch, so the first batch leaves it at zero.
Each batch adds the number of elements in its input tensor to tokens_seen, so tokens_seen measures how much input the model has consumed so far. global_step is different. It counts batches across all epochs rather than restarting at each epoch, which is why it can be used to decide when evaluation runs.
Notice what does not happen at an epoch boundary. global_step keeps counting from 9 to 10 when epoch 2 starts, rather than resetting to zero. tokens_seen likewise keeps growing. Both counters describe the whole run, so the epoch boundary changes which pass over the data is underway but not the running totals.
When Evaluation Runs
Evaluation runs only when global_step is divisible by eval_freq. Because global_step is incremented after each batch and starts at negative one, the first batch brings it to zero. Zero is divisible by any eval_freq, so evaluation also runs after the first batch. After that, evaluation occurs after every eval_freq-th batch counted from that point.
Counting evaluations in a three-epoch run
A training set has 100 examples, the batch size is 10, num_epochs is 3, and eval_freq is 5. How many batches run in total, what is the final global_step, and at which global_step values does evaluation run?
- 1Batches per epoch
Divide the training set size by the batch size: 100 divided by 10 gives 10 batches per epoch.
- 2Total batches
Three epochs of 10 batches each gives 30 batch iterations.
- 3Final global_step
global_stepstarts at -1 and rises by one per batch, so after 30 batches it is 29. - 4Evaluation values
Evaluation runs when
global_stepis divisible by 5. The values from 0 to 29 that qualify are 0, 5, 10, 15, 20, and 25. - 5Count of evaluations
Those six values mean evaluation runs six times, after batches 1, 6, 11, 16, 21, and 26.
30 batches run; global_step ends at 29; evaluation runs 6 times at global_step values 0, 5, 10, 15, 20, and 25.
Learning Steps Versus Monitoring Steps
The workflow ends with two steps that are not needed for learning itself. One prints the training and validation set losses so you can watch progress, and the other generates sample text so you can inspect what the model currently produces. The source describes these as optional steps used for tracking training progress. They change nothing about the weights; they only let the person running training see how things are going.
| Aspect | Learning steps | Monitoring steps |
|---|---|---|
| Examples | Reset gradients, calculate loss, backward pass, optimizer.step() | Printing losses, generate_and_print_sample |
| Effect on weights | Changes the weights through optimizer.step() | Changes nothing about the weights |
| Needed for training to work | Yes | No, they are optional |
| Purpose | Fit the model to the data | Let the person running training observe progress |
Steps required for learning compared with optional monitoring steps
The Two Helpers Left Undefined
train_model_simple calls two functions that this section does not define. You will see their calls when tracing the listing. Treat them as black boxes for now: their internals are outside the scope of this function, and the function itself is designed so that it can be read without them.
| Helper | What it receives | What it contributes |
|---|---|---|
| evaluate_model | The model, evaluated on both training and validation data | Its results are recorded into the two loss lists |
| generate_and_print_sample | The model, tokenizer, device, and start_context | Produces and prints a sample text |
The two helper functions called by train_model_simple
Mistakes That Hide the Loop's Logic
Common mistake Skipping or reordering the gradient reset
What it looks likeCalling backward and optimizer.step on a batch without clearing the gradients from the previous batch.
Why it failsPyTorch accumulates gradients unless they are reset, so old gradients mix into the current update.
Do this insteadReset gradients at the start of each batch's update cycle, before the loss is computed.
Common mistake Thinking global_step restarts at each epoch
What it looks likeExpecting global_step to return to zero when epoch 2 begins.
Why it failsglobal_stepcounts batches across all epochs rather than restarting at each epoch.Do this insteadRead global_step as a running total of batches for the entire run, and use it to decide when evaluation happens.
Common mistake Treating tokens_seen and global_step as the same counter
What it looks likeAssuming both increase by 1 per batch.
Why it failsglobal_stepincreases by one per batch, whiletokens_seenincreases by the number of elements in each batch's input tensor.Do this insteadKeep them separate: global_step counts batches, tokens_seen measures how much input the model has consumed.
Common mistake Treating monitoring steps as part of learning
Why it failsPrinting losses and generating sample text change nothing about the weights.
Do this insteadOnly the reset, loss, backward pass, and optimizer.step() change what the model learns.
Common mistake Expecting evaluate_model or generate_and_print_sample to be defined in this listing, when they are only called here.
Practice Counting Batches and Evaluations
A training set has 200 examples, the batch size is 8, num_epochs is 2, and eval_freq is 10. How many batches run per epoch, what is the final global_step, and at which global_step values does evaluation run?
Hints
- Batches per epoch is the training set size divided by the batch size.
global_stepstarts at -1 and rises by one per batch, so the first batch leaves it at 0.- List the
global_stepvalues from 0 up to the final value that are divisible by 10.
Recap of the Training Loop
- 1Each weight update runs the core cycle: reset gradients, calculate loss, perform a backward pass, and update weights with
optimizer.step(). - 2An epoch is one full pass over the training set, and the number of batches per epoch equals the training set size divided by the batch size.
- 3
tokens_seencounts the input elements consumed so far, whileglobal_stepcounts batches across all epochs and starts at negative one. - 4Evaluation runs when
global_stepis divisible byeval_freq, and monitoring steps such as printing losses and generating sample text do not change the weights. - 5
train_model_simpledepends onevaluate_modelandgenerate_and_print_sample, which are called but defined elsewhere.
Key Takeaways
- Each weight update runs the core cycle: reset gradients, calculate loss, perform a backward pass, and update weights.
- An epoch is one full pass over the training set, and batches per epoch equal the training set size divided by the batch size.
tokens_seenmeasures input consumed so far, whileglobal_stepcounts batches across all epochs and starts at negative one.- Evaluation runs when
global_stepis divisible byeval_freq; printing losses and generating sample text only monitor progress. evaluate_modelandgenerate_and_print_sampleare called bytrain_model_simplebut are defined elsewhere.