Week 7 · Lesson 6

The week 7 lab

Teach your compiler the three loop axes. It checks the thread rules, runs private allocs fresh in each iteration, unrolls upcast loops, and outlines thread loops for uir_parallel_for. You're done when every week 7 test passes, including three speed tests on a 768×768 GEMM, and weeks 1 to 6 still pass, and you've written 10 tests of your own.

Warm up

The schedule the speed tests use, on a GEMM small enough to read.

Warm-up: schedule a GEMM

Make the row-block loop a thread loop and the six tile loops upcast loops. Only attributes change, and the result's bits don't.

The command-line contract

commandweek 7 changes
your.exe run FILErange takes axis=loop|upcast|thread. Every axis runs as an ordinary loop, except that a private alloc starts at zero in every iteration of its thread loop. New static errors (exit 2): an unknown axis, an upcast bound that isn't a literal from 1 to 32, a thread loop inside a thread loop, and a store (or a load of memory the loop stores into) that isn't at r·S + e with 0 ≤ e, e + size ≤ S, one S per buffer.
your.exe render FILEUnroll upcast loops. Outline thread loops into a function and call uir_parallel_for, declared in your C. Private allocs are declared inside the outlined function.

How the tests are graded

Running the suite

python tools/test.py --compiler "C:/path/to/your.exe" --week 7
python tools/test.py --compiler "C:/path/to/your.exe" --week 7 -k s_gemm -v --jobs 1   # the speed tests, one at a time. A slow one prints both times
python tools/bench.py --compiler "C:/path/to/your.exe" tests/week7/render/s_gemm_768_threads_and_upcast.uir

Then run weeks 1 to 6 again, one --week at a time. Pass the built executable, not dune exec.the same wrapper script as in week 1. The speed tests read data/a768.f32 and data/b768.f32 next to them, 2.4 MB each.

Write your 10 tests

Ten tests in path/to/your-repo/tests/week7/, checked with python tools/test.py --check-tests path/to/your-repo/tests/week7. Good targets: a store at r·S + e where e just fits and one where it's one over, a vector store whose size pushes it out of its slice, a load of a neighbor's element, a private alloc read before it's written, a thread loop inside a plain loop that captures the outer counter, and an upcast loop of 32.

A build order that works

  1. Parse axis=

    Store the axis on the range. Validate it, and check upcast bounds. Treat every axis as a plain loop everywhere else.

    Checkpoint: weeks 1 to 6 pass, and every week 7 run test passes except the thread-rule and private-alloc ones.

  2. The thread rules

    After linearizing, walk each thread loop's body. Reject nesting. For each store, and each load of memory the body stores into, split the flat offset into a sum of terms, find the one term with r, and bound the rest with your week 1 bounds.

    Checkpoint: every err_thread_* test exits 2, and the valid ones run.

  3. Private allocs

    An alloc whose every load and store is inside one thread loop is private. Re-zero it at the start of each iteration in run.

    Checkpoint: thread_private_alloc_fresh and loop_alloc_carries_over both pass.

  4. Render upcast

    In the emitter, emit an upcast loop's body once per value, with the range as a literal.

    Checkpoint: r_upcast_sum_order and the r_gemm_tiled_* tests pass, bit for bit.

  5. Render thread loops

    Emit the body into its own list of lines, collect its free names and their C types, and write a struct, a static function and the uir_parallel_for call. Declare private allocs inside the function.

    Checkpoint: every render test passes.

  6. Speed

    Run the speed tests with -v. If one is slow, compare your C with python _reference/uirc.py render. Look for an upcast loop left as a loop, missing restrict, or a private alloc that ended up shared.

    Checkpoint: all three speed tests pass.

Checklist

The AI policy, still

An AI assistant may explain concepts, draw diagrams, and help you debug by asking questions, but must never write or rewrite compiler code. "Why is my upcast tile slower than the reference's?" is a fine question. "Write my outlining pass" is not.

Check yourself

Your thread-loop renderer forgets to declare private allocs inside the outlined function, and uses one shared array instead. What fails?

What does the spec say each iteration starts with?

A shared array gives wrong answers when an iteration reads before writing (r_thread_private_alloc), and even when the program resets it, threads race on it. The planted bug "render private allocs shared" fails r_thread_private_alloc and both threaded speed tests.

The speed test reports "too slow: 12.0 ms, target 6.4 ms, reference 3.2 ms" on s_gemm_768_threads_and_upcast. What's the first thing to check?

Compare with the measured times in lessons 7.3 and 7.4.

On that machine the GEMM took about 12 ms with threads only and 3.2 ms with threads and upcast. A factor of about 4 is the signature of an upcast loop rendered as a for loop. The bits are already right, or the test would have said so first.

Your machine has 2 hardware threads. What do the thread speed tests check?

The spec's grading section.

With 2 threads, even a perfect threaded kernel is at most 2× faster than a sequential one, which is too close to the 0.5 target to separate right from wrong. So those tests check correctness only.