Batching turns memory-bound into compute-bound
Today’s enemy: the distance to your data.
▶ Script:
4_memory_and_gpu.jl(§1–3) · Notebook: Memory & GPU · 🐍 numpy (Colab)
| Level | Size | Latency | If L1 = 1 s |
|---|---|---|---|
| registers | ~100s B | ~0 | instant |
| L1 | 32–64 KB / core | ~1 ns | 1 s |
| L2 | 256 KB–1 MB | ~4 ns | 4 s |
| L3 | 8–32 MB | ~15 ns | 15 s |
| RAM | 16–64 GB | ~100 ns | ~2 min |
Fast code honors that bet.
Sum the same array many times, from 8 KB to 64 MB.
L1 ≈ 1 ns, RAM ≈ 100 ns.
How much slower per access at 64 MB?
(~100×? Write it down.)
▶ Script: cell “1. The memory hierarchy — the cache cliff”
| footprint | ns / access | lives in |
|---|---|---|
| 8 KB | 0.049 | L1 |
| 4 MB | 0.128 | L2 (18 MiB) |
| 64 MB | 0.317 | RAM (> 24 MiB L3) |
Steps visible. But 6.5×, not ~100×. Why?
100× is real when the next address is unpredictable (linked list, random indices).
Sum an 8000×8000 matrix, two loop orders:
M[i,j], M[i+1,j] adjacent).▶ Script: cell “2. Locality — the same sum, walked two ways”. Predict first.
8000² Float64 = 512 MB ≫ cache:
| walk | time | each 64 B line… |
|---|---|---|
| columns | 30.4 ms | fully used (8 values) |
| rows | 361.4 ms | one value, then evicted |
Wrong order: right result, silently slow.
If Julia just “preferred columns”, the gap would show at any size.
Shrink the matrix. When does the gap vanish?
▶ Script: cell “Proof it IS the cache”
| size | footprint | rows / columns |
|---|---|---|
| 100² | 78 KB — fits | 0.9× |
| 4000² | 122 MB — spills | 7.2× |
Not “columns are fast”: the cache decides.
C-order vs F-order arrays, 8000²:
| numpy | ratio | why |
|---|---|---|
A.sum(axis=0) |
≈ 1× | numpy reorders |
| copy F→C / C→C | 13.2× (109.5 / 8.3 ms) | can’t reorder a copy |
numpy hides it; Julia shows it. Neither removes it.
▶ Script: cell “Allocating is memory traffic too”
| n = 10⁷ | time | allocated |
|---|---|---|
z = … |
54 ms | 76 MiB |
z .= … |
17 ms | 0 bytes |
One dot: ~3×. But measure: the gain isn’t always there.
| n×n, Float64 | FLOP | bytes | intensity |
|---|---|---|---|
| mat × vec | 2n² | 8n² | 0.25 — constant |
| mat × mat | 2n³ | 8n² | n/4 — grows |
Compare GFLOP/s, not seconds.
▶ Script: cell “3. Arithmetic intensity”
W * x: each weight used once.X: W * X reuses each weight B times.Not a GPU trick: it raises intensity. Must pay on the CPU.
One layer W (4096² Float32), B examples as columns.
What happens to the time per example?
▶ Script: cell “Batching: one layer W, B examples”
| B | total | µs / example | GFLOP/s |
|---|---|---|---|
| 1 | 2.55 ms | 2551.8 | 13.1 |
| 8 | 5.65 ms | 706.0 | 47.5 |
| 64 | 11.06 ms | 172.9 | 194.1 |
| 256 | 39.06 ms | 152.6 | 219.9 |
| 1024 | 141.80 ms | 138.5 | 242.3 |
18.4× per example, 13.1 → 242.3 GFLOP/s. No GPU.
2551.8 → 172.9 (B=64) → 152.6 (B=256) → 138.5 µs: flattens.
W = 64 MiB > 24 MiB L3: a fixed RAM stream per batch.\[\text{time per example} \approx \text{compute} + \frac{\text{stream } W}{B}\]
Flat = success: now compute-bound.
scripts/4_memory_and_gpu.jl — §1–3 here, §4–7 GPU deck.python/4_memory_and_gpu_python.ipynb⬅ Previous: 3 — Threads & concurrency · Next: 5 — GPU ➡