Weight-stationary, cycle by cycle
The TPU's dataflow: weights sit still, skewed activations march right, partial sums flow down and drain out of the bottom. Every register, every cycle.
Loading the animation…
Concept
This is the whole machine, one clock cycle per step, with the value in every register. The array is PEs: one PE for every weight of .
- Load (the first cycles). The weights enter at the top, one row of per cycle, and shift down until PE holds (the vermillion "w" in each PE). From then on they stay put: the dataflow is named after what is stationary.
- Stream. Row of enters from the left, but not all at once: enters row of the array at compute cycle . This skew, the staircase of blue values waiting at the left edge, is what makes everything meet at the right time. Each activation steps one PE to the right per cycle, so it is used by all PEs of its row.
- Accumulate downwards. Each PE multiplies the activation passing through by its weight and adds the product to the partial sum that arrived from the PE above (sky "p"), then passes the new sum down. PE handles row at cycle ; the PE above did so one cycle earlier, so its partial sum is exactly the one needed.
- Drain. After PEs the sum is complete, and leaves the bottom of column (purple), at compute cycle .
With the default problem ( is 5 × 4, is 4 × 4, on 16 PEs), the run takes 15 cycles: 4 to load and 11 to stream, compute and drain. The first result, 25, leaves in cycle 8 and the last in cycle 15. The array does 80 useful MACs, 33.3% of what its PEs could have done in that time; chapter 4 is about where the rest goes.
Change , or : the model re-runs and the animation restarts. More rows of use the same loaded weights again, which is why the TPU holds weights: in inference a weight matrix is reused by every token in a batch.
Maths
Write for the compute cycle (after the load cycles). The left edge of row receives
Every register is a flip-flop, so a value latched by PE at the end of cycle reaches PE (activations) or PE (partial sums) in cycle . By induction on , is in PE in cycle ; by induction on , the partial sum for holding terms arrives at PE in the same cycle . So PE computes
and leaves the bottom at . The last result, , leaves at , so a run takes
The tests check every one of these claims against the frames: each product happens in PE at cycle , each result leaves when complete, and equals numpy's matmul.
Code
The heart of the model, cut from src/lib/sa/model.ts: one PE in one compute cycle. prev is the previous cycle's registers, so everything moves one PE per cycle by construction (ws covers weight- and input-stationary, which are the same machine with and swapped):
const wVal = cell[0]![0];
cell[4] = [hIn[0], wVal];
cnt.macs += 1;
cell[2] = [pVal + hIn[0] * wVal, mi, ni, terms + 1];
cnt.regWrites += 1;
if (kk === k - 1) {
out.push([mi, ni, cell[2][0]]);
cnt.writes += 1;
}
The Python reference has the same loop; the TypeScript port reproduces every register of every PE in every cycle exactly, for every problem size the sliders allow.