Skip to content

Commit 25e9807

Browse files
Add Module 001: GPU Fundamentals (#1)
Initial curriculum module with lecture notes, quiz, and four hands-on exercises (CPU-GPU comparison, peak throughput, occupancy calculator, roofline analysis). Each exercise ships with starter.py and check.py. Adds .gitignore for Python build artifacts and exercise outputs. Co-authored-by: curriculum-night-runner <noreply@anthropic.com>
1 parent 5d453d8 commit 25e9807

19 files changed

Lines changed: 1988 additions & 3 deletions

File tree

.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
.pytest_cache/
6+
.mypy_cache/
7+
.ruff_cache/
8+
9+
# Virtual envs
10+
.venv/
11+
venv/
12+
env/
13+
14+
# Editor
15+
.vscode/
16+
.idea/
17+
*.swp
18+
.DS_Store
19+
20+
# Build / coverage
21+
build/
22+
dist/
23+
.coverage
24+
htmlcov/
25+
coverage.xml
26+
27+
# Exercise outputs
28+
roofline.png

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@ This curriculum was developed with input from:
592592

593593
**Ready to become an AI/ML Performance Engineering expert?**
594594

595-
Start with [Module 1: GPU Fundamentals →](lessons/01-gpu-fundamentals/README.md)
596-
597-
or jump into [Project 1: Model Compression →](projects/project-01-model-optimization/README.md)
595+
Start with [Module 1: GPU Fundamentals →](modules/mod-001-gpu-fundamentals/README.md)
596+
*(Module 1 ships with 6 learning objectives, lecture notes, four
597+
autograded CPU-only exercises, and a 12-question quiz. Modules 2+
598+
are scheduled.)*
599+
600+
> The longer-form curriculum spec (8 modules + 3 projects) lives in
601+
> [`CURRICULUM.md`](CURRICULUM.md). Modules will be promoted from
602+
> spec to implementation incrementally.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Module 1 — GPU Fundamentals
2+
3+
> **Track:** AI/ML Performance Engineer
4+
> **Time budget:** ~20 hours (2 weeks part-time)
5+
> **Module status:** first build, 2026-05-22
6+
7+
This is the first module in the Performance Engineering track. It sets the
8+
mental model you will reuse for the rest of the curriculum: what a GPU
9+
actually is, why it is fast on some workloads and slow on others, and how to
10+
reason about that quantitatively *before* you write a single line of CUDA.
11+
12+
The lecture is intentionally light on CUDA code. You cannot meaningfully
13+
optimize what you do not understand at the architecture level, and almost
14+
all real-world performance work is decided in the math (arithmetic
15+
intensity, memory bandwidth, occupancy) — not in the kernel syntax. The
16+
CUDA programming syntax is the focus of [Module 2](../mod-002-cuda-programming/README.md).
17+
For this module, your job is to think like a hardware engineer.
18+
19+
## Learning objectives
20+
21+
By the end of this module, you will be able to:
22+
23+
1. **Explain** the SIMT execution model and how a warp differs from a thread
24+
in CPU-style multi-threading.
25+
2. **Calculate** the theoretical peak FP32, TF32, and FP16 throughput of a
26+
given NVIDIA GPU from its published spec sheet.
27+
3. **Calculate** theoretical peak memory bandwidth from a memory clock,
28+
memory bus width, and memory type (GDDR / HBM).
29+
4. **Identify** whether a workload is compute-bound or memory-bound using
30+
arithmetic intensity and the roofline model.
31+
5. **Estimate** kernel occupancy from a launch configuration (block size,
32+
registers per thread, shared memory per block) and explain which of those
33+
three resources is the limiting one.
34+
6. **Distinguish** the levels of the GPU memory hierarchy (registers,
35+
shared / L1, L2, global / HBM) and predict which level a memory access
36+
will hit given a kernel's access pattern.
37+
38+
These are measurable; every exercise targets one or more of them.
39+
40+
## Prerequisites
41+
42+
You should be comfortable with:
43+
44+
- Python (numpy in particular)
45+
- Basic linear algebra (matrix multiply, what FLOPS means)
46+
- Reading a hardware spec sheet without panicking
47+
48+
You do **not** need to have written CUDA before. You also do not need to
49+
have an NVIDIA GPU available *to do this module* — all four exercises run
50+
on CPU. (A GPU is required from Module 2 onward.)
51+
52+
## Module structure
53+
54+
| File | What's in it |
55+
|---|---|
56+
| [`lecture-notes.md`](lecture-notes.md) | The actual material. Six lessons covering CPU vs GPU, NVIDIA architecture, memory hierarchy, threads / blocks / grids, warp execution, and performance metrics. |
57+
| [`exercises/exercise-01-cpu-gpu-comparison`](exercises/exercise-01-cpu-gpu-comparison/) | Use the SIMT model to predict which of three workloads accelerates on a GPU and by roughly how much. CPU-only. |
58+
| [`exercises/exercise-02-peak-throughput`](exercises/exercise-02-peak-throughput/) | Given spec-sheet values, compute peak FP32 / TF32 / HBM bandwidth for A100, H100, RTX 4090. CPU-only. |
59+
| [`exercises/exercise-03-occupancy-calculator`](exercises/exercise-03-occupancy-calculator/) | Reimplement NVIDIA's occupancy calculation from scratch. CPU-only. |
60+
| [`exercises/exercise-04-roofline-analysis`](exercises/exercise-04-roofline-analysis/) | Plot a roofline for an A100 and place six kernels on it. CPU-only. |
61+
| [`quiz.md`](quiz.md) / [`quiz-answers.md`](quiz-answers.md) | 12-question check on the learning objectives. |
62+
| [`resources.md`](resources.md) | Primary sources — NVIDIA architecture whitepapers, the CUDA C Programming Guide, Volkov's thesis, the roofline paper. |
63+
64+
## How to work through this module
65+
66+
1. Read `lecture-notes.md` straight through once. It is short on purpose.
67+
2. Do the four exercises in order. Each one has a `starter.py` you edit
68+
and a `check.py` that grades your answer. `python check.py` should
69+
print `PASS` when you are done.
70+
3. Take the quiz **without** scrolling to the answer key. If you miss
71+
more than two questions, reread the relevant lesson before moving on
72+
to Module 2.
73+
74+
## Assessment criteria for this module
75+
76+
You are ready to move on when you can, on a blank page:
77+
78+
- Write the SIMT execution model in three sentences without using the word
79+
"thread" the way a CPU programmer would use it.
80+
- Compute peak FP32 throughput of any NVIDIA GPU from CUDA-core count and
81+
boost clock.
82+
- Sketch a roofline and place a kernel on it given its arithmetic
83+
intensity and achieved GFLOP/s.
84+
85+
Module 2 picks up from these primitives and starts writing kernels.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Exercise 1 — When does the GPU actually help?
2+
3+
> **Targets learning objectives:** 1, 4
4+
> **Time:** ~45 min
5+
> **Requires:** Python 3.10+, numpy. No GPU needed.
6+
7+
## What you'll do
8+
9+
You will be given three concrete workloads. For each, you predict —
10+
*without writing CUDA* — whether a modern data-center GPU will outperform
11+
a modern server CPU, and by approximately how much. You will defend each
12+
answer with a one-paragraph argument grounded in the SIMT execution
13+
model.
14+
15+
The point of this exercise is to make you justify GPU-vs-CPU decisions
16+
the way they get justified in real engineering reviews: with arithmetic
17+
intensity, memory bandwidth, and parallelism analysis — not with vibes.
18+
19+
## The workloads
20+
21+
1. **`dense_matmul`** — Multiply two FP32 matrices of shape (8192, 8192).
22+
The output is FP32. Inputs and outputs all live in device memory
23+
already (no host-device transfer cost in your analysis).
24+
25+
2. **`json_parse`** — Parse a single 1-GB JSON file containing a deeply
26+
nested object hierarchy. Output is a Python dict.
27+
28+
3. **`elementwise_relu`** — Apply `y = max(0, x)` to a single FP32
29+
tensor of shape (1024, 1024). Input and output already live on the
30+
appropriate device (CPU memory for the CPU case, device memory for
31+
the GPU case).
32+
33+
## What to submit
34+
35+
Edit `starter.py`. For each workload, fill in:
36+
37+
- `gpu_helps: bool` — Will the GPU outperform a comparable CPU?
38+
- `expected_speedup_bucket: str` — One of `"<1x"`, `"1-2x"`,
39+
`"2-10x"`, `"10-100x"`, `">100x"`.
40+
- `reasoning: str` — One paragraph (3–6 sentences). At minimum,
41+
identify (a) is this compute-bound or memory-bound, (b) how much
42+
parallelism is available, (c) what kills it on the GPU if anything.
43+
44+
Then run:
45+
46+
```bash
47+
python check.py
48+
```
49+
50+
The check looks at your bucketed answer (it does **not** grade the
51+
prose — that is what the solutions repo's worked answer is for) and
52+
prints `PASS` when all three are right.
53+
54+
## Hints
55+
56+
- For `dense_matmul`: how many independent multiply-accumulates are in
57+
the output? How many bytes of HBM are read? Compute the arithmetic
58+
intensity and place it relative to an A100's ridge point (~9.75
59+
FLOP/byte for FP32).
60+
- For `json_parse`: how much arithmetic per byte of input? How many
61+
truly independent sub-tasks are there? Is the workload going to live
62+
on the GPU's arithmetic units or on its control-flow logic?
63+
- For `elementwise_relu`: arithmetic intensity is ~0.125 FLOP/byte
64+
(one compare, one select per FP32). The interesting question is
65+
*kernel launch overhead vs total work*. How many bytes does the
66+
GPU move for a (1024, 1024) FP32 tensor? At HBM bandwidth, how long
67+
does that take? Compare to a 5–20 µs kernel launch.
68+
69+
## What "right" looks like
70+
71+
This is a reasoning exercise; the buckets are coarse on purpose. There
72+
is one defensible answer per workload at this granularity, and the
73+
solutions repo's `SOLUTION.md` walks through the derivation in full.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Autograder for Exercise 1. Run: python check.py
2+
3+
Grades only the bucketed answer (gpu_helps + expected_speedup_bucket).
4+
The reasoning prose is graded by the human reader against the
5+
solutions repo's worked answer.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import sys
11+
from pathlib import Path
12+
13+
# Make sibling starter.py importable regardless of CWD.
14+
sys.path.insert(0, str(Path(__file__).parent))
15+
16+
try:
17+
import starter # type: ignore[import-not-found]
18+
except ImportError as e:
19+
print(f"FAIL: could not import starter.py ({e})")
20+
sys.exit(1)
21+
22+
23+
EXPECTED = {
24+
# Dense GEMM at 8192^3 has AI ~ 341 FLOP/byte, far above the A100
25+
# ridge point (~9.75). It is the canonical compute-bound GPU win.
26+
# Practical speedups for FP32 dense GEMM on cuBLAS vs MKL on a
27+
# modern server CPU land at >100x (8192^3 is ~1.1 TFLOP of work;
28+
# a 19.5 TFLOPS A100 does it in ~60 ms, vs several seconds on
29+
# most server CPUs).
30+
"dense_matmul": {"gpu_helps": True, "bucket": ">100x"},
31+
# JSON parsing is irregular control flow, tiny arithmetic per byte,
32+
# serial dependency from one nested element to the next. The GPU
33+
# has no path to win.
34+
"json_parse": {"gpu_helps": False, "bucket": "<1x"},
35+
# 4 MB tensor at ~2 TB/s HBM is ~2 microseconds of memory work.
36+
# Kernel launch alone is 5-20 us. The CPU does this in <1 ms with
37+
# warm caches and no kernel-launch tax; the GPU is bottlenecked by
38+
# launch overhead at this size. Most realistic: 1-2x (GPU might
39+
# barely win or barely lose). Either way, not a meaningful
40+
# speedup; the bucketed answer is 1-2x.
41+
"elementwise_relu": {"gpu_helps": True, "bucket": "1-2x"},
42+
}
43+
44+
VALID_BUCKETS = {"<1x", "1-2x", "2-10x", "10-100x", ">100x"}
45+
46+
47+
def grade() -> int:
48+
answers = getattr(starter, "ANSWERS", None)
49+
if not isinstance(answers, dict):
50+
print("FAIL: ANSWERS dict missing from starter.py")
51+
return 1
52+
53+
failures: list[str] = []
54+
for name, expected in EXPECTED.items():
55+
a = answers.get(name)
56+
if not isinstance(a, dict):
57+
failures.append(f" {name}: entry missing or not a dict")
58+
continue
59+
gh = a.get("gpu_helps")
60+
bk = a.get("expected_speedup_bucket")
61+
if gh is ... or bk is ... or gh is None or bk is None:
62+
failures.append(f" {name}: not filled in (still ... )")
63+
continue
64+
if not isinstance(gh, bool):
65+
failures.append(f" {name}: gpu_helps must be bool, got {type(gh).__name__}")
66+
continue
67+
if bk not in VALID_BUCKETS:
68+
failures.append(
69+
f" {name}: expected_speedup_bucket must be one of {sorted(VALID_BUCKETS)}, got {bk!r}"
70+
)
71+
continue
72+
if gh != expected["gpu_helps"]:
73+
failures.append(
74+
f" {name}: gpu_helps={gh} but expected {expected['gpu_helps']}"
75+
)
76+
continue
77+
if bk != expected["bucket"]:
78+
failures.append(
79+
f" {name}: expected_speedup_bucket={bk!r} but expected {expected['bucket']!r}"
80+
)
81+
continue
82+
83+
if failures:
84+
print("FAIL:")
85+
for f in failures:
86+
print(f)
87+
return 1
88+
89+
print("PASS — all three workloads bucketed correctly.")
90+
print("(Reasoning prose is not autograded; compare against the solutions repo.)")
91+
return 0
92+
93+
94+
if __name__ == "__main__":
95+
sys.exit(grade())
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Exercise 1 starter — fill in your answers for the three workloads.
2+
3+
Each entry of `ANSWERS` is a dict with three keys:
4+
5+
gpu_helps: bool
6+
True if you expect a modern data-center GPU (A100/H100 class) to
7+
outperform a comparable modern server CPU on this workload.
8+
9+
expected_speedup_bucket: str
10+
One of: "<1x", "1-2x", "2-10x", "10-100x", ">100x".
11+
">100x" means the GPU is more than 100 times faster.
12+
"<1x" means the CPU is faster.
13+
14+
reasoning: str
15+
3-6 sentences of justification. The autograder does not grade
16+
the prose; the solutions repo has the worked answer.
17+
18+
Replace each `...` with your answer. Then run:
19+
20+
python check.py
21+
"""
22+
23+
ANSWERS = {
24+
"dense_matmul": {
25+
# Multiply two FP32 (8192, 8192) matrices, all data already on device.
26+
"gpu_helps": ...,
27+
"expected_speedup_bucket": ...,
28+
"reasoning": ...,
29+
},
30+
"json_parse": {
31+
# Parse a 1 GB deeply-nested JSON file.
32+
"gpu_helps": ...,
33+
"expected_speedup_bucket": ...,
34+
"reasoning": ...,
35+
},
36+
"elementwise_relu": {
37+
# y = max(0, x) on a single FP32 (1024, 1024) tensor,
38+
# data already on the appropriate device.
39+
"gpu_helps": ...,
40+
"expected_speedup_bucket": ...,
41+
"reasoning": ...,
42+
},
43+
}

0 commit comments

Comments
 (0)