SafeZone AI Learn
Learn/ Practitioner's Toolkit/ The Working Environment · lesson 2 of 5

GPUs, CUDA and memory arithmetic

The driver/toolkit/torch triangle that decides whether torch.cuda.is_available(), what GPUs are actually fast at (and when they aren't), mixed precision — and the VRAM arithmetic that predicts your OOM on a calculator before the run.

Deep learning is economically possible because one hardware family does dense linear algebra ~100× cheaper than CPUs — every matmul in the Deep Learning module’s lessons lands on it. But the GPU stack is also the toolkit’s most fragile tower, and its memory its hardest wall. This lesson gives you the three things practitioners actually need: a mental model of the software stack (so is_available() == False is a five-minute fix, not a day), a model of GPU performance (so you know when you’re leaving 10× on the table), and the memory arithmetic — which, unlike most of this module, is exact math you can do on a napkin and should do before every sizable run.

The stack: three versions that must agree

Bottom to top: the driver (kernel module; ships with the OS/GPU installer; talks to silicon), the CUDA runtime (libraries implementing the compute API), and PyTorch built against some CUDA version. The modern simplification most tutorials haven’t caught up with: torch wheels bundle their own CUDA runtime — you do NOT install the CUDA toolkit to use PyTorch. Only two version constraints remain: the driver must be at least as new as the bundled runtime needs (drivers are backward- compatible with older runtimes), and the wheel must match your platform/ Python. Hence the entire diagnosis tree for torch.cuda.is_available() == False: (1) nvidia-smi fails → no/broken driver — that’s the layer to fix, nothing Python-side will help; (2) nvidia-smi works but torch says no → you installed a CPU-only wheel (check torch.version.cudaNone is the tell) or a wheel whose CUDA needs a newer driver than nvidia-smi reports; (3) both fine but wrong GPU/none visible → CUDA_VISIBLE_DEVICES or a container missing --gpus. Ninety-odd percent of incidents are case (2): reinstall from the correct index URL, done.

import torch
print(torch.__version__, torch.version.cuda)   # None ⇒ CPU-only wheel — case (2)
print(torch.cuda.is_available(), torch.cuda.get_device_name(0))
print(torch.cuda.get_device_properties(0).total_memory / 2**30)  # the budget

What GPUs are fast at — and the two regimes

A GPU is thousands of simple cores plus very high-bandwidth memory, designed for throughput on regular parallel work. Two numbers govern everything: peak FLOP/s and memory bandwidth, and their ratio sets the arithmetic intensity (FLOPs per byte moved) a kernel needs to be compute-bound. Big matmuls (the DL module’s workloads) clear the bar — that’s the 100×. Elementwise ops, small batches, and short sequences do not: they are memory-bound, running at bandwidth speed no matter the TFLOPs on the spec sheet. Practical corollaries: batch small ops together (the vectorization lesson’s law, GPU edition), prefer a few big matmuls to many small ones, and know that inference of LLMs is memory-bandwidth- bound (the LLM track’s serving lesson builds on exactly this fact). Also budget honestly for transfers: PCIe is ~10–60 GB/s against ~1–3 TB/s on-device — a model that ping-pongs tensors to CPU each step can be slower than not using the GPU at all. Mixed precision (bf16 — the DL module’s training lesson has the fine print) roughly doubles both effective compute and effective memory, which is why it is on by default everywhere.

The memory arithmetic

VRAM during training is four ledgers, three of them exact:

Pbweightsweights+Pbgradsgradients+Pboptoptimizer states+(batch × seq × arch)activations\underbrace{P \cdot b_{\text{weights}}}_{\text{weights}} + \underbrace{P \cdot b_{\text{grads}}}_{\text{gradients}} + \underbrace{P \cdot b_{\text{opt}}}_{\text{optimizer states}} + \underbrace{\text{(batch × seq × arch)}}_{\text{activations}}

The canonical case — full fine-tuning with AdamW in mixed precision: bf16 weights (2 B) + bf16 grads (2 B) + fp32 Adam m and v (4+4 B) + fp32 master weights (4 B) = 16 bytes per parameter before a single activation: a 7B model wants ~112 GB just to exist in training. The two levers that make the impossible fit: quantize the frozen weights and shrink the trainable set (LoRA — the LLM track derives it; its memory win is visible below), and offload/shard what remains (ZeRO/FSDP, the DL module’s closing map). Inference is the cheap ledger: weights at chosen precision plus a KV-cache the LLM track prices exactly.

Set 7B, full AdamW fine-tune: the bar shows the infamous ~16 bytes/param — and the “fits on” line goes empty for single consumer cards, which is the right conclusion BEFORE launching the job. Now flip to LoRA: gradients and optimizer states collapse to the ~0.5% trainable slice and the same model fits a 24 GB card — that single bar-chart delta is 90% of why parameter-efficient tuning exists (the LLM track derives the other 10%). Flip to inference and walk precision down bf16 → int8 → int4: the weights bar halves twice — quantization as memory arithmetic. Then push batch×seq up and watch activations eat a training budget: sequence length is a QUADRATIC-ish memory actor once attention’s cache joins in, which is the bridge to the serving lesson. Every OOM you will ever hit is one of these four bars overflowing; this widget is the two-minute pre-run habit.

Operational habits that close the loop: measure, don’t guess — torch.cuda.max_memory_allocated() after a probe step is ground truth; fragmentation makes the usable budget ~10% less than the total; OOM first-response is (in order) halve the batch + gradient-accumulate (free, exact — DL module proved it), turn on activation checkpointing (~30% compute for a big activation cut), then shrink/shard the optimizer ledger (8-bit Adam, LoRA, FSDP). And del + torch.cuda.empty_cache() is for notebooks, not a strategy.

Exercises

Work these before the next lesson

  1. Walk the diagnosis tree on your own machine (or deliberately break it in a venv: install the CPU wheel, observe, fix by reinstalling from the CUDA index). Record which case you hit and the exact evidence line.
    Solution

    Worked solutions are part of Premiumunlock all of them for £5/month →

  2. 5 more exercises — each with a worked solution — are part of Premium. Unlock everything for £5/month →

References

  • PyTorch docs, “CUDA semantics” — the authoritative page on the memory allocator and streams.
  • NVIDIA, “GPU Performance Background User’s Guide” — the roofline/arithmetic-intensity model, from the source.
  • S. Rajbhandari et al., “ZeRO: Memory Optimizations Toward Training Trillion Parameter Models”, 2020 — the ledger model, formalized and sharded.
  • T. Dettmers et al., “8-bit Optimizers via Block-wise Quantization”, 2022.