Before we can understand why LLMs need GPUs or why they run out of memory, we need a clear picture of what they are and how they actually work.
A Large Language Model (LLM) is a deep neural network trained on vast amounts of text to model the statistical structure of language. At its core, the model learns a probability distribution over tokens: given a sequence of prior tokens, it estimates the likelihood of every possible next token and samples from that distribution to generate text.
What makes this powerful — and computationally expensive — is scale. To generalise across all of human language, code, mathematics, and reasoning, the model must compress an enormous amount of structured knowledge into its parameters: the numeric weights that define every linear transformation inside the network. Qwen2.5-7B has 7.61 billion of them. These weights are learned during training by repeatedly adjusting each parameter to reduce prediction error, a process that requires storing not just the weights themselves but gradients and optimiser states for every single one.
Almost all modern LLMs use the Transformer architecture (introduced by Google in 2017). Understanding its structure explains why GPUs are required and where memory goes.
These three modes have completely different memory requirements. Understanding the difference is essential before we can understand OOM errors.
Throughout this guide we use Qwen2.5-7B as our case study. It's a real, production-grade model small enough to fine-tune on a single high-end GPU, but large enough to exhibit all the memory challenges at scale. Here is its exact configuration from the official Hugging Face model card:
The CPU and GPU are both processors, but they are designed for completely different jobs. Understanding this architectural difference explains why you cannot simply use a CPU for LLM training — it's not a software preference, it's a hardware constraint.
A CPU (Central Processing Unit) is the brain of your computer. It is designed for flexibility and low latency — it can switch rapidly between very different tasks: reading a file, making a network request, executing a database query, running your operating system. CPUs do this by having a small number of very powerful, sophisticated cores — each one capable of handling complex branching logic, speculative execution, out-of-order processing, and cache management.
A high-end server CPU like the AMD EPYC 9654 has 96 physical cores, each running at 3.7 GHz. Each core can execute up to 16 floating-point operations per clock cycle using AVX-512 SIMD instructions. Peak throughput: ~5.7 TFLOPS (trillion floating-point operations per second).
A GPU (Graphics Processing Unit) was originally designed to render video game graphics — computing the color of every pixel on screen simultaneously. Pixels are independent: the color of pixel (100, 200) doesn't depend on pixel (300, 400). So instead of a few powerful sequential cores, GPUs use thousands of simple parallel cores that all do the same operation on different data simultaneously.
For AI workloads, this is perfect: multiplying each row of a matrix by each column (which is what a neural network layer does) is exactly the kind of operation where thousands of independent parallel calculations help enormously. An NVIDIA H100 has 16,896 CUDA cores and 528 Tensor Cores (specialized matrix-multiply hardware), plus 80 GB of extremely fast HBM3 memory on the same chip package.
Every single layer in a transformer does the same fundamental operation: matrix multiplication. Given an input vector (representing the current token) and a weight matrix (the learned knowledge), multiply them together. For a single linear layer in Qwen2.5-7B, this means multiplying a [3584]-element vector by a [3584 × 3584]-element matrix — 3584 × 3584 = 12.8 million multiply-add operations, just for one layer of one token.
With 28 layers and ~8 linear projections per layer, a single forward pass through Qwen2.5-7B involves approximately 3.2 billion multiply-add operations per token. On a CPU, even with AVX-512 vectorization, this is sequential. On a GPU, thousands of these multiply-adds happen simultaneously. A transformer layer that takes 80–150 ms on a 64-core EPYC takes 0.3–0.5 ms on an A100 — a 240× difference.
VRAM (Video RAM) is the GPU's dedicated memory. Unlike system RAM which sits on your motherboard connected via a bus, VRAM is physically on the same package as the GPU chip — stacked directly on top of it using a technology called HBM (High Bandwidth Memory). This proximity is what allows HBM3 to deliver 3,350 GB/s of bandwidth.
GPU compute units can only directly process data that is in VRAM. If a model's weights are in system RAM, they must be transferred over the PCIe bus (32–128 GB/s) before the GPU can use them. This creates a bottleneck: at 128 GB/s PCIe, transferring 15.2 GB of weights takes ~120ms — for every forward pass. At HBM3 speeds, the same transfer is essentially instantaneous. This is why you cannot substitute system RAM for VRAM.
During inference, the model streams 15.2 GB of weights through the compute units for every token generated. If bandwidth is too low, the cores sit idle waiting for data — this is "memory-bandwidth bound." The GPU's HBM provides 67× more bandwidth than CPU DDR5.
Tensor Cores are specialized hardware units inside NVIDIA GPUs (introduced with the Volta V100). While regular CUDA cores do one multiply-add per clock cycle, a Tensor Core performs an entire 16×16 matrix-multiply-accumulate (MMA) operation in one cycle. For the math: 16×16×16 = 4,096 individual multiply-adds, executed simultaneously in a single instruction.
This is why the H100's "BF16 throughput with Tensor Cores" (989 TFLOPS) is so much higher than its "FP32 CUDA core throughput" (~60 TFLOPS). The Tensor Cores are 15× more powerful for matrix math — but they only activate when the data is in BF16 or FP16 format. This is the primary reason precision format choice matters so much for both performance and memory usage.
Every number stored in the model — every weight, every activation, every gradient — is stored in a specific numeric format. The format controls two things simultaneously: how many bytes it occupies (memory) and how fast the GPU can compute with it (throughput). Smaller formats save memory and can be faster, but they lose precision — and in the wrong place, that destroys training quality.
Modern LLM training uses a technique called Automatic Mixed Precision (AMP): the forward pass and gradient computation happen in BF16 (fast, memory-efficient), but the optimizer update happens in FP32 (slow, but numerically stable).
The reason: the Adam optimizer applies tiny adjustments to weights at every training step. These adjustments (learning_rate × gradient) are often on the order of 10⁻⁶. BF16's 7-bit mantissa only gives ~2–3 significant decimal digits of precision. A weight value of 1.2345678 receiving an update of 0.0000001 would be rounded to 1.2345678 unchanged in BF16 — the update is invisible. FP32's 23-bit mantissa preserves it. So we keep a "master copy" of all weights in FP32 for the optimizer step, then convert back to BF16 for the next forward pass. This "master weight copy" doubles the weight memory cost during training.
A model with 7.61B parameters is not just "15.2 GB." The moment you start training or even run inference with a long context, memory fills up from multiple sources simultaneously. Understanding each source is the key to diagnosing and preventing OOM errors.
GPU VRAM during LLM work is occupied by up to six distinct objects. Some are always present; others only appear during training. OOM errors happen when the sum of all active objects exceeds the GPU's capacity.
Select a workload to see exactly which memory tenants are active, how large each is, and why. The stacked bar shows relative proportions; the table explains each component.
| Component | Formula | Size | Explanation |
|---|
The KV Cache deserves special attention because it surprises most engineers the first time they encounter it at scale. The formula below uses Qwen2.5-7B's actual architecture numbers — not approximations:
| Sequence Length | Batch = 1 | Batch = 8 | Batch = 32 | Note |
|---|---|---|---|---|
| 512 tokens | 0.03 GB | 0.23 GB | 0.94 GB | Short chat — trivial |
| 2,048 tokens | 0.12 GB | 0.94 GB | 3.76 GB | Standard chat context — manageable |
| 8,192 tokens | 0.47 GB | 3.76 GB | 15.03 GB | Long document — batch starts to bite |
| 32,768 tokens | 1.88 GB | 15.03 GB | 60.13 GB | Long context serving — fills an 80 GB GPU |
| 131,072 tokens | 7.52 GB | 60.13 GB | 240 GB | Full context — requires multi-GPU serving |
The most common operational surprise: training runs stably for hours, then someone increases batch size from 4 to 8 — and it immediately crashes. This feels random but follows precise mathematical laws.
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True in your environment to use a more fragmentation-resistant allocator.
RuntimeError: CUDA out of memory. Tried to allocate 3.50 GiB (GPU 0; 79.20 GiB total capacity; 74.85 GiB already allocated; 2.94 GiB free; 76.34 GiB reserved in total by PyTorch) — the key numbers are: 74.85 GB actually in use, 76.34 GB reserved by PyTorch (includes fragmented free blocks it holds), only 2.94 GB is genuinely free, and a 3.5 GB contiguous block doesn't exist. The GPU is not actually "full" — it's fragmented.Drag the sliders to change batch size, sequence length, and training mode. Watch the memory bar grow, the GPU gauges fill, and the OOM error appear the moment you cross the limit. This is exactly what happens in real training runs.
PyTorch's caching allocator reserves memory blocks from CUDA and holds them in a pool for reuse — even after freeing a tensor, the memory stays "reserved" in the pool. When you need a new allocation, PyTorch first tries to find a free block in its pool. If the required size doesn't exist as a contiguous block (due to fragmentation), it requests more from CUDA. If CUDA has no more to give — even though the pool shows "free" memory — you get OOM. nvidia-smi shows PyTorch's reserved pool as "used," which makes it look like you're out when you're actually fragmented.
A rich ecosystem of techniques exists to reduce, redistribute, and avoid the memory costs we've seen. None of them make the GPU requirement disappear — they extend how far a given GPU can reach. Every technique trades memory for something: compute time, model quality, hardware complexity, or configuration effort.
| Technique | What it reduces | Cost / trade-off | Best used for |
|---|---|---|---|
| Gradient Checkpointing | Activation memory (~8×) | ~25–33% slower training (recompute) | Any long-sequence fine-tuning |
| LoRA | Gradients + optimizer states | Only adapts via low-rank; may not match full FT quality | Task fine-tuning on strong base models |
| QLoRA | Base model weight storage (15.2→5 GB) | Quantization + dequant overhead; quality check needed | 7B–34B adaptation on T4/A10G |
| FlashAttention | O(seq²) attention matrix | Hardware/kernel compatibility | Any sequence > 2K tokens |
| Unsloth | Kernel overhead, logit spike, HBM traffic | Model/kernel support coverage | Single-GPU LoRA/QLoRA fine-tuning |
| DeepSpeed ZeRO | Redundant optimizer/gradient/weight copies | Communication overhead, setup complexity | Full fine-tuning, multi-GPU |
| vLLM + PagedAttention | KV cache fragmentation waste | Serving-engine integration | High-throughput production inference |
| Gradient Accumulation | Peak activation memory | More forward/backward passes per optimizer step | Simulating large batches on small VRAM |
The problem: Standard attention computes a full [seq × seq] score matrix and writes it to HBM (slow, global GPU memory). At seq=32K in Qwen2.5-7B, this matrix across all layers would occupy ~241 GB — physically impossible on any single GPU.
FlashAttention's solution (Dao et al. 2022): Instead of computing the full matrix and storing it, tile the computation into blocks small enough to fit in GPU SRAM (the on-chip fast memory, 20–50 MB). Use an "online softmax" algorithm to combine tile results incrementally — mathematically equivalent to the full computation, but HBM reads are reduced from O(seq²) to O(seq²/SRAM_size).
What it saves and doesn't save: Eliminates the [seq × seq] matrix from HBM. Does NOT reduce activation memory from linear layers, the KV cache, or gradients. FlashAttention 3 (2024) further optimizes for H100's Tensor Memory Accelerator (TMA) hardware.
Why CPU can't do this: The algorithm requires GPU SRAM to be physically adjacent to compute units. CPU L1 cache (32–64 KB per core) is too small and structurally incompatible with the warp-level access patterns FlashAttention requires.
The problem: Backpropagation needs all intermediate activations from the forward pass. Storing all of them for 28 layers costs ~16.4 GB at batch=4, seq=2048 — often more than the weights themselves.
The solution: During the forward pass, only save activations at "checkpoint" boundaries (e.g., every 4 layers). Discard everything in between. During the backward pass, when a gradient is needed for a discarded layer, recompute that layer's forward pass from the nearest saved checkpoint on the fly.
The trade-off: You pay for ~30% of your forward computations twice (once during forward, once recomputed during backward). Training takes ~30% longer. But activation memory drops from ~16.4 GB to ~2 GB — an 8× reduction. For any fine-tuning at sequence length ≥ 2048, this is almost always worth it.
One line to enable: model.gradient_checkpointing_enable() in HuggingFace Transformers.
The insight (Hu et al. 2021): When fine-tuning a pretrained model on a new task, the required weight changes (ΔW) have low intrinsic rank — meaning they can be well-approximated by the product of two small matrices. Instead of updating the full W (e.g. [3584 × 3584] = 12.8M parameters), LoRA learns two small matrices A [3584 × r] and B [r × 3584] such that ΔW = A × B.
At rank r=16: A is [3584 × 16] = 57,344 params; B is [16 × 3584] = 57,344 params. Total per layer: 114,688 — just 0.9% of the original 12.8M. Applied across all Qwen2.5-7B linear layers: ~40M trainable parameters (0.53% of 7.61B).
Memory impact: Gradients shrink from 30.4 GB (full 7.61B params) to ~162 MB (40M adapter params). Adam optimizer states shrink from 60.9 GB to ~324 MB. Base model weights still occupy 15.2 GB in VRAM but receive no gradient updates.
Crucial caveat: Activation memory barely changes — the full forward pass still runs through all 28 frozen layers, generating the same intermediate tensors. Activations, not optimizer states, dominate LoRA memory. Gradient checkpointing is usually required alongside LoRA.
The approach (Dettmers et al. 2023): Takes LoRA further by storing the frozen base model in 4-bit NF4 (NormalFloat4) instead of BF16. NF4 is a specially designed 4-bit data type with 16 quantization levels placed at positions that minimize error for normally distributed weights (which neural network weights typically are).
How it works in practice: The NF4 weights are stored in 4-bit but dequantized to BF16 immediately before each matrix multiplication. Computation always happens in BF16 on Tensor Cores — you're paying for the storage savings, not sacrificing compute precision. The dequantized copy is discarded after use; only the 4-bit version is stored persistently.
Actual size: The raw math gives 7.61B × 0.5 bytes = 3.8 GB. But NF4 requires per-64-element FP16 scale factors, double-quantization metadata, and allocator alignment overhead. Practical loaded size: 4.5–6 GB. Combined with LoRA adapters (~500 MB for rank=16), Unsloth, and gradient checkpointing, QLoRA for Qwen2.5-7B fits on a T4 at batch=2, seq=2048.
What it is: Unsloth reimplements the most memory-intensive operations in the training loop using custom Triton kernels (NVIDIA's GPU programming language for high-performance custom operations) instead of relying on generic PyTorch and HuggingFace PEFT implementations that were not designed with peak memory in mind.
Chunked cross-entropy loss: Standard PyTorch materializes the full [batch × seq × 152,064] logits tensor before computing loss — a 1.25 GB spike at batch=2, seq=2048. Unsloth computes the loss on 2048-token chunks, never holding the full tensor. Saves 1–2.5 GB per step with no quality impact.
Fused QKV projection: Standard attention computes Q, K, and V in three separate passes, each reading the full input from HBM. Unsloth fuses all three into one kernel: one HBM read, three outputs. ~1.5–2× speedup for this step alone.
Smart gradient checkpointing: Unsloth profiles each layer type's recompute cost and keeps activations that are expensive to recompute (attention outputs), discarding cheaper ones (MLP outputs). Finds a better memory/speed balance than blanket full checkpointing.
The boundary: Unsloth removes overhead, but cannot eliminate the fundamental memory requirements. Still bounded by base model size, activation memory from sequence length and batch, and GPU VRAM capacity.
The problem: Standard data-parallel training (the simplest multi-GPU approach) replicates the full model state on every GPU. With 8× A100 GPUs, the optimizer states alone (60.9 GB) exist on all 8 cards: 487 GB of identical redundant data. Every GPU holds the exact same thing.
ZeRO-1: Partition only optimizer states. Each GPU holds 1/8 of optimizer states (7.6 GB instead of 60.9 GB). All-gather states before the optimizer step. Weights and gradients still replicated.
ZeRO-2: Also partition gradients. Each GPU reduces-scatters gradients during backward, keeping only its 1/8 shard. ~8× combined reduction on gradient+optimizer memory.
ZeRO-3: Also partition model parameters. Each GPU holds 1/N of the weights. Before computing a layer, it all-gathers the weights it needs, computes, then discards. Enables training models larger than any single GPU's VRAM. ~50% more communication overhead than ZeRO-2.
ZeRO-Offload / ZeRO-Infinity: Extends partitioning to CPU RAM (256 GB–2 TB available) or NVMe SSD. Optimizer states live in CPU RAM and are fetched via PCIe during the optimizer step. 40–80% throughput penalty — use only when the model must fit at all costs.
Each tab shows how memory distributes across 4 GPUs for a 9B-class model (18 GB weights, 24 GB optimizer, 18 GB gradients). Switch tabs to see exactly what gets partitioned at each ZeRO stage.
4 GPUs — standard data-parallel. Every GPU holds a complete, identical copy of everything.
ZeRO-1 — Partition optimizer states only. Weights and gradients remain fully replicated on every GPU.
ZeRO-2 — Partition optimizer states and gradients. Weights still replicated. Recommended default for LoRA fine-tuning.
ZeRO-3 — Partition everything including model weights. Every forward pass layer requires an all-gather from all GPUs.
| GPU | VRAM | BF16 Inference (run the 7B model in BF16) | 4-bit Inference (run 7B quantised to INT4/NF4) | QLoRA Fine-tune (adapt 7B, 4-bit base, LoRA adapters) | LoRA Fine-tune (adapt 7B, BF16 base, LoRA adapters) | Full Fine-tune (update all 7.61B params) |
|---|---|---|---|---|---|---|
| T4 | 16 GB | Too tight | Dev/small batch | Unsloth required | OOM | No |
| A10G / L4 | 24 GB | Small batch only | Comfortable | Comfortable | Careful settings | No |
| A100 40GB | 40 GB | Comfortable | High throughput | Large seq+batch | Comfortable | Not enough |
| A100 80GB | 80 GB | Production tier | Max concurrency | Any config | Long context | Needs ZeRO/FSDP |
| H100 80GB | 80 GB | Optimal | Optimal | Optimal | Optimal | With ZeRO |
| 2× A100 80GB | 160 GB | 70B inference | 70B inference | 34B LoRA | 34B LoRA | 7B with ZeRO-2 |
| 8× H100 | 640 GB | 70B+ models | 70B+ models | 70B fine-tune | 70B fine-tune | Full 70B training |
This is not a theoretical question. Teams sometimes attempt CPU training when GPU quota is unavailable, or assume a fast CPU server can substitute. The answer: the code runs correctly. The job finishes in 2027.
The fundamental issue is not that CPUs can't do the math — they can. The issue is how long it takes. LLM training is dominated by large matrix multiplications. Let's measure one: a 3584×3584 GEMM (General Matrix Multiply) — the dominant operation in each Qwen2.5-7B transformer layer.
The throughput gap has two independent causes, not one. Even if you somehow solved the TFLOPS problem on a CPU, you would immediately hit the bandwidth wall — and vice versa. These are separate, compounding constraints.
A 64-core CPU can do ~5.7 TFLOPS of FP32 math. An A100 does 312 TFLOPS in BF16 via Tensor Cores — a 55× gap. For a 3,584 × 3,584 matrix multiply: CPU takes 80–150 ms, A100 takes 0.3–0.5 ms. A transformer layer has 8 such GEMMs × 28 layers = 224 GEMMs per forward pass.
LLM inference is memory-bandwidth bound, not compute-bound. Even if CPUs had enough TFLOPS, they couldn't deliver weights to the compute units fast enough. At 50 tokens/second, inference needs 760 GB/s of sustained reads. CPU DDR5 delivers ~100 GB/s maximum — 7.6× too slow regardless of how many cores you have.
For matrix multiplication, parallelism across thousands of simultaneous multiply-adds is what matters — not sequential speed. All CPU cores share the same memory bus. Adding more cores increases compute pressure on the same fixed ~100 GB/s DDR5 bandwidth, making the bandwidth bottleneck worse, not better. A 1,000-core hypothetical CPU would still be memory-starved: 1,000 cores waiting for data from a 100 GB/s bus is slower per-core than 100 cores doing the same.
GPU HBM solves both problems simultaneously: each GPU die has its own dedicated HBM stack (3,350 GB/s on H100), physically adjacent to the Tensor Cores. The bandwidth scales with the GPU, not with a shared motherboard bus. This co-location is the fundamental architectural advantage that cannot be replicated by adding more CPUs.
Adding more CPU cores does not fix the bandwidth problem — it compounds it. All cores share the same memory bus. More cores means more contention for the same ~100 GB/s DDR5 bandwidth. Even with 1,000 CPU cores, you would be memory-starved: 1,000 cores waiting for data arriving at 100 GB/s is far slower per-core than 16,896 GPU cores fed by 3,350 GB/s HBM. The bandwidth is not shared on the GPU — it scales with the chip architecture.
Everything you need to make hardware decisions and speak precisely about GPU memory — without looking anything up.
An LLM does not fail because the code is careless — it fails because the GPU is asked to hold several large objects simultaneously: model weights activations gradients optimizer states KV cache. Batch size and sequence length are the two knobs that most reliably push a stable configuration over the edge. A T4 (16 GB) can handle 4-bit QLoRA fine-tuning with careful settings — it is useful for PoC and development, not production serving. The A100 or H100 at 80 GB is the practical enterprise tier for long-context serving, larger batches, BF16 inference, and full fine-tuning pipelines. GPU infrastructure is not an experiment expense — it is a product dependency with a concrete technical foundation.
| Scenario | VRAM | Minimum GPU |
|---|---|---|
| Inference BF16, 2K ctx, batch=1 | 17–19 GB | A10G |
| Inference BF16, 32K ctx, batch=1 | 19–24 GB | A10G |
| Inference BF16, 8K ctx, batch=32 | 35–43 GB | A100 80GB |
| 4-bit inference, 8K ctx, batch=1 | 7–11 GB | T4 (comfortable) |
| QLoRA r=16, batch=2, seq=2048 | 11–18 GB | T4 + Unsloth |
| LoRA r=16, batch=4, seq=2048 | 34–36 GB | A100 40GB |
| LoRA r=64, batch=4, seq=4096 | 70–80 GB | A100 80GB |
| Full FT, AdamW, batch=4, seq=2048 | ~160 GB | 2× A100 80GB |
| Workload | Minimum | Recommended |
|---|---|---|
| Dev / debugging / experiments | T4 16GB | RTX 4090 24GB |
| QLoRA fine-tune 7B | T4 + Unsloth | A10G / L4 |
| LoRA fine-tune 7B | A10G 24GB | A100 40GB |
| LoRA fine-tune 13–34B | A100 40GB | A100 80GB |
| Full fine-tune 7B | 2× A100 80GB | 4× A100 80GB |
| Production inference 7B (BF16) | A10G 24GB | A100 40GB |
| Long-context serving (32K+) | A100 80GB | H100 80GB |
| Production inference 70B (4-bit) | 2× A100 40GB | 2× A100 80GB |
| Pre-training from scratch (7B) | 8× A100 80GB | 16–64× H100 |
| Format | Bytes/param | Qwen2.5-7B |
|---|---|---|
| FP32 | 4 | 30.4 GB |
| BF16 | 2 | 15.2 GB |
| INT8 | 1 | 7.6 GB |
| NF4 (raw) | 0.5 | 3.8 GB raw / 4.5–6 GB loaded |
| Context | batch=1 | batch=8 | batch=32 |
|---|---|---|---|
| 2K | 0.12 GB | 0.94 GB | 3.76 GB |
| 8K | 0.47 GB | 3.76 GB | 15 GB |
| 32K | 1.88 GB | 15 GB | 60 GB |