Two hand-built animations of GPU-architecture coursework at Georgia Tech (OMSCS CS8803, GPU Hardware and Software). Everything here is my own measured data and my own visualization code. No course framework or solution source is in this repository — see docs/HONOR_CODE.md.
I extended a trace-driven 8-core GPU simulator with a compute-execution stage, then a tensor execution stage: 64-cycle tensor ops issued 8-wide, with a RAW dependency check bolted onto the round-robin warp scheduler. Exactly two XML config lines separate the two machines.
The result is counter-intuitive, and it is the reason this animation exists. The tensor core is slower on every half-precision benchmark. With a 64-cycle tensor latency, 8-wide issue, and only 4 warps resident per core, there are not enough warps to hide the latency — the extra throughput never gets used. The float benchmarks are bit-identical between the two configurations, because those traces contain no HMMA instructions and the tensor path is never taken.
| half-precision benchmark | cycles Δ | stall cycles Δ | IPC Δ | lockstep until |
|---|---|---|---|---|
| gemm_half | +1.21% | +2.16% | −1.20% | kernel 5 of 7 |
| cnn_half | +8.77% | +19.18% | −8.07% | kernel 1 of 15 |
| ffn_half | +9.30% | +15.16% | −8.50% | kernel 0 of 6 |
| gpt2_half | +20.89% | +56.15% | −17.28% | kernel 28 of 74 |
All eight benchmarks × two configurations matched the course reference exactly (100%).
Data: data/proj4_summary.json,
data/proj4_timeline.json — per-kernel, per-core cumulative
cycle/stall/instruction counts extracted from my own simulator run logs.
A static analysis over GPU assembly (SASS): parse the assembly, split it into basic blocks,
build the control-flow graph, then run a taint analysis seeded at S2R Rn, SR_TID.X — the
instruction that reads the thread index. Taint propagates along def-use edges, and, importantly,
through memory: a load from a thread-dependent address produces a thread-dependent value. Any
branch whose predicate ends up tainted will diverge across the warp.
The featured kernel is my own bitonic compare-exchange source, compiled with nvcc (-arch=sm_75)
and disassembled with nvdisasm, then fed through my analysis pass — so the instruction stream shown
is derived entirely from my own artifact. On it (9 basic blocks, 8 edges, 23 instructions) the
analysis finds three divergent branches — 0050, 00c0, 00e0 — which map back to the three
conditions in the source: the ij > i guard, the (i & k) test, and the a[i] > a[ij] compare.
The worklist is keyed on (block, tainted-register-set), not just block, so the same block is
re-analyzed under different taint states until the analysis converges. Across the full 20-kernel
test corpus the pass parses every kernel (325 basic blocks, 1,921 instructions) and flags 136
divergent branches.
Data: data/prj5_cfg.json, data/prj5_trace.json,
data/prj5_corpus.json — the CFG and a step-by-step record of the
worklist, produced by an instrumented copy of my analysis pass that is checked against the real
pass's output before it emits anything.
npm ci
npx playwright install chromium
npm run capture:all
Each animation is a single self-contained HTML file that exposes a deterministic frame API —
window.__set_fps(n), window.__total_frames, window.__seek(n) — so a headless Chromium can
render frame n exactly, with no dependence on wall-clock time. tools/capture.mjs walks every
frame, writes PNGs, and runs a two-pass ffmpeg palettegen/paletteuse to a size-capped GIF plus an
H.264 MP4. It also fails the build if the page makes any non-file: request, which is how
"self-contained" stays true.
animations/ the two self-contained animation pages
data/ my measured results and derived analysis data (JSON)
gifs/ rendered GIF + MP4 artifacts
tools/ capture pipeline + data extraction (see tools/README.md)
docs/ honor-code statement
MIT for the visualization code. The data files are my own measured results.

