Week 8 study guide
Week 8 writes kernels the way a GPU runs them: a grid of blocks, each a group of threads with its own shared memory and a barrier. Two loop axes describe that, a stronger rule keeps threads and blocks from colliding, and render-cuda writes the CUDA. No GPU is needed: the grader's emulator checks every bit. This page recaps the week, collects the rules worth memorizing, and walks the lab one step at a time.
The week in one page
8.1 How a GPU runs code
A kernel is one function every thread runs. Threads come in blocks of up to 1024, a launch starts a grid of blocks, and each thread finds its work from blockIdx.x and threadIdx.x. Blocks run in any order and never wait for each other. Memory is global (everyone), shared (one block) or registers (one thread). -fmad=false and -prec-div=true keep the bits identical to the CPU's.
8.2 Global and local loops
axis=global is the grid, axis=local a block's threads, with a literal bound from 1 to 1024. Neither changes a result. Offsets that are stored, or loaded where the same code stores, must be mixed-radix numbers: digits are loop indices (or their div or mod by a literal) times positive literals, and sorted by coefficient each must beat the most the smaller digits add up to. The offset contains the parallel index, and every access to a buffer is the same expression.
8.3 Shared memory and barriers
Consecutive local loops are phases, with a barrier between them. The rule is checked per phase, so a later phase may read any shared slot an earlier one wrote. An alloc used only in one global loop is shared memory, zeroed for each block. A missing barrier can pass by luck in a block of one or two warps, so the suite also uses blocks of 1024.
8.4 Rendering CUDA
Each top-level global loop is a __global__ function. Shared allocs are __shared__ arrays, cleared by the threads. Each phase is a block on threadIdx.x followed by __syncthreads(). A host kernel launches the kernels in order with UIR_LAUNCH, which the grader defines for real CUDA or for the emulator. For the CPU, a global loop renders like a thread loop.
8.5 Getting a GPU
The emulator grades everything but speed. With an NVIDIA GPU: nvcc on Linux, or --cuda-docker on Windows. Otherwise Colab, Kaggle, or a rented machine on Vast.ai or RunPod for well under a dollar an hour. Bring no secrets to a rented machine, and stop it when you're done.
8.6 Lab
The suite has 39 tests: run tests for the rules, r_ tests for the CPU render, c_ tests for CUDA (four of them must be refused), and one GPU speed test that needs half the reference's speed. Weeks 1 to 7 must stay green.
Cheat sheet
| .uir | run | render (CPU) | render-cuda |
|---|---|---|---|
axis=global | a loop. Shared allocs zeroed each iteration | outlined, run by uir_parallel_for | a __global__ function, blockIdx.x |
axis=local | a loop | a loop | { int32_t t = threadIdx.x; ... } then __syncthreads(); |
| alloc used in one global loop | fresh zeros per block | declared in the outlined function | __shared__, cleared by the threads, then a barrier |
| digit | largest value |
|---|---|
c · i, i a loop of n | n - 1 |
c · (i div k) | (n - 1) div k |
c · (i mod k) | k - 1 (or n - 1 if smaller) |
a vector access of k elements | coefficient 1, largest value k - 1 |
Mistakes people make
Each of these is a bug that was planted in the reference compiler to prove the suite catches it. The week 8 test guide lists every test with the bugs known to fail it.
- No barrier between phases. Caught by
c_reverse_shared,c_reverse_shared_1024and the speed test. - Not clearing shared memory. On a GPU it holds an earlier block's leftovers. Caught by
c_shared_dirty_then_fresh. - Running a phase on one thread. The bits stay right, but the GPU sits idle. Caught by
s_cuda_gemm_512on a GPU. - Letting digits touch. A coefficient equal to the most the smaller digits add is a collision. Caught by
err_digits_exactly_touch. - Forgetting the parallel index. Every block writing
o[t]. Caught byerr_blocks_write_one_placeanderr_threads_write_one_cell. - Checking only stores in a phase. Caught by
err_same_phase_neighbor. - Missing a shape rule. A store outside a local loop, a local loop outside a global one, two block sizes, or two layouts for one buffer. Caught by
err_store_outside_local,err_local_outside_global,err_local_sizes_differanderr_block_layouts_differ.
Stuck on the lab?
Build in this order, and don't move on until the check passes.
- Axes as plain loops. Parse
globalandlocal, check local bounds, then ignore them. The run tests that expect a result pass, exceptblock_shared_starts_at_zero. - Shared memory in run. Done when this program's buffer is
0 1 2 1 2 3(and0 1 2 1 3 5without the axes):%o = buffer 6 dtype=i32 slot=0 %sh = alloc 3 dtype=i32 %b = range 2 axis=global %t = range 3 axis=local %sv = shrink %sh %t shape=[] %cur = load %sv %bt = add %b %t %n = add %cur %bt %s = store %sv %n %e1 = end %s %t %sh1 = after %sh %e1 %u = range 3 axis=local %rv = shrink %sh1 %u shape=[] %r = load %rv %m = mul %b 3 %j = add %m %u %ov = shrink %o %j shape=[] %s2 = store %ov %r %e2 = end %s2 %u %eb = end %e1 %e2 %b %o1 = after %o %eb - The rules. Shape rules first, then the mixed-radix rule per phase and per kernel. Done when every run test passes.
- The CPU render. Reuse your thread-loop outlining. Done when the
r_tests pass. - render-cuda. Done when every
c_test passes in the emulator, then on a GPU with the speed test.
Check yourself
Blocks b in 0..1, threads t in 0..7. Which store offset is valid?
What's the most t adds?
t adds up to 7, so the coefficient of b must be more than 7.Phase 2 reads a shared slot that another thread wrote in phase 1. Is that a race?
What separates two phases?
__syncthreads().An alloc is used only inside a global loop. What does it hold when block 5 starts?
Shared memory, in .uir.