Index Ai Academy
Phase 4 · Deep learning · Tooling

GPUs and compute

Why deep learning runs on graphics cards.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Sooner or later, every talk about AI turns into a talk about compute. Compute means the chips, the memory and the hours a model needs. It explains why a model needs a certain graphics card. It explains why training runs cost millions, and why shrunken models run on laptops. It also explains why the price of using AI keeps falling. A few rough formulas let you check a vendor's claims and size a training job. They show why one feature costs more than another. You don't need to be an infrastructure engineer to use them.

What you'll be able to do

  • Explain why matrix multiplication dominates deep learning and why GPUs suit it
  • Count the floating-point operations of a layer and of a whole training run
  • Estimate the memory a model needs to run and to train, by numeric precision
  • Turn compute estimates into GPU time and cost, the way AI budgets are planned

What it covers

  1. Many simple cooks beat a few master chefs
  2. It's (almost) all matrix multiplication
  3. Feel the cost grow
  4. Memory to run a model
  5. Memory to train a model: about 16 bytes per parameter
  6. Compute: the 6ND rule
  7. Worked example: from FLOPs to hours and dollars
  8. Real training runs
  9. Where the course's code runs

Many simple cooks beat a few master chefs

Picture two kitchens. The first has four master chefs. Each one can cook any dish, fast and well. The second has thousands of line cooks. Each of them can only do one simple job, like chopping. Now the order comes in: 100,000 carrots, all chopped the same way. The second kitchen wins easily.

A computer's main chip, the CPU, is the first kitchen. It has a few strong, flexible cores. A core is one worker that runs instructions. A GPU (graphics processing unit) is the second kitchen. It has thousands of simple cores. They all do the same step, each on different numbers. GPUs were built to color millions of screen pixels at once. Deep learning turned out to need the same kind of work.

Quick check
A GPU has thousands of simple cores and a CPU has a few strong ones. Which job suits the GPU?
  1. One long chain of steps, where each step depends on the one before
  2. The same simple step repeated on millions of numbers
  3. Starting the programs on your laptop
Show the answer

The same simple step repeated on millions of numbers

GPU cores all do the same step at once, each on different numbers, like line cooks chopping 100,000 carrots. Work that must happen one step after another suits the CPU's few strong cores.

It's (almost) all matrix multiplication

You met matrix multiplication in Phase 2. It's most of the work in a neural network. A linear layer applied to a whole batch of examples is one matrix multiply. So are the main steps of attention, the heart of the transformers you'll meet in Phase 5.

How much work is one multiply? Each number in the answer is a dot product: multiply pairs, then add them up. Every single multiply or add counts as one FLOP. That's short for floating-point operation. ("Floating point" is how computers store numbers with a decimal point.)

Call the number of rows in the first matrix mm. Call its number of columns kk. The second matrix must then have kk rows. Call its number of columns nn. The answer has mm rows and nn columns.

Each answer number is a dot product of kk pairs. That's kk multiplies and about as many adds. So the total work is:

FLOPs≈2 m n k\text{FLOPs} \approx 2\, m\, n\, k

The wavy equals sign, ≈\approx, means "roughly equal to".

In plain words: count the numbers in the answer, which is mm times nn. Each one costs about kk multiplies and kk adds. Multiply it all together.

Here's a small case. Multiply a 2 × 3 matrix by a 3 × 2 matrix. So m=2m = 2, k=3k = 3 and n=2n = 2. The answer is 2 × 2, which is 4 numbers. Each one takes 3 multiplies and about 3 adds. That's about 6 FLOPs each, and 4 × 6 = 24 FLOPs in all. The formula agrees: 2 × 2 × 2 × 3 = 24.

m, k, n = 2, 3, 2         # a (2 × 3) matrix times a (3 × 2) matrix
print(2 * m * n * k)      # about 24 FLOPs
It runs right here in your browser, with nothing to install.
Go deeper Why "about"?

Adding up kk products takes only k−1k - 1 adds. In the small case, 3 products need 2 adds. So the exact count is 4 × 5 = 20 FLOPs, not 24. In real layers, kk is in the thousands. One add more or less makes no real difference. So everyone uses 2mnk2mnk, because it's easy to remember.

Why GPUs fit this work

Here's the key fact. Each answer number is its own dot product. None of them waits for another's result. So the work splits perfectly across many cores, like carrots across many cooks. That's exactly what GPUs are built for.

Modern GPUs add two more things. Tensor cores are special units that multiply a small block of a matrix in one step. And very fast memory keeps all those cores fed with numbers.

Quick check
A layer multiplies a 4 × 5 matrix by a 5 × 10 matrix. What does this print?
m, k, n = 4, 5, 10
print(2 * m * n * k)
  1. 40
  2. 200
  3. 400
  4. 4,000
Show the answer

400

The answer is 4 × 10 = 40 numbers. Each is a dot product of 5 pairs, so about 5 multiplies and 5 adds. 40 × 10 = 400 FLOPs. 200 forgets to count the adds.

Feel the cost grow

The code below times three matrix multiplies. Each uses square matrices, so all three sizes are the same: m=k=nm = k = n. The work is then 2n32n^3 FLOPs. Here n3n^3 means nn times nn times nn.

The code reports speed in GFLOP/s, which means billions of FLOPs per second. Your timings will differ from anyone else's.

import time
import numpy as np

for n in [128, 256, 512]:
    A, B = np.random.rand(n, n).astype(np.float32), np.random.rand(n, n).astype(np.float32)
    start = time.perf_counter()
    for _ in range(5):
        A @ B
    seconds = (time.perf_counter() - start) / 5
    flops = 2 * n ** 3
    print(f"{n} × {n}: {flops / 1e6:7.1f} million FLOPs in {seconds * 1000:6.1f} ms  →  {flops / seconds / 1e9:5.1f} GFLOP/s")

Doubling nn multiplies the work by 8, because 2 × 2 × 2 = 8. This browser tab manages a few GFLOP/s at best.

A modern data-center GPU does about 101510^{15} FLOPs per second in low precision (explained next). That's a 1 followed by 15 zeros, called a petaFLOP. It's hundreds of thousands of times faster than this tab.

Precision: how many bytes per number

Every number in a model takes up memory. A byte is the basic unit of computer memory. One byte is 8 bits, and a bit is a single 0 or 1.

More bytes per number give more exact numbers. It's like keeping more decimal places on a price. Fewer bytes give rougher numbers that take less room. That choice is called precision.

FormatBytes per numberTypical use
float324classic training, this course's examples
bfloat16 / float162modern training and serving (bf16 keeps float32's range)
float8 / int81fast inference, some training
4-bit formats0.5quantized models on laptops and phones

The number in each name is its size in bits. So float32 uses 32 bits, which is 4 bytes. "Inference" means running a trained model to get answers.

Halving the bytes halves the memory. On hardware that supports the smaller format, it also roughly doubles the speed. The loss in quality is usually small. The quantization lesson in Phase 6 covers the trade-offs.

Memory to run a model

To run a model, its weights must sit in the GPU's memory. The weights are also called the model's parameters: the numbers it learned in training. The rule is one multiplication:

memory = parameters × bytes per parameter

Take a model with 7 billion parameters, called "7B" for short. In bfloat16 that's 7 billion × 2 bytes, or 14 billion bytes. A gigabyte (GB) is a billion bytes, so that's 14 GB. At 4 bits, which is half a byte, it's 7 billion × 0.5 = 3.5 GB. That fits on a laptop.

params = 7e9                          # 7 billion; 7e9 means 7 × 10**9
print(params * 2 / 1e9, "GB in bfloat16")
print(params * 0.5 / 1e9, "GB at 4 bits")

It prints 14.0 and 3.5. Dividing by 1e9 turns bytes into gigabytes.

You need some extra room on top. The model stores activations, the in-between results of each layer. When it writes text, it also keeps an attention cache. That cache holds notes about the earlier words, so the model doesn't redo that work.

Quick check
Roughly how much memory do the weights of a 13-billion-parameter model take in bfloat16?
  1. About 13 GB
  2. About 26 GB
  3. About 6.5 GB
  4. About 52 GB
Show the answer

About 26 GB

bfloat16 uses 2 bytes per number. 13 billion × 2 bytes = 26 GB, before activations and caches.

Memory to train a model: about 16 bytes per parameter

Training needs far more memory than running. The usual setup has two parts. Mixed precision keeps some numbers in 2 bytes and some in 4. Adam is the popular update rule from the optimizers lesson. With both, the standard estimate is about 16 bytes per parameter:

  • 2 bytes: the weights in bfloat16, used for the forward and backward passes.
  • 2 bytes: the gradients in bfloat16. A gradient says which way to nudge each weight.
  • 4 bytes: a float32 "master copy" of the weights. Tiny updates are added here, so rounding doesn't wipe them out.
  • 8 bytes: Adam's two running averages for each weight, 4 bytes each in float32.

Activations come on top of all this.

In plain words: to train, multiply the number of parameters by 16. That gives the bytes for weights, gradients and Adam's notes, before activations.

So a 7B model needs about 7 billion × 16 = 112 billion bytes. That's 112 GB. It's more than a typical 80 GB data-center GPU holds, and activations push it higher still.

print(2 + 2 + 4 + 8, "bytes per parameter")
print(7e9 * 16 / 1e9, "GB to train a 7B model")

It prints 16 and 112.0. That's why big training runs are split across many GPUs. It's also why parameter-efficient fine-tuning is popular. One method, LoRA (Phase 6), trains a small add-on instead of every weight.

Quick check
You want to fully train a 3-billion-parameter model with mixed precision and Adam. What does this print, in GB, before activations?
print(3e9 * 16 / 1e9)
  1. 1.5
  2. 6.0
  3. 12.0
  4. 48.0
Show the answer

48.0

Training takes about 16 bytes per parameter: 3 billion × 16 = 48 billion bytes, which prints as 48.0 GB. 6.0 is only enough to run it in bfloat16, at 2 bytes per parameter.

Compute: the 6ND rule

Memory tells you which GPU you need. Compute tells you how long it will run. For a transformer, a short formula gives the total work of training.

It uses two symbols. NN is the number of parameters in the model. DD is the number of training tokens. A token is a small chunk of text, often a whole word or part of one.

C≈6 N DC \approx 6\, N\, D

Here CC is the total compute, counted in FLOPs.

In plain words: each training token costs about 6 FLOPs for every parameter. About 2 are for the forward pass, which makes a prediction. About 4 are for the backward pass, which works out the gradients.

Here's a toy case. A model with 1,000 parameters trains on 500 tokens. That's 6 × 1,000 × 500 = 3,000,000 FLOPs. It sounds like a lot, but a laptop does it in a tiny fraction of a second.

N = 1000          # parameters
D = 500           # training tokens
print(6 * N * D)  # training FLOPs

Running a trained model costs far less. It's about 2N2N FLOPs per generated token, because it needs only the forward pass.

Go deeper Why the backward pass costs twice the forward pass

In the forward pass, each weight is used in about one multiply and one add per token. That's the

  1. The backward pass does two jobs of about the same size. It works out the gradient for each

weight, so the optimizer can update it. It also works out the gradient for each layer's inputs, so the chain rule can carry on to the layer before. Two jobs, each about as costly as the forward pass, give 4. In total, 2 + 4 = 6.

This is a rule of thumb. It leaves out some work, such as attention's extra cost on long texts.

Try it now

The rules of thumb from this lesson, live. Time scales with parameters times tokens, so doubling both takes four times as long.

Worked example

Worked example: from FLOPs to hours and dollars

Turning FLOPs into time takes one more idea. A GPU's advertised speed is its peak: the most it can do in perfect conditions. Real training runs rarely use more than 40–50% of the peak. The rest goes on moving data and on GPUs waiting for each other. The share you actually get is called utilization.

Here's the recipe:

  1. Work out the FLOPs with 6ND6ND.
  2. Divide by the GPU's peak FLOP/s times the utilization. That gives seconds.
  3. Divide by 3,600 to get hours. Multiply by the price per GPU-hour.

Try it on a small model: 100 million parameters and 3 billion tokens. The GPU's peak is 101510^{15} FLOP/s, and you get 50% of it. Renting it costs $3 per GPU-hour.

params = 100e6                    # N: 100 million parameters
tokens = 3e9                      # D: 3 billion tokens
flops = 6 * params * tokens
seconds = flops / (1e15 * 0.5)    # peak FLOP/s × utilization
hours = seconds / 3600
print(f"{flops:.1e} FLOPs, {seconds:,.0f} seconds, {hours:.1f} GPU-hours")
print(f"cost: about {hours * 3:.0f} dollars at 3 dollars per GPU-hour")
In the lesson you can change this and run it.

Step 1: 6 × 100 million × 3 billion = 1.8×10181.8 \times 10^{18} FLOPs. The e+18 in the output means "times 10 to the 18th".

Step 2: half of 101510^{15} is 5×10145 \times 10^{14} FLOPs per second. Dividing gives 3,600 seconds.

Step 3: 3,600 seconds is exactly one hour. One GPU-hour at $3 costs $3.

Double the parameters or double the tokens, and the cost doubles too. Tokens matter as much as model size.

Real training runs

Now the same recipe for three real-sized jobs. The code uses the peak speeds of two common NVIDIA data-center GPUs: the A100 and the newer H100. It assumes 40% utilization.

def training_flops(params, tokens):
    return 6 * params * tokens

def gpu_hours(flops, flops_per_second, utilization=0.4):
    return flops / (flops_per_second * utilization) / 3600

A100_BF16 = 312e12        # NVIDIA A100 peak dense bfloat16 FLOP/s
H100_BF16 = 989e12        # NVIDIA H100 (SXM) peak dense bfloat16 FLOP/s

runs = {"GPT-3 (175B params, 300B tokens)": (175e9, 300e9),
        "a 7B model on 2T tokens": (7e9, 2e12),
        "fine-tuning 7B on 50M tokens": (7e9, 50e6)}
for name, (n, d) in runs.items():
    c = training_flops(n, d)
    print(f"{name:<34} {c:.1e} FLOPs = {gpu_hours(c, A100_BF16):>9,.0f} A100-hours = {gpu_hours(c, H100_BF16):>9,.0f} H100-hours")

Read the first line. GPT-3 has 175 billion parameters and trained on 300 billion tokens. The rule gives about 3×10233 \times 10^{23} FLOPs. That matches what the GPT-3 paper reports.

Multiply the GPU-hours by a cloud price per GPU-hour, and you have a budget. The price is a few dollars at the time of writing, but check current rates. At that price, even a GPT-3-sized run costs on the order of a million dollars of compute. Today's frontier models cost far more.

The last line is the hopeful one. Fine-tuning a 7B model on 50 million tokens takes only about 1 to 5 GPU-hours.

Quick check
A GPU's peak speed is 1,000 trillion FLOP/s. Which speed is the sensible one to plan a training budget with?
  1. About 10 trillion FLOP/s
  2. The full 1,000 trillion FLOP/s
  3. 2,000 trillion FLOP/s, because training runs forward and backward passes
  4. About 400 to 500 trillion FLOP/s
Show the answer

About 400 to 500 trillion FLOP/s

Real training runs rarely use more than 40–50% of a GPU's peak. Planning with the full peak makes the job look two to two and a half times as fast and cheap as it will be.

Where the course's code runs

On your own machine, torch.cuda.is_available() tells you whether PyTorch can see an NVIDIA GPU. On a Mac with Apple silicon, use torch.backends.mps.is_available() instead.

Tiny models like the ones in this phase train fine on a CPU. Anything the size of a transformer wants a GPU. Cloud notebooks such as Colab and Kaggle give you one for free, in small amounts.

In this browser, everything runs on the CPU. It uses the course's own small version of PyTorch, built on NumPy. That's why the models here stay tiny.

Practice what you just read

  • 7 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

Why are GPUs so much faster than CPUs for neural networks?

  1. Neural networks are mostly matrix multiplications, which split into many identical, independent operations that thousands of GPU cores run in parallel
  2. GPUs have much faster single cores than CPUs, and neural networks spend nearly all of their time waiting on one long sequence of calculations that has to run in order
  3. GPUs store far more data on disk than CPUs can reach, and neural networks spend nearly all of their time loading the examples and weights that training needs next
  4. Neural networks need graphics output, because activations are stored as pixels on the screen, and a GPU is the only part of a computer that can read them back again

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson