Thousands of threads — provided you feed them
The batching module ended on “feed the hardware”. This is the hardware that starves the fastest.
▶ Notebook (demo + solution): Memory & GPU — the GPU sections
⬅ Previous: 4 — Memory & batching · Next: Course home ➡
Both face the same wall: memory is far away. They bet in opposite directions.
| CPU | GPU | |
|---|---|---|
| threads | few, fast | thousands, slow |
| caches | big (24 MiB L3), automatic | small per thread, + explicit shared memory |
| hides latency by | caching the data | switching to another warp |
| wants | one big job | an ocean of identical jobs |
The CPU avoids the wait. The GPU covers it — with so many threads that someone is always ready to run. Give it too little work and there is nobody to switch to: the latency shows.
The dispatch module’s promise, cashed in:
* is a generic function. Its arguments are now CuArray, so multiple dispatch picks the GPU method.Note
This is the pay-off promised back in the dispatch module: the same * picks the GPU version all by itself, because of the types of its arguments.
We multiply two N × N matrices, CPU vs GPU.
N runs from 64 to 4096.
Does the GPU always win?
Square matmul, Float32, CPU measured cold — then the GPU:
| N | CPU | GPU | speedup | GPU GFLOP/s |
|---|---|---|---|---|
| 64 | 0.01 ms | 0.03 ms | 0.25× — it loses | 18 |
| 128 | 0.03 ms | 0.03 ms | 1.2× — break-even | 153 |
| 512 | 1.15 ms | 0.09 ms | 13.0× | 3035 |
| 2048 | 69.8 ms | 3.02 ms | 23.1× | 5689 |
| 4096 | 557 ms | 38.6 ms | 14.4× | 3558 |
A GPU is not “faster”. It is wider. Narrow work doesn’t get faster on a wider machine.
One layer W (4096²), applied to B examples at once:
| B | GPU total | µs / example | GFLOP/s | % of the card |
|---|---|---|---|---|
| 1 | 0.41 ms | 405.0 | 82.8 | 1.7% |
| 8 | 0.42 ms | 52.6 | 638 | 13% |
| 64 | 0.45 ms | 7.05 | 4761 | 100% |
| 256 | 2.23 ms | 8.71 | 3853 | 81% |
0.41 → 0.45 ms while B goes 1 → 64.57× better per example — and nothing about the GPU changed. Only how much we asked of it.
32 threads move in lockstep (a warp). They issue their loads together.
It is the column-major lesson again, one bus further out: locality decides on both sides.
Same 20 M reads, only the access pattern changes:
| access | time | |
|---|---|---|
contiguous (stride 1) |
2.39 ms | coalesced |
stride 33 |
6.53 ms | 2.7× slower |
stride 1023 |
12.5 ms | 5.2× slower |
out = a[idx] also reads the index array (80 MB) and writes the result (80 MB) — both perfectly coalesced whatever the stride.Two thirds of the traffic is coalesced by construction. Only a third can be punished — so the ratio is capped, no matter how bad the gather gets. Amdahl’s law, on memory traffic.
That argument makes a prediction. Remove the 80 MB index read — compute the index inside the kernel — and only half the traffic stays coalesced, so the ratio must rise.
stride 33 |
ratio |
|---|---|
gather a[idx] (2/3 coalesced) |
2.74× |
| kernel, index computed (1/2 coalesced) | 4.18× |
The prediction held. That is the difference between an explanation and an excuse — a story you can test and might have lost.
Monte-Carlo π, 100 million points:
| time | ||
|---|---|---|
| CPU, scalar loop | 274.7 ms | |
| GPU | 11.5 ms | 24× |
Remember the race condition? There is no shared counter here either —
sumreduces. Minimise sharing scales from 22 threads to 100 million.
* onto the GPU.Not one of these was a language trick. Every single one was what the machine actually does: cost per operation, data movement, sharing.
⬅ Previous: 4 — Memory & batching · Next: Course home ➡