25

GPUs, memory, and quantization

Why does GPU memory rule over everything else?

13 min

GPUs, memory, and quantization

With voice. You can pause and jump to any step.

Lesson complete

Subtitles

In the last lesson we counted operations. Today, a more basic question: a model with seventy billion parameters, where is it kept while it works?

Voice

No voice: the lesson advances on its own with subtitles.

The voice for this lesson is being recorded: for now it advances on its own with subtitles.

0:00 / 0:00

What you will achieve

  • Calculate how much memory a model takes from its parameters and the bytes per parameter (FP32, FP16, INT8, INT4), and decide which GPU it fits in.
  • Explain why training with Adam takes about sixteen bytes per parameter and why, when writing, the limit is usually memory bandwidth.
  • Recognize the three basic ways to split a model across several GPUs, and what quantization gains and risks, with real measurements.

Key ideas

  1. A model works out of the GPU’s memory, its VRAM. It takes parameters × bytes per parameter: 70 billion in FP16 is 140 GB, more than an H100’s 80.
  2. Each format stores a number with more or less detail: FP32 (4 bytes, about 7 digits), FP16 (2 bytes, about 3), INT8 (1 byte), and INT4 (½ byte), which round to the marks of a ruler.
  3. Training with Adam takes about 16 bytes per parameter: the parameter and its gradient in FP16, a copy in FP32, and Adam’s two averages. Eight times what using it takes, not counting activations.
  4. Fitting is not enough: when writing, every token reads all the parameters, and memory bandwidth usually sets the speed, not computation.
  5. If it does not fit, you split it (data, pipeline, tensor; Llama 3.1 405B used 8 × 16 × 128 = 16,384 H100s) or quantize it. In Pythia, 8 and 4 bits barely change the loss; 2 bits break it.

What many people believe

Common belief If a model fits in my GPU’s memory, I can also train it there.

What is true Using it takes about 2 bytes per parameter in FP16; training it with Adam, about 16, plus activations. A model that barely fits for use needs about eight times more memory to train. That is why training is split across many GPUs, or only a small part of the parameters is tuned (lesson 27).

Check yourself

Four questions. No grade, no account: they just show whether the idea stuck.

CalculationHow much memory do the parameters of a 13-billion model take in FP16?
ConceptTraining with Adam in mixed precision takes about 16 bytes per parameter. Where do they come from?
ConceptWhen writing an answer token by token, why do a GPU’s cores spend much of their time waiting?
ApplicationYour team has a 12 GB RTX 3060 and wants to use an open model with 8 billion parameters. What is the most sensible choice?

Under the hoodFor those who want the math and the code. You do not need it to follow the course.

Bytes per parameter and formats

The memory the weights take is a multiplication:

Mweights=N⋅b,M_{\text{weights}} = N \cdot b,

with NN parameters and bb bytes per parameter. For Llama 3.1 70B in FP16: 70×109⋅2=1.4×101170\times10^{9}\cdot 2 = 1.4\times10^{11} bytes =140=140 GB.

Decimal formats (the IEEE 754 standard) store a sign, an exponent (the range), and a mantissa (the digits):

format bytes sign · exponent · mantissa decimal digits largest value
FP32 4 1 · 8 · 23 log⁡10224≈7.2\log_{10}2^{24}\approx 7.2 ≈3.4×1038\approx 3.4\times10^{38}
FP16 2 1 · 5 · 10 log⁡10211≈3.3\log_{10}2^{11}\approx 3.3 65,504
BF16 2 1 · 8 · 7 log⁡1028≈2.4\log_{10}2^{8}\approx 2.4 same as FP32

BF16 (Kalamkar et al., 2019) gives up digits to keep FP32’s range; Llama 3 was trained in BF16. INT8 and INT4 have no exponent: they store an integer plus a shared scale (see below).

A detail about units: datasheets say “80 GB” of VRAM, but that is 80⋅23080\cdot 2^{30} bytes (GiB), 7% more than 80×10980\times10^{9}. In this lesson, 1 GB = 10910^{9} bytes, as with bandwidth; the difference changes no conclusion.

Training: sixteen bytes per parameter

With Adam in mixed precision (Micikevicius et al., 2018), each parameter needs the weight and gradient in FP16 and, in FP32, a copy of the weight and Adam’s two averages (Kingma and Ba, 2015). ZeRO (Rajbhandari et al., 2020, §3.1) sums it up:

Mtrain≈(2+2+4+4+4⏟K = 12) N=16N bytes.M_{\text{train}} \approx (2 + 2 + \underbrace{4 + 4 + 4}_{K\,=\,12})\,N = 16N \text{ bytes}.

The FP32 copy exists because each step’s update (gradient times learning rate) is often thousands of times smaller than the weight: added in FP16, with its 10 mantissa bits, it would be lost to rounding. We measured it on an RTX 3060 (measure-memory.py, the Pythia-160M architecture with random weights): 16.19 bytes per parameter in full FP32 and 16.05 in mixed precision, after AdamW’s first step.

On top of that come the activations, the intermediate results backpropagation needs. Per layer of a 16-bit transformer with linear-memory attention they are about 34 s b h34\,s\,b\,h bytes (ss = tokens per sequence, bb = sequences per batch, hh = width; Korthikanti et al., 2022). On the same RTX 3060 we measured 0.949 times that formula. They can be reduced by recomputing them on the way back, at the cost of more compute.

ZeRO and its PyTorch version, FSDP (the one Meta used), split those 16 bytes across the data copies, so each GPU stores only part of the states.

Bandwidth: the ceiling when writing

When generating one token at a time, every step reads all the weights:

tokens per second≤bandwidthN⋅b.\text{tokens per second} \le \frac{\text{bandwidth}}{N\cdot b}.

An H100 SXM moves 3,350 GB/s; an 8-billion model in FP16 (16 GB) stays under about 209 tokens per second, however fast it computes. Its dense FP16 compute peak is about 989 trillion operations per second (the datasheet says 1,979 “with sparsity”): 989×1012/3.35×1012≈295989\times10^{12}/3.35\times10^{12}\approx 295 operations per byte, about 590 per 2-byte parameter, which a matrix–vector product uses for only 2 (multiply and add).

Measured on an RTX 3060 (measure-memory.py): copying 1 GiB, 333 GB/s; multiplying a 16384×1638416384\times16384 FP16 matrix by one vector, 1.57 ms, 341 GB/s, and just 0.34 trillion operations per second; by 4,096 vectors at once, 27.7 trillion: 81 times more on the same chip, because each weight read is reused. Batching requests is the way out, which we will see in lesson 26.

Parallelism

The number of GPUs is the product of the three splits: G=T⋅P⋅DG = T\cdot P\cdot D (tensor, pipeline, data).

  • Data: each copy sees another slice of the batch; at the end of the step the gradients are averaged (a sum across all of them, an all-reduce).
  • Pipeline (GPipe, Huang et al., 2019): KK stages of layers; the batch is cut into MM micro-batches so the stages work at the same time. The idle fraction (the “bubble”) is on the order of K−1M+K−1\frac{K-1}{M+K-1}; GPipe found it negligible with M≥4KM \ge 4K.
  • Tensor (Megatron-LM, Shoeybi et al., 2019): in each block’s network, the first table is split by columns and the second by rows, so one sum across GPUs per block in each direction is enough. Since it happens at every layer, it stays inside the server, where the GPUs are joined by NVLink (900 GB/s on the H100).

Llama 3.1 405B (Llama Team, 2024, table 4): 8 tensor × 16 pipeline × 128 data = 16,384 H100s, using 41% of their compute; the long-context stage added a fourth split, by context (8 × 16 context × 16 × 8). And to use it: in BF16 it does not fit in one 8-H100 server (§6.1), which is why Meta serves it split across two servers or quantized to FP8.

Group quantization

The lesson’s rule (symmetric round-to-nearest, no calibration), for a group gg of 32 weights and bb bits:

qmax⁡=2b−1−1,sg=max⁡i∈g∣wi∣qmax⁡,qi=clip⁡ ⁣(round⁡wisg, −qmax⁡, qmax⁡),w^i=qi sg.q_{\max} = 2^{b-1}-1,\qquad s_g = \frac{\max_{i\in g}|w_i|}{q_{\max}},\qquad q_i = \operatorname{clip}\!\Big(\operatorname{round}\frac{w_i}{s_g},\,-q_{\max},\,q_{\max}\Big),\qquad \hat w_i = q_i\, s_g.

With 8 bits there are 255 marks (from −127 to 127); with 4, 15. Each group also stores its scale in FP16: 16/32=0.516/32 = 0.5 extra bits per weight, so “INT4” is really 4.5 bits (the 70-billion model takes about 39 GB, not 35).

Measured on Pythia-1.4B (build-data.py: the 96 matrices of its 24 layers, 1.208 billion weights; loss in nats per token over 287,744 tokens of WikiText-2):

bits FP16 8 6 5 4 3 2
one scale per group of 32 2.5805 2.5806 2.5858 2.5975 2.6565 3.1589 11.045
one scale per matrix 2.5805 2.6002 3.2172 5.3017 10.692 10.445 21.347

With a single scale per matrix, the model already breaks at 4 bits: a few outlier weights, up to 13 times the standard deviation in the median matrix, stretch the ruler, and in that matrix two out of three weights land on the zero mark at 4 bits. That is why small groups are used. Production methods go further: LLM.int8() (Dettmers et al., 2022) separates the dimensions with outliers and keeps them in 16 bits; GPTQ (Frantar et al., 2023) corrects the rounding with second-order information and brings 175-billion models to 3 or 4 bits with negligible accuracy loss. Caveat: Pythia saw Wikipedia during training, so the absolute level of the loss is optimistic; what is compared is the difference, measured on the same text.

In code

import numpy as np

def gb(params, bytes_per_param):
    return params * bytes_per_param / 1e9

for fmt, b in [("FP32", 4), ("FP16", 2), ("INT8", 1), ("INT4", 0.5)]:
    print(f"70B in {fmt}: {gb(70e9, b):5.0f} GB")
print(f"training with Adam (16 B): {gb(70e9, 16):.0f} GB")
print(f"writing ceiling, 8B in FP16 on an H100: {3350 / gb(8e9, 2):.0f} tokens/s")

# quantize a group of 32 weights: a ruler with 2^(b-1) - 1 marks per side
rng = np.random.default_rng(0)
w = rng.normal(0, 0.02, 32)
for bits in (8, 4, 2):
    qmax = 2 ** (bits - 1) - 1
    s = np.abs(w).max() / qmax                     # the group's scale
    q = np.clip(np.round(w / s), -qmax, qmax)      # the integers that are stored
    err = np.linalg.norm(w - q * s) / np.linalg.norm(w)
    print(f"{bits} bits: relative error {err:.3f}, weights at zero {np.mean(q == 0):.0%}")

Output:

70B in FP32:   280 GB
70B in FP16:   140 GB
70B in INT8:    70 GB
70B in INT4:    35 GB
training with Adam (16 B): 1120 GB
writing ceiling, 8B in FP16 on an H100: 209 tokens/s
8 bits: relative error 0.006, weights at zero 0%
4 bits: relative error 0.100, weights at zero 19%
2 bits: relative error 0.792, weights at zero 84%

What comes next

When the model is used, another memory appears, the attention cache, which grows with every token of the conversation, along with the way to make the most of bandwidth: serving many people at once. That is lesson 26, inference.

Decision lens

What this idea means if your job is to decide, not to code.

  • To size hardware: memory ≈ parameters × bytes per parameter, plus a margin for computation (the cache in lesson 26). An 8-billion model in INT8 fits on a 12 GB card; a 70-billion model in FP16 needs at least two H100s.
  • Ask what format a model is served in: the same name can be offered in FP16 or quantized to 4 bits, with different costs, speed, and quality.
  • If someone proposes training or fully fine-tuning a model, count about 16 bytes per parameter before buying: training takes about eight times the memory of using it.
  • A quantized version can pass standard benchmarks and still fail on rare cases (Meta saw this with Llama 3). Evaluate it on your own data before switching.

Terms in this lesson

Full transcript
CommentsThis lesson’s public conversation: questions, facts to check, ideas.

Comments live in GitHub Discussions, in the course’s public repository; writing one requires a GitHub account. They load from giscus.app when you open this section. The course reviewers read them, and if a change is accepted, it is applied in both languages. Open the thread on GitHub

Sources