Skew, fill and drain
Why the inputs arrive as a staircase, how long the array takes to fill and empty, and what that does to utilisation as the matrices grow or stop fitting.
Loading the animation…
Concept
The skew that chapter 2 needed has a price. Row of reaches PE at cycle , so in any one cycle the busy PEs are those on a band of anti-diagonals: the wavefront. It enters at the top-left corner, takes cycles to reach the far corner (the fill), and after the last row of has entered it takes as long again to leave (the drain). Only in between, and only if the stream is long enough, is every PE busy.
With the default 10 rows of through a 4 × 4 array, the fill takes 6 cycles, all 16 PEs are busy only briefly, and the whole run (with the weight load) takes 20 cycles at 50.0% utilisation. Shorten the stream below rows and the array never fills at all.
The skew itself costs hardware too: something has to delay row by cycles. The RTL challenge on the model page uses a chain of registers per row, for the left edge (6 registers for 4 rows); some designs skew in the memory system instead.
Loading the animation…
Concept
Utilisation is the fraction of the array's MAC slots that did useful work. The fill, drain and weight load are a fixed overhead per pass, so the way to amortise them is a long stream. On an 8 × 8 weight-stationary array with its weights all used, one row of gives 4.3%, 8 rows give 26.7%, and 64 rows give 74.4%; reaching 90% takes 198 rows. For a 256 × 256 array, as in the first TPU, the overhead is 766 cycles and 90% takes 6,894 rows. This is why such arrays want big batches (more rows of activations per loaded weight tile), and why the TPU double-buffers its weights: a weight load hidden behind the previous tile's stream costs nothing (chapter 5).
Switch to matrix width to see the other loss. Here 32 and 8 fit, and grows past the array's 8 columns. At the tile fills the array (59.3%); at a second tile is needed for one column, and utilisation drops to 35.6%. It climbs back as that tile fills, and drops again at 17 (43.9%): a matrix dimension just past a multiple of the array size wastes most of a tile. It is one reason model dimensions are usually multiples of a power of two.
Maths
For weight-stationary on a block, PE is busy at compute cycle when it holds a row of : . The busy count at is the number of in the block with ; it rises over the fill, is while (if ), and falls over the drain. Summed over all cycles it is , every MAC once.
Including the load cycles, a pass takes , so with the block filling an array (, )
and needs : for and , 198. When a dimension does not fit, the problem is cut into tiles of at most rows of and columns of , run one after another; tile has and and costs , and
The model runs every tile cycle by cycle and checks the sum of their cycles against this closed form.
Code
The closed form for one pass, cut from src/lib/sa/model.ts (tests check it equals the number of frames the cycle-accurate model produces, for every dataflow and shape):
export function cycles(df: Dataflow, m: number, k: number, n: number): number {
if (df === "ws") return k + (m + k + n - 2);
if (df === "os") return k + m + n - 2 + m;
return k + (n + k + m - 2);
}