M0 · Prerequisites and setupM0.230 min read
Lesson 2 of 106 · Module 1 of 14 · Week 0
Threads:The infrastructure thread
How GPUs work and what happens during a training run
A GPU is a processor with thousands of simple cores that do the same arithmetic on many numbers at once, which is exactly the shape of the matrix multiplications inside a neural network — so it runs them tens to hundreds of times faster than a CPU. During a training run its memory holds four things at once: model weights, activations, gradients, and optimizer state, and it is that memory total, not the GPU's speed, that decides whether your job runs or dies with an out-of-memory error.
By the end you can
- 01Explain in one sentence why a GPU beats a CPU for neural networks, and name the case where a CPU still wins.
- 02List the four consumers of GPU memory during training and say which one disappears at inference time.
- 03Estimate whether a given model will fit in a given amount of VRAM, to one significant figure, with your assumptions stated.
- 04Read a CUDA out of memory message and name three levers that fix it, in the order you should try them.
- 05Describe what actually happens, step by step, in one training iteration.
What a GPU is and what it does for an LLM
A GPU (graphics processing unit) is a co-processor designed for throughput: getting an enormous volume of identical arithmetic done per second. It sits alongside the CPU, has its own dedicated memory, and only works on data that has been explicitly copied into that memory. The CPU is designed for latency: finishing any one unpredictable task as fast as possible.
The contrast in one table:
| Property | CPU | GPU |
|---|---|---|
| Core count | a handful to a few dozen | thousands of simpler cores |
| Per-core cleverness | high — deep branch prediction, large caches, out-of-order execution | low — designed to run in lockstep with its neighbours |
| Optimised for | latency on one task, branchy unpredictable control flow | throughput on many identical operations |
| Memory | system RAM, large and comparatively slow to reach in bulk | dedicated on-package memory (VRAM/HBM), much smaller and much higher bandwidth |
| Wins at | operating systems, business logic, single-threaded code, small data | dense linear algebra, large batches, anything expressible as "same op, many numbers" |
| Loses at | dense linear algebra at scale | branchy logic, tiny workloads where the copy costs more than the compute |
The term you will see for the GPU execution model is SIMT — single instruction, multiple threads. Thousands of threads execute the same instruction stream over different array elements. Because a matrix multiplication computes millions of independent dot products, and each dot product is the same multiply-and-add sequence, the mapping is near perfect. This is the entire reason the modern LLM era is a GPU era.
NVIDIA's ecosystem terms that go with this, at the identity level the exam expects:
| Term | What it is |
|---|---|
| CUDA | NVIDIA's programming platform and API for running general-purpose computation on its GPUs. When PyTorch says device='cuda', this is what it means. |
| VRAM / HBM | The GPU's own memory. HBM (high-bandwidth memory) is the type used on datacentre parts. Its capacity is the hard ceiling on model size for a single GPU. |
| Tensor Cores | Specialised units that perform small matrix multiply-accumulate operations in reduced precision, much faster than general-purpose cores. What makes mixed-precision training worth doing. |
| Driver and CUDA toolkit | The software layers between your framework and the hardware. Version mismatches here are a top cause of "it worked yesterday". |
| A100 (Ampere) · H100 (Hopper) · L40S (Ada) | Successive NVIDIA datacentre GPU generations. Know them as names and rough generational order. Spec-sheet depth is reported as overkill for this exam [FIELD]. |
| MIG (Multi-Instance GPU) | Hard partitioning of one physical datacentre GPU into several isolated smaller GPUs, each with its own slice of memory. |
That last row matters more than it looks: it is a memory feature. Partitioning a GPU gives each tenant a fixed memory slice, which tells you what the scarce resource really is.
How a GPU training run works, step by step
L1 — Intuition: the factory floor
Think of a CPU as one extremely skilled craftsman who can build anything, one item at a time, adapting as they go. A GPU is a factory floor with thousands of workers who can each do exactly one simple motion, all at the same time, on a conveyor belt of identical parts. Ask the craftsman to tighten ten million identical bolts and he will be slow, however skilled. Ask the factory to hand-build a bespoke item and it will be useless.
Neural network training is ten million identical bolts.
Two consequences follow from the factory picture and they are the two facts that dominate practice:
- Parts must be delivered to the floor. The GPU can only work on data in its own memory. Copying between system RAM and GPU memory is comparatively slow, so real pipelines try to move data over once and keep it there. A workload that spends its life copying is called transfer-bound, and it will be slower than doing the work on a CPU.
- The floor has a fixed footprint. VRAM is finite, and much smaller than system RAM. Everything the training step needs must be on the floor simultaneously. When it does not fit, nothing degrades gracefully — the job raises an out-of-memory error and stops.
L2 — Mechanism: one training iteration in five stages
Here is what actually happens when you call a training loop. Every stage has a memory consequence, listed alongside.
| Stage | What happens | Memory effect |
|---|---|---|
| 1. Batch transfer | A batch of tokenised text is copied from system RAM into GPU memory. | Small and transient. |
| 2. Forward pass | The batch flows layer by layer through the model. Each layer's output — its activations — is kept, because the backward pass needs it. | Memory climbs steadily through the pass. Scales with batch size × sequence length × layers. |
| 3. Loss computation | The final layer's output is compared against the true next tokens, producing one scalar loss. | Negligible. |
| 4. Backward pass | Gradients are computed layer by layer in reverse. Each parameter gets a gradient the same shape as itself. Activations are consumed and freed as they are used. | Peak memory usually lands here. Gradient buffer ≈ the size of the weights. |
| 5. Optimizer step | The optimizer combines gradients with its own stored state to update every weight. | Optimizer state is allocated once and persists for the whole run. |
Then repeat, thousands of times. The loss should trend downward; that trend is the only evidence that anything is working.
The four persistent-or-peak memory consumers, which is the list to memorise:
| Consumer | Roughly proportional to | Present at training? | Present at inference? |
|---|---|---|---|
| Model weights | parameter count × bytes per parameter | Yes | Yes |
| Activations | batch size × sequence length × hidden size × layers | Yes | Only the current step's, plus the KV cache |
| Gradients | one value per trainable parameter | Yes | No |
| Optimizer state | a multiple of trainable parameters, depending on optimizer | Yes | No |
This table is the reason inference fits comfortably on hardware that cannot train the same model at all. Drop gradients and optimizer state and you have removed the majority of the training footprint.
Precision is the multiplier on all of it. A number stored in fp32 takes 4 bytes; in fp16 or bf16, 2 bytes; in int8, 1 byte; in fp4/int4 schemes, half a byte. So the same model's weights occupy half as much memory in bf16 as in fp32, which is why mixed precision — keeping most arithmetic in 16-bit while holding a few sensitive accumulations in 32-bit — became the default for training. You will meet quantization properly in the deployment module; here it is enough to know that precision is a straight multiplier on the memory bill.
Optimizer state is the consumer that surprises people. Plain SGD stores essentially nothing extra. Adam and AdamW, which are what LLM work actually uses, store two additional values per parameter — a running average of gradients and a running average of squared gradients. So Adam's state alone is roughly twice the size of the model's trainable parameters, in whatever precision it keeps them. That single fact explains most "why can't I fine-tune a model that loads fine" confusion.
L3 — Where the time actually goes: compute-bound vs memory-bound
A useful mental model, and one that recurs throughout this course: any GPU operation is limited either by how fast the cores can do arithmetic (compute-bound) or by how fast numbers can be moved between memory and cores (memory-bandwidth-bound).
| Regime | The bottleneck | Typical of | What helps |
|---|---|---|---|
| Compute-bound | arithmetic throughput | large matrix multiplications, training with big batches | more or faster cores; lower precision on Tensor Cores; better kernels |
| Memory-bandwidth-bound | moving numbers in and out of memory | generating one token at a time, small batches, elementwise ops | larger batches; fusing operations; reducing bytes moved (quantization) |
| Transfer-bound | the link between system RAM and GPU memory | badly written data loaders, tiny workloads | keep data resident; overlap loading with compute |
Training with a healthy batch size is usually compute-bound, which is the happy case: the hardware is doing what it is for. Single-stream text generation is famously memory-bound, because each new token requires reading the whole set of weights out of memory to produce a handful of numbers. That asymmetry is the root of most modern serving optimisation — batching many requests together, caching, and reducing the bytes each token has to read — and it is the subject of 12-05 much later.
Two further mechanisms worth naming now because they explain error messages you will see:
- Asynchronous execution. GPU work is queued, not executed inline. Your Python line returns before the GPU has finished. A consequence: a CUDA error's stack trace can point at an innocent line, because the failure surfaced whenever the queue was next synchronised. If a traceback makes no sense, this is usually why.
- The caching allocator. Frameworks do not return freed GPU memory to the driver immediately; they keep a pool to avoid slow allocation calls. This means "memory in use" as your framework reports it and "memory the driver thinks is allocated" differ, and it means memory can become fragmented — you can have 3 GB free in total and still fail to allocate a contiguous 2 GB block. Hence the genuinely infuriating error that says memory is available and then refuses to give it to you.
GPU vs CPU vs TPU, and VRAM vs system RAM
Two comparison tables. The first is the processor choice.
| CPU | GPU | Other accelerators (TPU and similar) | |
|---|---|---|---|
| Design goal | latency on unpredictable work | throughput on dense parallel arithmetic | throughput on a narrower set of tensor operations |
| Cores | few, complex | thousands, simple | large systolic matrix units |
| Memory | system RAM — plentiful, comparatively low bandwidth | dedicated VRAM/HBM — scarce, high bandwidth | on-package high-bandwidth memory |
| Best for | data prep, tokenisation, classical ML on small data, orchestration | training and serving neural networks; GPU-accelerated dataframes | large-scale training in the ecosystems that provide them |
| Software | everything | CUDA and the whole PyTorch/TensorFlow stack | vendor-specific stacks |
| Where it appears in this course | tokenisation, chunking, evaluation scripts, small-corpus retrieval | training, fine-tuning, inference, RAPIDS | mentioned for completeness only |
Note the row that most candidates skip: the CPU is not a failure case. Tokenising a corpus, chunking documents, computing BLEU, running a similarity search over a few thousand embeddings — all of these are perfectly good CPU work, and reaching for a GPU adds transfer cost for no benefit. Knowing when GPU acceleration does not pay off is an explicitly examinable judgement, and it comes up again in the RAPIDS material.
The second table is the memory distinction, which is the one that causes the most confusion in a first lab.
| System RAM | GPU VRAM | |
|---|---|---|
| Who uses it | the CPU, your Python process, the dataset you loaded | the GPU, and only the tensors you explicitly moved there |
| Typical size on a modest machine | tens of GB | single-digit to low-tens of GB, varying enormously by part |
| What fills it in an LLM workload | the dataset, the tokenizer, the Python interpreter, checkpoint files being written | weights, activations, gradients, optimizer state |
| Error when exhausted | the OS kills the process, or swaps and grinds | CUDA out of memory, immediately and cleanly |
| Can one substitute for the other? | Partly — "offloading" moves some tensors to system RAM at a heavy speed cost | No; the GPU cannot compute on tensors that are not in its own memory |
The two most common confusables in this area, stated flatly:
- "My machine has 32 GB, so a 20 GB model fits." No. 32 GB of system RAM has almost nothing to do with whether a model fits in VRAM. These are separate pools, and only one of them the GPU can compute in.
- "The GPU is too slow." Usually it is not slow, it is out of memory, or it is memory-bandwidth-bound, or your data loader is starving it. Speed complaints in this field are overwhelmingly memory complaints in disguise.
Worked example: constructed memory arithmetic for a 7-billion-parameter model
Everything in this section is a constructed illustrative example, not a measured benchmark. The purpose is to show the shape of the arithmetic; the real number for any specific model and framework differs, and you should always confirm against your own nvidia-smi reading rather than trusting an estimate. Framework overhead, kernel workspaces and allocator behaviour add real amounts that a back-of-envelope calculation deliberately ignores.
Suppose a model with 7 billion parameters. Take the four consumers in turn.
Step 1 — weights. Bytes per parameter depends entirely on precision:
fp32 (4 bytes): 7e9 × 4 = 28e9 bytes ≈ 28 GB
bf16 (2 bytes): 7e9 × 2 = 14e9 bytes ≈ 14 GB
int8 (1 byte): 7e9 × 1 = 7e9 bytes ≈ 7 GB
4-bit (0.5): 7e9 × 0.5 = 3.5e9 ≈ 3.5 GB
Already the headline result: the same model is an 8× different memory problem depending on precision alone. Note these are GB in the decimal sense — M0.4 is entirely about why that distinction is not pedantry.
Step 2 — gradients. One gradient value per trainable parameter, typically in the same precision as the compute. Training all 7B parameters in bf16:
gradients ≈ 7e9 × 2 = 14e9 bytes ≈ 14 GB
Step 3 — optimizer state. Adam/AdamW keeps two values per parameter. Many implementations keep them in fp32 for numerical stability even when compute is bf16, so assume 4 bytes each and state that assumption:
optimizer state ≈ 7e9 × 2 values × 4 bytes = 56e9 bytes ≈ 56 GB
Step 4 — running total before activations.
weights (bf16) 14 GB
gradients (bf16) 14 GB
optimizer (fp32 ×2) 56 GB
------
subtotal 84 GB ← and no activations yet, and no framework overhead
Step 5 — activations, order of magnitude. Activation memory scales with batch × sequence × hidden × layers and depends heavily on implementation details such as whether activation checkpointing is enabled. Rather than invent a figure, note the scaling and the lever: at a large batch and long sequence, activations can be the dominant term, and activation checkpointing trades recomputation time to shrink it substantially. Treat activations as "somewhere between a few GB and the largest single item on the list, depending on batch size", and measure rather than guess.
The conclusion this arithmetic forces: full fine-tuning of a 7B model with Adam is a job for many tens of GB of VRAM — well beyond a single consumer card and beyond a free notebook tier. This is not a marginal shortfall you can squeeze past with a smaller batch. It is roughly an order of magnitude.
Step 6 — now do the same arithmetic for a LoRA-style adapter. Suppose you freeze all 7B base weights and train only a small set of added parameters — say 0.1% of the model, which is 7 million trainable parameters. Constructed figure; real adapter sizes vary with rank and which layers you target.
base weights, frozen, 4-bit ≈ 3.5 GB
gradients: 7e6 × 2 bytes ≈ 0.014 GB
optimizer: 7e6 × 2 × 4 bytes ≈ 0.056 GB
---------
persistent subtotal ≈ 3.6 GB + activations + overhead
The gradient and optimizer terms have collapsed from 70 GB to well under a tenth of a GB, because they scale with trainable parameters, not total parameters. Activations do not shrink as dramatically, since data still flows through the whole frozen network. But the run has gone from impossible-on-one-GPU to plausible-on-one-modest-GPU.
That is the whole reason parameter-efficient fine-tuning exists, and you can now derive it from four lines of arithmetic rather than take it on faith. When the course reaches LoRA in the customization module, this is the calculation underneath it.
Step 7 — inference, for contrast. Drop gradients and optimizer state entirely:
weights (bf16) 14 GB + KV cache + the current step's activations
weights (4-bit) 3.5 GB + KV cache + the current step's activations
The KV cache grows with how many tokens you are holding in context and how many requests you are serving concurrently, and it is the term that makes serving-capacity planning interesting. Again: 12-05.
Provoking and reading an out-of-memory error
The single most useful hour you can spend on GPU literacy is deliberately causing an out-of-memory error while you are calm, so that when one happens under time pressure you recognise it in seconds. This is why this lesson sits before the first lab rather than inside it.
The provocation. In a notebook with any GPU attached, allocate progressively larger tensors until it fails:
import torch
assert torch.cuda.is_available()
blocks = []
try:
while True:
# 1 GiB of fp32 = 268,435,456 elements of 4 bytes
blocks.append(torch.empty(268_435_456, dtype=torch.float32, device='cuda'))
print(f"allocated {len(blocks)} GiB "
f"reserved={torch.cuda.memory_reserved()/2**30:.2f} GiB")
except torch.cuda.OutOfMemoryError as e:
print("OOM after", len(blocks), "GiB")
print(e)
Three things to observe, and they are the lesson:
- The count at which it fails is lower than the card's advertised capacity. The driver, the CUDA context, and framework workspaces all take a cut before your first tensor. Advertised VRAM is a gross figure, not a budget you get to spend.
- The error text names the numbers. A
CUDA out of memorymessage typically reports how much was requested, how much is free, and how much the allocator has reserved. Reading those three numbers is the diagnosis. If free memory exceeds the requested amount and it still failed, you are looking at fragmentation, not exhaustion. - Freeing is not immediate.
del blocksfollowed bytorch.cuda.empty_cache()returns memory to the driver; deleting alone returns it to the framework's pool. In a notebook, the surest reset is restarting the runtime, because a dead reference held by an output cell or a traceback object will pin gigabytes indefinitely.
The second provocation, which is more realistic: take a working training loop and increase the batch size until it fails. This teaches the more useful lesson, that batch size is the fastest lever you have, and that the failure boundary depends on sequence length too — a batch of 8 at 512 tokens and a batch of 2 at 2,048 tokens are similar activation loads.
The fix ladder, in the order to try it:
| Order | Lever | Costs you | Typical effect |
|---|---|---|---|
| 1 | Restart the runtime | a minute | reclaims everything pinned by stale references — fixes more cases than anyone admits |
| 2 | Reduce batch size | more steps per epoch | large and immediate; activations scale with it |
| 3 | Reduce sequence length / truncate inputs | possibly truncated context | large; activations scale with it too |
| 4 | Gradient accumulation | wall-clock time | keeps the effective batch size while shrinking the resident one |
| 5 | Mixed precision (bf16/fp16) | some numerical care | roughly halves weights, gradients and activations |
| 6 | Activation checkpointing | recomputation time in the backward pass | substantially cuts activation memory |
| 7 | Parameter-efficient fine-tuning (LoRA and friends) | you no longer update all weights | removes almost all gradient and optimizer state — see section 4 |
| 8 | Quantize the base model | some accuracy risk | halves or quarters the weight term |
| 9 | A bigger GPU, or several | money and complexity | the honest answer when the job genuinely needs it |
Steps 1 through 4 are free and cost nothing but time. Reaching for step 9 before trying steps 1 through 4 is the classic beginner move.
Reading the message properly. Three distinct failures wear similar-looking errors:
| Message pattern | What it actually means | First move |
|---|---|---|
CUDA out of memory. Tried to allocate X; Y free where X > Y | genuine exhaustion | the fix ladder above |
CUDA out of memory where free memory clearly exceeds the request | fragmentation of the allocator pool | restart the runtime; avoid wildly varying tensor sizes |
| Process killed with no CUDA message at all | system RAM exhausted, not VRAM | shrink what you loaded on the CPU side; stream the dataset |
CUDA error: device-side assert triggered | not a memory problem — usually an out-of-range index, often a token id beyond vocabulary size | look at your data, not your hardware |
no kernel image is available / driver-version errors | toolkit/driver mismatch | fix the environment, do not tune the model |
That fourth row is worth internalising: not every CUDA error is a memory error, and a device-side assert about indices is a data bug that happens to surface on the GPU. Misdiagnosing it as a memory problem sends you tuning batch sizes for an hour on a problem batch size cannot touch.
Why GPU behaviour matters for the NCA-GENL exam
Two reasons, and they pull in different directions, so it is worth being precise about how deep to go.
Reason one: it is examinable at identity level. The Software Development domain is 24% of the blueprint and includes official objective 4.4 — identify system data, hardware, or software components required to meet user needs. That objective is asking exactly the question this lesson answers: given a described need, what hardware does it require, and why. Elsewhere the blueprint covers GPU architecture generations, MIG partitioning, quantization and mixed precision, distributed training and collectives, and latency/throughput capacity planning. All of those are downstream of "what is a GPU and what fills its memory".
Reason two: it is explicitly not examinable at spec-sheet level. Candidate reports are consistent that extensive dives into GPU hardware specifications were "overkill" and did not appear [FIELD], and the course index carries that as a deliberate depth ceiling. So the calibration is: know what a GPU does, why memory is the binding constraint, and which lever fixes which symptom. Do not memorise HBM bandwidth figures, SM counts, or clock speeds. If you catch yourself learning a spec table, you are spending exam-prep time on something reported as absent.
The practical consequence is that this lesson's payload is the four-consumer memory model and the fix ladder — both of which are decision rules, which is the form the questions take.
Where it feeds forward:
| Later material | What it inherits from here |
|---|---|
| The first hands-on lab in this course | that it will run on a GPU with finite memory, and that an OOM is a solvable, expected event rather than a catastrophe |
01-06 Gradient descent and backpropagation | the mechanism behind stage 4 and 5 of the training loop, at the conceptual level |
| Quantization and mixed precision (Software Development module) | precision as a straight multiplier on memory; fp32/bf16/fp16/int8/fp8 as a memory-and-accuracy trade |
| GPU architectures and MIG (Software Development module) | why partitioning a GPU is fundamentally a memory-allocation feature |
| Distributed training and collectives | why you would ever need more than one GPU: because the four-consumer total exceeded one card |
| Latency, throughput and capacity planning | prefill vs decode, and the compute-bound/memory-bound distinction from L3 |
12-05 The KV cache | the memory-bound generation regime, which is where serving cost actually comes from |
| The LoRA / PEFT material | section 4 step 6, which is the argument for parameter-efficient fine-tuning |
If you understand only one thing from this lesson, make it this: capability in this field is bounded by memory far more often than by speed. Almost every architectural choice you will study — quantization, LoRA, activation checkpointing, paged attention, MIG, model parallelism — exists because something did not fit.
Common mistakes with GPU memory and training runs
| Mistake | Symptom | Cause | Fix |
|---|---|---|---|
| Confusing system RAM with VRAM | "I have 32 GB, why won't a 20 GB model load?" | two separate memory pools; only VRAM is computable-in by the GPU | check VRAM specifically (nvidia-smi), and budget against that number alone |
| Budgeting against advertised VRAM | OOM at what should be a comfortable size | driver, CUDA context and framework workspaces take a cut before your first tensor | treat advertised capacity as gross; leave real headroom |
| Forgetting optimizer state | model loads fine, then OOMs the instant training starts | Adam/AdamW stores about two extra values per trainable parameter | include it in the estimate, or switch to PEFT so it scales with adapters instead |
| Assuming inference memory ≈ training memory | over-provisioning for serving, or under-provisioning for training | gradients and optimizer state exist only during training | use the four-consumer table; inference drops two of the four |
| Tuning batch size for a device-side assert | an hour lost; the error never moves | an out-of-range index (often a token id ≥ vocab size) is a data bug, not a memory bug | read the error class before choosing the lever |
| Trusting the traceback line number | the reported failing line is obviously innocent | GPU execution is asynchronous; errors surface at the next synchronisation | reason about the whole step, or force synchronous execution while debugging |
| Leaving tensors pinned in a notebook | memory never comes back after a failure | output cells, tracebacks and stale variables hold GPU references | restart the runtime; it is step 1 of the fix ladder for a reason |
| Blaming GPU speed for a starved pipeline | low GPU utilisation, slow epochs, no OOM | the data loader or the host-to-device copy is the bottleneck | check utilisation before optimising the model; keep data resident and overlap loading |
The pattern behind six of these eight is the same: the symptom appears on the GPU, but the cause is somewhere else — in the host, in the data, in the allocator, in your assumptions. Building the habit of asking "which of the four consumers grew, or is this even a memory problem" is worth more than any amount of hardware trivia.
Do I need a GPU to study for the NCA-GENL exam?
No. The exam is a proctored multiple-choice paper; nothing in it requires you to have trained anything. For the labs in this course, a free hosted notebook tier with an attached accelerator is sufficient for everything designed to run, and the course's own design keeps GPU-dependent work modest for exactly this reason. What you cannot do on free or modest hardware is a full fine-tune of a multi-billion-parameter model — section 4 shows why in four lines of arithmetic — and the course does not ask you to. Understanding the constraint is the examinable skill; owning the hardware is not.
Why does a GPU train neural networks faster than a CPU?
Because a neural network's core operation is matrix multiplication, which is millions of independent dot products, and a GPU has thousands of cores that execute the same multiply-and-add instruction on different data simultaneously. A CPU has a few dozen very clever cores optimised for running different instructions in sequence, which is the wrong shape for this work. The gap is a throughput gap, not a cleverness gap. It disappears for branchy, unpredictable, or small workloads — where the CPU is genuinely the better choice, and where the cost of copying data to the GPU exceeds the compute saved.
What causes a CUDA out of memory error during training?
The sum of four things exceeded the GPU's available VRAM: model weights, activations from the forward pass, gradients, and optimizer state. Which one dominated depends on the run. If it fails immediately on loading, weights are too big. If it loads and then fails at the first step, gradients and optimizer state are the likely culprits — Adam alone stores roughly two extra values per trainable parameter. If it fails a few steps in, or only at longer sequence lengths, activations are the term that grew. A fifth possibility is that nothing exceeded anything and the allocator is fragmented, which the error message reveals by reporting more free memory than was requested.
How much VRAM do I need to fine-tune a 7B model?
Far more than you would guess from the weight size alone, if you fine-tune every parameter with Adam. The constructed estimate in section 4 puts weights at about 14 GB in bf16, gradients at another 14 GB, and Adam state at roughly 56 GB if it is kept in fp32 — around 84 GB before activations or framework overhead, which is many tens of GB beyond a single consumer card. With a parameter-efficient adapter over a 4-bit base, the same constructed arithmetic falls to roughly 3.6 GB persistent plus activations. Those are illustrative figures for showing the shape of the trade, not measurements; confirm against your own environment. The stable takeaway is the ratio, not the digits: gradients and optimizer state scale with trainable parameters, so freezing the base collapses them.
What is the difference between VRAM and system RAM for LLM work?
VRAM is the GPU's own memory and is the only place the GPU can compute. System RAM belongs to the CPU and holds your Python process, your dataset, and anything not explicitly moved to the device. VRAM is much smaller and much higher bandwidth. Exhausting VRAM produces an immediate, clean CUDA out of memory exception; exhausting system RAM usually gets your process killed by the operating system with no CUDA message at all — which is how you tell the two apart from the failure alone. Offloading can park some tensors in system RAM, but at a substantial speed cost, because they have to travel back over a comparatively slow link every time they are needed.
Does batch size change my training results or only my memory use?
Primarily memory and speed, but not only those. Batch size does not change what each example is, and no operation mixes one example with another during a forward pass, so results are equivalent up to floating-point ordering effects. However, batch size interacts with the optimizer: the gradient of a larger batch is an average over more examples, which changes the effective noise in each update and therefore interacts with the learning rate. Gradient accumulation exists precisely to decouple these — it lets you keep a large effective batch for optimisation purposes while only holding a small one in memory at a time. For the purposes of this exam, the decision rule is what matters: batch size is your fastest memory lever, and gradient accumulation is how you use it without changing the optimisation you intended.
What actually happens in one training iteration?
Five stages. A batch is copied into GPU memory. The forward pass sends it layer by layer through the model, keeping each layer's activations because the backward pass will need them — memory climbs throughout. The loss compares the final output with the true next tokens, producing one scalar. The backward pass walks in reverse computing a gradient for every parameter, consuming activations as it goes; peak memory usually lands here. Finally the optimizer combines each gradient with its stored state and updates every weight. Then it repeats, thousands of times, and the loss curve trending downward is your only evidence anything is working.
Glossary recap: the terms this lesson introduced
| Term | Definition |
|---|---|
| GPU | A throughput-oriented co-processor with thousands of simple cores that execute the same instruction over many data elements. |
| CPU | A latency-oriented processor with few complex cores, better at branchy sequential work. |
| SIMT | Single instruction, multiple threads — the GPU execution model that makes matmul fast. |
| CUDA | NVIDIA's platform and API for general-purpose GPU computing; what device='cuda' refers to. |
| VRAM | The GPU's own memory. The hard ceiling on what you can run on one device. |
| HBM | High-bandwidth memory, the memory type used on datacentre GPUs. |
| Tensor Cores | Specialised reduced-precision matrix multiply-accumulate units; what makes mixed precision worthwhile. |
| Activations | Intermediate layer outputs retained during the forward pass because the backward pass needs them. |
| Gradients | One value per trainable parameter, produced by the backward pass. Absent at inference. |
| Optimizer state | Extra per-parameter values an optimizer maintains. Adam/AdamW keeps about two; SGD keeps essentially none. Absent at inference. |
| Precision (fp32 / bf16 / fp16 / int8) | How many bytes each number occupies — 4, 2, 2, 1 — and therefore a direct multiplier on the memory bill. |
| Mixed precision | Running most arithmetic in 16-bit while keeping sensitive accumulations in 32-bit. |
| Quantization | Storing weights at lower precision to cut memory and bandwidth, at some accuracy risk. |
| Gradient accumulation | Summing gradients over several small batches before stepping, preserving a large effective batch at small resident memory. |
| Activation checkpointing | Discarding some activations and recomputing them in the backward pass; trades time for memory. |
| Compute-bound | Limited by arithmetic throughput. Typical of large-batch training. |
| Memory-bandwidth-bound | Limited by moving numbers between memory and cores. Typical of one-token-at-a-time generation. |
| Transfer-bound | Limited by the host-to-device link; the signature of a starved data loader. |
| Asynchronous execution | GPU work is queued, so an error's reported line may not be the guilty one. |
| Caching allocator | The framework's GPU memory pool, which is why freed memory is not immediately returned and why fragmentation happens. |
| Fragmentation | Enough free memory in total, but no contiguous block large enough. Diagnosed when free exceeds requested and it still fails. |
| OOM (out of memory) | The CUDA out of memory exception raised when an allocation cannot be satisfied. |
| MIG (Multi-Instance GPU) | Hard partitioning of one datacentre GPU into isolated instances, each with its own memory slice. |
| A100 / H100 / L40S | Successive NVIDIA datacentre GPU generations — Ampere, Hopper, Ada. Know the names, not the spec sheets. |
Key takeaways on GPUs and training runs
- A GPU is a throughput machine. Thousands of simple cores doing the same arithmetic on different numbers. Matrix multiplication is precisely that shape, which is why deep learning lives here.
- A CPU is not a failure case. Tokenisation, chunking, evaluation scripts and small-corpus retrieval are good CPU work; moving them to a GPU can be slower. Knowing when acceleration does not pay is an examinable judgement.
- Four things fill GPU memory during training: weights, activations, gradients, optimizer state. Memorise this list; it answers most capacity questions you will be asked.
- Inference drops two of the four. No gradients, no optimizer state — which is why serving fits on hardware that could never train the same model.
- Precision is a straight multiplier. fp32 is 4 bytes per number, bf16 is 2, int8 is 1. The same model can be an 8× different memory problem before you change anything else.
- Adam's state is roughly twice the trainable parameter count, often kept in fp32. This is the term that turns "the model loaded fine" into an OOM one step later.
- Gradients and optimizer state scale with trainable parameters, not total parameters. That single sentence is the entire argument for parameter-efficient fine-tuning, and section 4 derives it.
- Advertised VRAM is gross, not net. Driver, context and workspaces take a cut. Budget with headroom.
- Provoke an OOM on purpose, once. Then read the three numbers in the message — requested, free, reserved — and you will diagnose real ones in seconds. If free exceeds requested, it is fragmentation, and a runtime restart is the fix.
- Work the fix ladder in order: restart, batch size, sequence length, gradient accumulation, mixed precision, activation checkpointing, PEFT, quantization, bigger hardware. The first four are free.
- Not every CUDA error is a memory error. A device-side assert about indices is a data bug; a "no kernel image" error is an environment bug. Classify before you tune.
- Depth ceiling, deliberately. Know what a GPU does and why memory binds you. Candidate reports say spec-sheet detail did not appear [FIELD]; spend that time on tool identity and decision rules instead.
Next: getting a working GPU notebook without owning a GPU
You now know what a GPU does, what fills its memory, and how to read the error it raises when you ask for too much. What you do not have yet is somewhere to run any of it. That is a fifteen-minute problem with a few genuinely non-obvious traps — accelerator runtimes that are not attached by default, sessions that reset and take your files with them, quotas that change without notice, and dependency installs that vanish the moment the runtime restarts.
Next: M0.2a Setting up Google Colab and Jupyter notebooks — choosing between a hosted notebook and a local install, attaching and verifying an accelerator, the verification cell to run before you trust anything, what survives a restart and what does not, and the cost and quota realities to plan around.