Architecture

How the compiler is put together, and the decisions that are not obvious from the code. Change a fixed decision here first (see CONTEXT.md). Per-pass details are in PASSES.md; the formats are in LANGUAGE.md, IR.md, TTPU_FORMAT.md and TGPU_FORMAT.md.

Pipeline

 .tiny -> AST -> Graph IR -> graph passes -+-> Tile IR -> tile passes -> .ttpu.json   (tiny-tpu RTL)
                                           |
                                           +-> memory plan -> Kernel IR -> kernel passes
                                                            -> registers -> .tgpu + launch  (tiny-gpu)

Every level has a printer, a verifier and an interpreter; every pass can be validated against the numpy oracle (--verify-each); and both backends run on the real chip/simulator, never on a re-implementation.

Optimization levels (-O)

Level Graph IR Tile IR (tpu) Kernel IR (gpu)
-O0 shape-infer, range-check none naive kernels
-O1 (default) + simplify fixpoint, fuse + skip-zero-passes + licm, strength-reduce, kernel-dce
-O2 same as -O1 same as -O1 + shared-tile, unroll, fold

Individual passes can be switched off by name (--disable licm), which is how the table below isolates each optimization.

Kernel optimizations, measured

Real simulator runs (tinyc/gpu/report.py; regenerate with python scripts/opt_table.py --write, and a test fails if this table drifts). “Cycles” are tiny-gpu cycles: a thread’s cycle ends at a taken branch, so straight-line code is one cycle however long it is; read them together with instructions. relu makes the exact counts data dependent, so these use a fixed seed.

mm_relu 4x4 (C = relu(A @ B), int8, seed 42)

configuration instructions cycles global loads shared loads registers
-O0 (naive) 1000 6 144 0 4
+ licm 840 (-16%) 5 128 (-11%) 0 5
+ strength-reduce (= -O1) 728 (-27%) 5 128 (-11%) 0 6
-O2 with shared-memory tiling only 728 (-27%) 6 80 (-44%) 64 5
-O2 with unrolling + folding only 504 (-50%) 2 128 (-11%) 0 7
-O2 (all passes) 504 (-50%) 3 80 (-44%) 64 5

mm_relu 8x8 (C = relu(A @ B), int8, seed 42)

configuration instructions cycles global loads shared loads registers
-O0 (naive) 7320 10 1088 0 4
+ licm 6168 (-16%) 9 1024 (-6%) 0 5
+ strength-reduce (= -O1) 5208 (-29%) 9 1024 (-6%) 0 6
-O2 with shared-memory tiling only 4952 (-32%) 10 576 (-47%) 512 5
-O2 with unrolling + folding only 3544 (-52%) 2 1024 (-6%) 0 7
-O2 (all passes) 3288 (-55%) 3 576 (-47%) 512 5

What each pass does, and what it costs:

Why -O2 is not simply better

Decisions worth knowing

Decision Why
Temporaries are assigned once; only accumulators, pointers and counters are reassigned It makes every kernel pass sound without dataflow analysis: a single-assignment value can be moved or rewritten freely.
Kernel passes are validated against the oracle after each pass The structured kernels are interpreted (independently of the allocator and assembler); the final assembly is run on the simulator.
The estimate is an abstract interpretation Values that depend only on block/thread ids are exact, so loads, stores, shared accesses and the tiled copy’s guards are exact; only a branch on loaded data (relu) gives min/max.
One fresh simulator per kernel launch, memory carried over tiny-gpu’s set_grid resize leaves R7 = 0 for threads it adds.
-O levels are cumulative, kernel passes are switchable by name Reproducibility, and so the table above can attribute each gain.

Known limits