Skip to content

A05 Exercises

Do these yourself before peeking at ../solutions/. Write real, compiling, edge-case-correct code.

This directory has its own Makefile, so you can build and run without leaving it:

make                                   # list targets
make all GPU_ARCH=gfx950               # build every exercise for your GPU
make 01_exclusive_to_inclusive.exe     # build just one
make run                               # build + run all (FAIL is expected until solved)
make clean

Set GPU_ARCH to your device (gfx950 MI350, gfx942 MI300, gfx90a MI200); find it with /opt/rocm/bin/offload-arch. The equivalent raw command, if you prefer to see it spelled out:

hipcc -O3 --offload-arch=gfx942 -I../hip 01_exclusive_to_inclusive.cpp -o 01_exclusive_to_inclusive.exe && ./01_exclusive_to_inclusive.exe
# NVIDIA: rename to .cu, replace hip* with cuda*, and: nvcc -O3 -arch=sm_90 -I../cuda ...

(The exercises reuse the module's HIP_CHECK macro via -I../hip.)

# File Skill
1 01_exclusive_to_inclusive.cpp Convert exclusive ↔ inclusive scan; handle the boundary element and recover the grand total an exclusive scan discards.
2 02_segmented_scan.cpp Implement a segmented scan (resets at flagged boundaries) — the primitive behind ragged/jagged ML batches.
3 03_scan_analysis.md Whiteboard: work vs depth for Hillis–Steele vs Blelloch, the extra-work factor, and when work-inefficient still wins.

Each drill maps to an interview competency (off-by-one rigor on scan variants / composing an associative operator that carries state / cost-model reasoning under real hardware limits). Solutions include the reasoning, not just the answer key.


Sample sandbox output

The PASS/FAIL states, mismatch counts, and totals below are deterministic — they follow from the fixed inputs in each drill, so you can compare against them directly. Timings and bandwidths are hardware-dependent and are left as placeholders; run the labs to get yours. NVIDIA output is equivalent (CUDA error … instead of HIP error …).

Exercise 1 — Exclusive ↔ inclusive (01_exclusive_to_inclusive.exe)

The starter's two conversion kernels write zeros, so only the given Blelloch scan passes:

$ ./01_exclusive_to_inclusive.exe
n = 256
given  blelloch exclusive scan : PASSED (0 mismatches)
TODO 1 exclusive -> inclusive  : FAILED (256 mismatches)
TODO 2 inclusive -> exclusive  : FAILED (255 mismatches)
TODO 3 grand total             : FAILED (got 0.0, expected 766.0)

[!TIP] Read those two counts: TODO 1 misses all 256 elements, TODO 2 misses 255 — index 0 is accidentally right because the exclusive scan's first element is the identity. That one-element difference is the whole lesson of the drill.

Once all three TODOs are done:

$ ./01_exclusive_to_inclusive.exe
n = 256
given  blelloch exclusive scan : PASSED (0 mismatches)
TODO 1 exclusive -> inclusive  : PASSED (0 mismatches)
TODO 2 inclusive -> exclusive  : PASSED (0 mismatches)
TODO 3 grand total             : PASSED (got 766.0, expected 766.0)

Exercise 2 — Segmented scan (02_segmented_scan.exe)

The starter runs an unsegmented scan, so it is correct up to the first segment head and then carries the previous segment's total forward forever:

$ ./02_segmented_scan.exe
segmented inclusive scan: n = 256, 7 segments (head every 37 elements)
result : FAILED (219 mismatches)
first mismatch at i = 37 (segment head at 37): got 111.0, expected 3.0
An unsegmented scan is correct until the first head, then carries the
previous segment's total forward. That is the bug the flags must stop.

219 = 256 − 37: every element from the first head onward is wrong. After fixing the combine to consult and propagate the flags:

$ ./02_segmented_scan.exe
segmented inclusive scan: n = 256, 7 segments (head every 37 elements)
result : PASSED (0 mismatches)

The solution also prints each head's value -> scan, where the two must be equal — the visible proof that the scan restarted.

Exercise 3 — Work vs depth (no program)

Paper drill — no runtime output. Check your numbers against ../solutions/03_scan_analysis_solution.md. Expected headline: at n = 1024 Hillis–Steele does 9,217 adds against Blelloch's 2,046 — a 4.5× extra-work factor that grows as (log₂n − 1)/2 — yet Blelloch pays 20 barriers to Hillis–Steele's 10 and runs 14 of its 20 tree levels below 25% thread utilization. Both sit far below the roofline ridge point, which is why the "inefficient" one is still the right choice inside a small tile.

Module labs, for comparison

Running the built module labs should look like this (<...> = hardware-dependent):

$ make run-hip
== hip/scan.exe ==
parallel scan: n = 4194304 elements (16.0 MiB), tile = 256 elements
  hillis-steele: 256 threads/tile (1 elem/thread), 8 steps
  blelloch     : 128 threads/tile (2 elems/thread), 16 steps (up+down)

Part 1 - single-tile correctness (first 256 elements)
kernel                                   scan type   result
---------------------------------------- ----------- --------
hillis-steele (ping-pong buffers)        inclusive   PASSED
hillis-steele (in-place + register)      inclusive   PASSED
blelloch (up-sweep / down-sweep)         exclusive   PASSED

Part 2 - per-tile scans of the whole array (16384 tiles, same for both)
kernel                                   time (ms)   GB/s      result
---------------------------------------- ----------- --------- --------
hillis-steele  O(n log n) work           <ms>        <GB/s>    PASSED
blelloch       O(n) work                 <ms>        <GB/s>    PASSED

Part 3 - full exclusive scan of all 4194304 elements (3-phase decomposition)
recursion levels : 3 (tile sums, then sums of those, ...)
time             : <ms>
effective BW     : <GB/s>  (useful bytes only: read n + write n)
result           : PASSED

== hip/compaction.exe ==
stream compaction (keep non-zeros): n = 4194304
kept (GPU)       : 2796202
kept (std::copy_if): 2796202
time             : <ms>  (mark + 3-phase scan + scatter)
effective BW     : <GB/s>  (useful bytes: read n + write kept)
result           : PASSED (0 value mismatches)

Both labs must print PASSED. The interesting question is not correctness but how far the reported GB/s sits below the copy ceiling you measured in A02 Lab 1 — see §5 of this module's README.