Trainer API Training Loop

Implement a training loop with Trainer API — LLM Finetuning.

Focus: implement a training loop with trainer api

Sponsored

You’ve cleaned your dataset, tokenized it, and chosen a model — now comes the moment of truth: actually training it. But writing a raw PyTorch training loop from scratch is a rite of passage you don’t need to repeat every time you fine-tune an LLM. In this lesson, you’ll learn to implement a training loop with the Trainer API from Hugging Face Transformers — a high-level abstraction that handles batching, gradient accumulation, logging, checkpointing, and evaluation, so you can focus on getting your model to converge, not on debugging loss.backward() again. Let’s turn your fine-tuning intent into a running, training pipeline.

The problem this lesson solves

Writing a training loop for an LLM by hand means dealing with a mountain of boilerplate: iterating over batches, moving tensors to the GPU, zeroing gradients, calling backward(), clipping gradients, handling evaluation loops, saving checkpoints, managing learning rate schedules, and keeping your training logs readable. Every one of these steps is a place where a silent bug can hide — a wrong device, a forgotten zero_grad(), an off-by-one batch index, or an eval loop that runs on the training set.

The pain is especially sharp when you’re fine-tuning a large language model for a real task like instruction following or chat. Your model may have billions of parameters, your dataset may be huge, and a single run can take hours or days. A naive loop might work on a toy model but falls apart on a production-scale run when you need features like early stopping, gradient accumulation, or mixed-precision training.

The Trainer API solves all this by giving you a battle-tested training loop that Hugging Face has tuned across thousands of experiments. Instead of writing 200 lines of loop code, you write 10 lines of configuration, and the library handles the dirty details — consistently, efficiently, and reproducibly.

Core concept / mental model

Think of the Trainer API as a smart engine you plug into a car you already know. The car is your model and your tokenizer; the engine’s job is to burn the fuel (your dataset) and drive the car to a destination (trained weights). You don’t need to know every bolt of the engine, but you do need to know how to configure it — the fuel octane (hyperparameters), the route (training vs. evaluation datasets), and the pit stops (checkpoints and logging).

Key definitions

  • TrainingArguments: a configuration object that defines how the Trainer trains — learning rate, batch size, number of epochs, weight decay, gradient accumulation steps, logging intervals, and whether to use mixed precision or not.
  • Trainer: the engine that orchestrates the loop — takes your model, training arguments, datasets, tokenizer, and optional data collator, and exposes a single train() method.
  • Data collator: a callable that takes a list of training examples and returns a batch ready for the model. For causal language modeling, you often use DataCollatorForLanguageModeling with mlm=False to mask nothing and just create labels for the decoder.
  • Compute metrics: an optional callback that runs after each evaluation to compute metrics like perplexity or accuracy.

The mental model is simple: TrainingArguments describes the “how,” Trainer orchestrates the “what.” If you know what you want to optimize (loss) and how fast (learning rate), you can express that in TrainingArguments and let Trainer handle the rest.

How it works step by step

The Trainer API abstracts the standard training loop, so understanding the underlying steps helps you debug and customize when things go wrong. Here’s what happens under the hood when you call trainer.train():

  1. Instantiate the model: The model you pass (often loaded with AutoModelForCausalLM for text generation, or AutoModelForSequenceClassification for classification) is placed on the computing device (CPU or GPU).
  2. Set up training arguments: TrainingArguments stores all hyperparameters, logging settings, and save strategy. It also enables mixed-precision (fp16 or bf16) if you set fp16=True.
  3. Prepare data collator: The data collator turns tokenized examples into batches. It may also shift labels for causal LM (labels = input_ids shifted right) and add padding to fixed length.
  4. Enter the epoch loop: For each epoch, Trainer iterates over the training dataset, grouped into batches by the data collator. For each batch, it performs a forward pass, computes loss, does a backward pass, and updates model weights based on the optimizer (default is AdamW) and learning rate scheduler.
  5. Gradient accumulation (optional): If gradient_accumulation_steps > 1, Trainer accumulates gradients over several steps before updating weights — this simulates a larger batch size when memory is limited.
  6. Logging and checkpointing: Every logging_steps steps, it records loss and learning rate to logs. Every save_steps steps (or at epoch end based on save_strategy), it saves a checkpoint.
  7. Evaluation (optional): If you provide an eval_dataset, Trainer runs the evaluation loop periodically (e.g., every eval_steps) to compute eval loss and your custom metrics via compute_metrics.
  8. Learning rate scheduling: Trainer applies a schedule — typically linear decay to zero, with optional warmup steps — and updates the scheduler each step.
  9. Early stopping (optional): With EarlyStoppingCallback, Trainer stops training when a metric (like eval loss) stops improving after early_stopping_patience evaluations.

That’s the full journey. Your role is just to feed it the model, data, and arguments.

Hands-on walkthrough

Let’s put it together. In this exercise, we’ll fine-tune a small GPT-2 model on a tiny instruction-like dataset. This is complete and runnable in a Colab with a GPU (T4 works), and you’ll see real loss numbers drop.

Step 1: Install and import libraries

pip install transformers datasets accelerate

Step 2: Load model, tokenizer, and data

from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments, DataCollatorForLanguageModeling
from datasets import Dataset

# tiny instruction dataset (replace with your real data)
training_data = [
    "What is the capital of France? Paris.",
    "What is 2 + 2? 4.",
    "What is the tallest mountain? Everest.",
    "What is the boiling point of water? 100 C.",
    "Who wrote Romeo and Juliet? Shakespeare.",
]

dataset = Dataset.from_list({"text": training_data})

# load a small causal LM
model_name = "gpt2"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# GPT-2 has no pad token; set it to eos_token for packing
tokenizer.pad_token = tokenizer.eos_token

# tokenize the dataset (note: this is a tiny demo; usually use streaming or larger files)
def tokenize_function(examples):
    return tokenizer(examples["text"], truncation=True, max_length=128, padding="max_length")

tokenized_dataset = dataset.map(tokenize_function, batched=True, remove_columns=["text"])

# split into train/eval
train_test = tokenized_dataset.train_test_split(test_size=0.2)
train_dataset = train_test["train"]
eval_dataset = train_test["test"]

# data collator for causal LM (mlm=False)
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

Step 3: Configure training arguments

training_args = TrainingArguments(
    output_dir="./gpt2-finetuned",       # where checkpoints are saved
    evaluation_strategy="steps",         # evaluate every eval_steps
    eval_steps=10,                       # evaluate every 10 steps
    save_strategy="steps",               # save checkpoint every save_steps
    save_steps=20,
    logging_strategy="steps",
    logging_steps=5,
    num_train_epochs=3,
    per_device_train_batch_size=2,
    per_device_eval_batch_size=2,
    learning_rate=5e-5,
    weight_decay=0.01,
    warmup_steps=10,
    fp16=True,                           # mixed precision for speed on GPU
    load_best_model_at_end=True,         # reload best checkpoint at end
    metric_for_best_model="eval_loss",
    gradient_accumulation_steps=1,
)

Pro tip: load_best_model_at_end=True requires evaluation_strategy="steps" (or "epoch") and metric_for_best_model — this saves you hours by automatically reverting to the best checkpoint seen during training.

Step 4: Create Trainer and train

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    data_collator=data_collator,
    tokenizer=tokenizer,
)

trainer.train()

Expected output (truncated)

You’ll see training logs like:

{'loss': 5.3456, 'learning_rate': 4.5e-05, 'epoch': 0.08}
{'loss': 4.9876, 'learning_rate': 4.0e-05, 'epoch': 0.16}
{'eval_loss': 4.5567, 'epoch': 0.25}

The loss should drop quickly if your data is small and consistent — that’s the Trainer at work.

Step 5: Save and reuse

trainer.save_model("./gpt2-finetuned-final")

To use the model for predictions later:

from transformers import pipeline
pipe = pipeline("text-generation", model="./gpt2-finetuned-final", tokenizer=tokenizer)
print(pipe("What is the capital of France?"))

Compare options / when to choose what

The Trainer API is not the only game in town. Here’s how it stacks up against alternatives.

Option Pros Cons Best for
Trainer API High-level, battle-tested, built-in logging/checkpointing, supports mixed precision, gradient accumulation, and eval Less control over internal loop; heavier import overhead Most LLM fine-tuning — from small to large models with Transformers
PyTorch raw loop Full control, minimal dependencies, educational Must handle every detail: device placement, gradient clipping, scheduler, logging — error-prone Understanding training internals; exotic custom losses or loops
Hugging Face accelerate + custom loop Control with convenience utilities for device placement, mixed precision, and distributed training Still requires writing loop logic Custom training experiments that need Trainer-like features but more flexibility
Other frameworks (PyTorch Lightning, etc.) Often richer experiment management (callbacks, logs, CLI) Introduces extra abstraction layers and dependencies Teams already using Lightning for research; need tighter integration with their ecosystem

Rule of thumb: default to Trainer for any LLM fine-tuning task on Transformers. If you hit its limits (e.g., non-standard training dynamics like meta-learning), drop to accelerate and a custom loop — not hand-rolled PyTorch.

Troubleshooting & edge cases

Here are common pitfalls you’ll hit and how to fix them.

1. KeyError: 'attention_mask' or padding errors

  • Symptom: Training crashes with a key error or wrong shape.
  • Cause: Some tokenizers (like GPT-2) don’t set a default padding token, so when padding is applied, the model gets confusing pad_token_id.
  • Fix: Set tokenizer.pad_token = tokenizer.eos_token and often also model.config.pad_token_id = tokenizer.pad_token_id.

2. CUDA out of memory (OOM)

  • Symptom: Crashes with torch.cuda.OutOfMemoryError.
  • Fix: Decrease batch size, use gradient_accumulation_steps to keep effective batch size, enable fp16=True, or switch to a smaller model / use LoRA (see parameter-efficient fine-tuning).
"per_device_train_batch_size": 1,  # reduce
"gradient_accumulation_steps": 8    # effective batch = 8

3. Loss not decreasing

  • Symptom: Loss stays flat or increases.
  • Fix: Check that data collator is set correctly (mlm=False for CLM), learning rate isn’t too high or low, and that labels are aligned (for CLM, labels should equal input IDs shifted). Trainer does this automatically, but if you define your own collator, double-check.

4. ValueError: max_length is larger than the configured

  • Symptom: Tokenizer complains about max_length exceeding model’s n_positions.
  • Fix: Set max_length=min(your_length, model.config.n_positions), or use truncation as shown.

5. Evaluation metrics appear as None

  • If you pass compute_metrics but return a dict, ensure the metric function uses eval_pred (EvalPrediction object) correctly. A simple example:
from transformers import EvalPrediction
import numpy as np

def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    # ignore padding tokens (treat index -100 as ignore)
    return {"accuracy": (predictions == labels).mean()}

But for causal LM, accuracy is less meaningful; often you only want eval loss.

What you learned & what's next

You now know how to implement a training loop with the Trainer API — from setting up TrainingArguments to running trainer.train(). You can configure evaluation, checkpointing, mixed precision, and gradient accumulation, and you can save and reload fine-tuned models. You also understand the mental model: TrainingArguments describes “how,” Trainer orchestrates “what.”

This is a huge step in your LLM fine-tuning journey. Next, you’ll likely want to evaluate your fine-tuned model more rigorously — compute perplexity, measure downstream task performance, or compare baselines. Or, if your model doesn’t fit in GPU memory, explore parameter-efficient fine-tuning (LoRA, QLoRA). Both build directly on the Trainer loop you just implemented.

Keep this lesson as your cheat sheet — the Trainer API will be your constant companion as you fine-tune bigger and better models.

Practice recap

Extend the hands-on example by adding EvaluationStrategy steps, a compute_metrics function that computes token-level accuracy, and try increasing gradient_accumulation_steps to 2 while reducing the batch size to 1 — then compare the resulting loss curves. Save the best model and load it to generate a response to a new question to confirm your fine-tuning worked.

Common mistakes

  • Forgetting to set tokenizer.pad_token (especially with GPT-2) causes padding errors — always set tokenizer.pad_token = tokenizer.eos_token or a dedicated pad token.
  • Using evaluation_strategy and save_strategy inconsistently (e.g., saving on epoch but evaluating on steps) with load_best_model_at_end=True can cause a crash — make at least one of them align and set metric_for_best_model.
  • Setting fp16=True on a CPU-only environment raises errors — only use fp16/bf16 when you have a GPU (or CPU with bf16 support on newer x86).
  • Using the default data collator for causal LM without mlm=False will mask input tokens and destroy label alignment — always use mlm=False for decoder-only models.
  • Ignoring gradient_accumulation_steps and trying to squeeze a large batch into memory — lower per-device batch and compensate with accumulation to stay within GPU memory.

Variations

  1. Use Trainer with a custom compute_metrics function to track task-specific metrics like F1 or accuracy during evaluation, in addition to loss.
  2. Drop to accelerate and write a custom training loop when you need full control over the loss or unusual training dynamics (e.g., meta-learning, contrastive losses).
  3. Experiment with SFTTrainer from the trl library for supervised fine-tuning with chat templates and inline data preprocessing.

Real-world use cases

  • Fine-tuning a small GPT-2 model on a custom Q&A dataset to answer domain-specific questions, using a few hundred examples and a single GPU.
  • Adapting a pretrained Transformer to classify support tickets into categories by loading AutoModelForSequenceClassification and training with Trainer.
  • Production fine-tuning of a large instruction-tuned model (like Llama) with QLoRA and Trainer, leveraging gradient accumulation and mixed precision on multi-GPU setups.

Key takeaways

  • The Trainer API abstracts the entire training loop — data batching, loss computation, backprop, logging, checkpointing, and evaluation — so you configure, not code, the details.
  • TrainingArguments is the single source of truth for hyperparameters; set strategy keys (evaluation, save, logging) consistently to get working checkpoints and automatic best-model reload.
  • Always set a padding token (e.g., pad_token = eos_token) for decoder-only models to avoid cryptic batch errors.
  • The data collator with mlm=False is the correct choice for causal language modeling; it creates shifted labels automatically.
  • Use gradient_accumulation_steps and fp16 to fit larger effective batch sizes on limited GPU memory.
  • When Trainer hits its limits, accelerate gives you a middle ground — built-in device/mixed-precision handling with a custom loop.

Sponsored

Sponsored