Introduction to Fine-Tuning LLMs

A Lesson Module for Graduate AI Agents Course


FieldDetails
Duration60–75 minutes (lecture ~45 min, guided walkthrough ~20 min)
PrerequisitesFamiliarity with how LLMs work (tokens, next-token prediction, transformer architecture at a high level)
FormatLecture + read-through walkthrough. Students do not write code in this lesson — they will code in the next exercise.
PlatformTinker API (thinkingmachines.ai)

1. What Is Fine-Tuning and Why Does It Matter?

Pre-trained LLMs are generalists. They have seen enormous amounts of text during pre-training and can do many things passably, but they are not specialists at anything in particular. Fine-tuning takes a pre-trained model and continues training it on a much smaller, task-specific dataset so the model internalizes a particular skill or behavior.

The key intuition: pre-training teaches the model language — grammar, facts, reasoning patterns, code syntax. Fine-tuning teaches the model a job — how to respond in a particular format, follow instructions, translate between representations, or behave according to specific rules.

When does fine-tuning make sense over alternatives?

Consider three approaches to making an LLM do something new:

The rule of thumb: If the task requires knowledge, use RAG. If the task requires a skill, fine-tune. Many real applications use both.


2. Fine-Tuning Approaches: Full, LoRA, and QLoRA

2.1 Full-Parameter Fine-Tuning

The simplest conceptual approach: take all the model's parameters and continue training them with gradient descent on your new dataset, using the same cross-entropy (next-token prediction) loss used during pre-training.

For a 7B parameter model, this means updating all 7 billion parameters on every training step. This requires:

Total: a 7B model easily requires 60+ GB of GPU memory for full fine-tuning. A 70B model requires a cluster of GPUs.

Full fine-tuning gives you the maximum flexibility — every parameter can change — but it is expensive and can be overkill for many tasks. It also risks catastrophic forgetting: if your fine-tuning dataset is small or narrow, the model may lose general capabilities it had before.

2.2 LoRA (Low-Rank Adaptation)

The core idea: Instead of updating all the model's weight matrices, freeze the original weights and attach small trainable "adapter" matrices alongside them.

For each weight matrix W (size n × n) in the transformer, LoRA adds two small matrices: B (size n × r) and A (size r × n), where r is the rank — typically 16 or 32. The effective weight becomes:

Only B and A are trained. The original W is frozen.

Why this works: The key insight from the LoRA paper (Hu et al., 2021) is that the change needed during fine-tuning is typically low-rank — you don't need to modify all dimensions of every weight matrix. A rank-32 adapter on a model with hidden dimension 4096 adds only 2 × 4096 × 32 = 262,144 parameters per layer, compared to 4096 × 4096 = 16.8 million in the original matrix. That is about 1.5% of the parameters.

Practical benefits:

What rank to use? Tinker defaults to rank 32. Higher ranks give the model more capacity to learn complex changes but cost more memory and compute. For reinforcement learning and small supervised learning datasets, even low ranks (8–16) work well. For large SL datasets, you may need rank 64 or 128. A useful rule of thumb from the Tinker documentation: LoRA gives good results as long as the number of LoRA parameters is at least as large as the number of completion tokens in your training data.

Does LoRA match full fine-tuning? The Thinking Machines team (who build Tinker) published research showing that LoRA matches full fine-tuning for RL and small-to-medium SL datasets. For very large SL datasets, full fine-tuning still has an edge — but for most practical tasks, LoRA is sufficient.

2.3 QLoRA (Quantized LoRA)

QLoRA (Dettmers et al., 2023) combines LoRA with quantization of the frozen base weights. Instead of keeping the base model in 16-bit precision, QLoRA stores it in 4-bit (NF4 format), dramatically reducing memory usage.

The math: a 7B model in 16-bit needs ~14 GB for weights alone. In 4-bit, it needs ~3.5 GB. Add the LoRA adapters (still in 16-bit) and the optimizer state for those adapters, and you can fine-tune a 7B model on a single consumer GPU with 8 GB of VRAM.

The tradeoff: Quantization introduces some approximation error. The base model is slightly less precise, and there can be subtle quality differences compared to full-precision LoRA. In practice, QLoRA achieves results very close to standard LoRA for most tasks, and the memory savings are enormous.

When to use which:

ApproachMemoryQualityWhen to Use
Full fine-tuningVery high (60+ GB for 7B)Best (all params trainable)Large datasets, production models, research
LoRAModerate (16–20 GB for 7B)Near-full-FT for most tasksMost practical fine-tuning
QLoRALow (8–12 GB for 7B)Slightly below LoRALimited GPU budget, prototyping, education

2.4 Where Tinker Fits

In a traditional fine-tuning setup, you would run all of this on your own GPUs — managing CUDA, distributed training, memory management, and fault recovery. This is where most of the engineering pain lives.

Tinker takes a different approach: you write a simple Python loop on your laptop (CPU only) that specifies the data, the loss function, and the optimizer settings. Tinker's servers handle the actual GPU computation — LoRA training distributed across their cluster. You never touch a GPU.

The API has four core primitives:

This is not a black-box "upload your data and we train for you" service. You control the training loop, the batch size, the learning rate, what data goes in each batch, and when to evaluate. Tinker just handles the distributed GPU infrastructure.


3. Supervised Fine-Tuning: The Mechanics

Regardless of whether you use full fine-tuning, LoRA, or QLoRA, the supervised fine-tuning (SFT) procedure follows the same pattern:

3.1 The Training Example Format

Each training example is a sequence of tokens divided into two parts:

For example, if you are training a model to translate English to Pig Latin:

The model is trained to predict the completion tokens given the prompt tokens. The weight mask ensures the loss only counts on the part we care about.

3.2 Next-Token Prediction and the Shift

LLMs predict the next token given all previous tokens. So the training data must be "shifted by one": the input at position i is the token at position i, and the target at position i is the token at position i+1.

Notice how the weight switches from 0 to 1 at exactly the point where the completion begins. Everything before that is context; everything after is what the model should learn.

3.3 The Training Loop

A single training step:

  1. Take a batch of training examples (e.g., 16 examples)

  2. Run the forward pass: compute the model's predicted probability of each target token

  3. Compute the loss: negative log-likelihood of the correct target tokens, weighted by the weight mask

  4. Run the backward pass: compute gradients of the loss with respect to the trainable parameters

  5. Update the parameters using an optimizer (typically Adam)

Repeat for many batches. Track the loss — it should decrease over time. Periodically evaluate on held-out test data to check for overfitting.

3.4 Key Hyperparameters


4. Walkthrough: Teaching Pig Latin with Tinker

Now we will walk through a complete fine-tuning example step by step. You will not write code in this section — just read through and understand each step. In the next exercise, you will write a similar script yourself for a more challenging task.

4.1 The Task

Teach Llama-3.2-1B to translate English phrases into Pig Latin. The rules are:

This is a good first fine-tuning task because: the rule is simple and deterministic, we can generate our own training data, and the base model is terrible at it (so the before/after contrast is dramatic).

4.2 Setup

Two dependencies:

The tinker package handles the API communication. The transformers package provides the tokenizer.

4.3 Create a Training Client

This creates a LoRA adapter (rank 32) attached to the frozen Llama-3.2-1B weights on Tinker's servers. The training_client object is your handle to that remote model. The tokenizer runs locally on your machine.

4.4 Define the Training Data

Just 7 handwritten examples:

In a real fine-tuning job you would use thousands or millions of examples. Here we use 7 to keep the demo simple and fast.

4.5 Tokenize with Weight Masks

Each example must be converted into a Datum — Tinker's format for a training example. The critical step is setting the weight mask so the model only learns to predict the Pig Latin output, not the English prompt.

What is happening here:

  1. The prompt "English: banana split\nPig Latin:" is tokenized into tokens. Each gets weight 0.

  2. The completion " anana-bay plit-say\n\n" is tokenized. Each gets weight 1. (Note the leading space — it separates the completion from the colon.)

  3. The full token sequence is shifted by one position to create input/target pairs for next-token prediction.

  4. The result is packaged as a Datum with model_input (what the model sees) and loss_fn_inputs (targets and weights for the loss function).

If we visualize the tokens around the transition from prompt to completion:

The model learns: "after seeing English: banana split\nPig Latin:, the next tokens should be anana-bay plit-say."

4.6 Train

The training loop is remarkably simple — 6 steps, all 7 examples in each batch:

What is happening on each step:

  1. forward_backward sends all 7 tokenized examples to Tinker. The server runs the forward pass through Llama-3.2-1B + the LoRA adapter, computes the cross-entropy loss (weighted by the weight mask), and backpropagates to get gradients for the adapter parameters. It returns a future — the computation happens asynchronously.

  2. optim_step tells the server to update the adapter weights using Adam with learning rate 1e-4. Also returns a future.

  3. We call .result() on both futures to wait for completion.

  4. We compute the weighted average loss from the returned log-probabilities. This should decrease over the 6 steps.

Note: both API calls return futures immediately. We submit both before waiting, which allows them to pipeline on the server.

Expected output (approximate):

The loss drops sharply because we have only 7 examples and the model can memorize them quickly. In a real task with thousands of examples, the loss would decrease more gradually.

4.7 Sample from the Fine-Tuned Model

To generate text, we first save the current adapter weights and create a sampling client:

Expected output (approximate):

The model has learned the Pig Latin pattern — it moves consonants to the end and appends "ay" — even for the phrase "coffee break" which was not in the training data. The outputs are not perfectly consistent (note the slight variations across samples), which reflects the fact that we trained on only 7 examples for 6 steps. More data and more training would improve consistency.

4.8 What Just Happened

Let's recap what the model actually learned:

All of the actual computation — the forward passes, the gradient calculations, the weight updates — happened on Tinker's GPU cluster. The Python script on your laptop only sent data and received results over the network.


5. Looking Ahead: The SQL Exercise

In the next exercise, you will write a fine-tuning script yourself. Instead of Pig Latin (a toy task with 7 examples), you will teach the same Llama-3.2-1B model to translate natural language questions into SQL queries, using a dataset of 78,000 real examples from WikiSQL and Spider.

The SQL task is a much stronger test of fine-tuning because:

The code structure will be nearly identical to the Pig Latin walkthrough: load data, tokenize with weight masks, run a training loop, sample before and after. The only differences are the dataset (5,000 examples from a file instead of 7 handwritten ones) and the prompt template (table schemas instead of English phrases).


6. Key Takeaways


7. References and Further Reading


End of Module — Introduction to Fine-Tuning LLMs