Concepts / Writing the train_model_simple Function

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.

  • Programming

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?

  • Calculate loss, reset gradients, update weights, backward pass

  • Update weights, backward pass, calculate loss, reset gradients

  • Reset gradients, calculate loss, backward pass, update weights

  • Backward pass, update weights, reset gradients, calculate loss

Reveal answer
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.

  1. 1
    Reset gradients. Clear the gradients left over from the previous update, because PyTorch accumulates gradients unless they are reset.
  2. 2
    Calculate 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.
  3. 3
    Backward pass. Compute new loss gradients, which describe how each weight should change to reduce that loss.
  4. 4
    Update 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.

Rule

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.

Epoch 1batches 1 to 10Batch cyclereset, loss, backward,updateEpoch 2batches 11 to 20Epoch 3batches 21 to 30next epochnext epoch10 batch iterations
How do the outer epoch loop and the inner batch loop nest, and how many batch iterations make up one full pass over the data? This example assumes 100 training examples and a batch size of 10, which gives 10 batches per epoch.

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.

GroupParametersRole
Data flowing intrain_loader, val_loadertrain_loader supplies batches for training; val_loader supplies batches used when the model is evaluated
Schedulenum_epochs, eval_freq, eval_iternum_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 happensmodel, optimizer, device, start_context, tokenizermodel 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.

Start of batchinput and target batchReset gradientsclear previous gradientsCalculate losscalc_loss_batchBackward passloss gradientsoptimizer.step()update weightsAdvance counterstokens_seen, global_stepglobal_step divisibleby eval_freq?Run evaluationevaluate_modelReturn to next batchyesno
In what exact order do the reset, loss, backward pass, and weight update happen inside one batch, and which steps trigger the evaluation branch?

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.

Before first batchglobal_step -1, tokens_seen0After batch 1global_step 0, tokens_seen40After batch 10global_step 9, tokens_seen400After batch 11global_step 10, tokens_seen440After batch 30global_step 29, tokens_seen1200batch 1 updatebatches 2 to 10epoch 2 beginsbatches 12 to 30
After each batch and at the epoch boundary, what values do global_step and tokens_seen hold? This example assumes each batch's input contains 40 elements (10 examples of 4 tokens each), so tokens_seen rises by 40 per batch.

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.

Worked example

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?

  1. 1
    Batches per epoch

    Divide the training set size by the batch size: 100 divided by 10 gives 10 batches per epoch.

  2. 2
    Total batches

    Three epochs of 10 batches each gives 30 batch iterations.

  3. 3
    Final global_step

    global_step starts at -1 and rises by one per batch, so after 30 batches it is 29.

  4. 4
    Evaluation values

    Evaluation runs when global_step is divisible by 5. The values from 0 to 29 that qualify are 0, 5, 10, 15, 20, and 25.

  5. 5
    Count of evaluations

    Those six values mean evaluation runs six times, after batches 1, 6, 11, 16, 21, and 26.

Result

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.

AspectLearning stepsMonitoring steps
ExamplesReset gradients, calculate loss, backward pass, optimizer.step()Printing losses, generate_and_print_sample
Effect on weightsChanges the weights through optimizer.step()Changes nothing about the weights
Needed for training to workYesNo, they are optional
PurposeFit the model to the dataLet 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.

HelperWhat it receivesWhat it contributes
evaluate_modelThe model, evaluated on both training and validation dataIts results are recorded into the two loss lists
generate_and_print_sampleThe model, tokenizer, device, and start_contextProduces 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 like

    Calling backward and optimizer.step on a batch without clearing the gradients from the previous batch.

    Why it fails

    PyTorch accumulates gradients unless they are reset, so old gradients mix into the current update.

    Do this instead

    Reset 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 like

    Expecting global_step to return to zero when epoch 2 begins.

    Why it fails

    global_step counts batches across all epochs rather than restarting at each epoch.

    Do this instead

    Read 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 like

    Assuming both increase by 1 per batch.

    Why it fails

    global_step increases by one per batch, while tokens_seen increases by the number of elements in each batch's input tensor.

    Do this instead

    Keep 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 fails

    Printing losses and generating sample text change nothing about the weights.

    Do this instead

    Only 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

MEDIUM

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_step starts at -1 and rises by one per batch, so the first batch leaves it at 0.
  • List the global_step values from 0 up to the final value that are divisible by 10.

Recap of the Training Loop

  1. 1
    Each weight update runs the core cycle: reset gradients, calculate loss, perform a backward pass, and update weights with optimizer.step().
  2. 2
    An 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. 3
    tokens_seen counts the input elements consumed so far, while global_step counts batches across all epochs and starts at negative one.
  4. 4
    Evaluation runs when global_step is divisible by eval_freq, and monitoring steps such as printing losses and generating sample text do not change the weights.
  5. 5
    train_model_simple depends on evaluate_model and generate_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_seen measures input consumed so far, while global_step counts batches across all epochs and starts at negative one.
  • Evaluation runs when global_step is divisible by eval_freq; printing losses and generating sample text only monitor progress.
  • evaluate_model and generate_and_print_sample are called by train_model_simple but are defined elsewhere.

Related concepts

More concepts starting with W