Output- and input-stationary
The same matrix multiply in three dataflows side by side: what stays put, what moves, and what that costs in memory reads, link hops and register writes.
Loading the animation…
Concept
Chapter 2 held the weights still. That is one of three choices for a matrix multiply, each named after the operand that stays put in each PE (the names come from Chen, Emer and Sze's taxonomy for the Eyeriss accelerator, which adds row-stationary for convolutions; SCALE-Sim, a systolic-array simulator, uses these three for matrix multiplies):
- Weight-stationary (WS): stays; streams in from the left; partial sums move down. A block of PEs.
- Output-stationary (OS): stays. enters row from the left and enters column from the top, both skewed, and they meet in PE at cycle , which adds their product to its own accumulator. Nothing but 8-bit operands moves until the end, when the finished results drain out. An block.
- Input-stationary (IS): stays (PE holds ); the columns of stream in from the left; partial sums move down. It is weight-stationary on the transposed problem . A block.
The animation runs all three on the same 3 × 3 by 3 × 3 problem on one clock, and counts what each moves. When the problem fits the array, all three read every element of and from the buffers exactly once (18 reads) and write each result once (9), and all three make the same number of PE-to-PE hops (45). The difference is what hops. WS and IS pass a partial sum down at every hop between rows: 18 partial-sum hops here. OS keeps its sums in place and moves them only when draining: 9. That matters because partial sums are wide: 8-bit operands accumulate into 32-bit sums. At 128 × 128 × 128, WS moves 91,553,792 bits between PEs and OS 66,584,576, though OS writes its accumulator registers more often (7,331,840 register writes against 5,251,072).
The bigger difference shows when the problem does not fit, which is the normal case: the stationary matrix is the one that gets reused across a whole stream, and the dataflow decides which matrix is tiled, which is re-read, and where partial sums of a split go (chapter 5).
Loading the animation…
Concept
Here is output-stationary with the numbers (switch to input-stationary to watch held and streamed). Every PE's sky "c" is its accumulator: it gains one term per cycle as the blue from the left and the vermillion from above pass through. After the last product the results drain down the columns, one row per cycle.
This animation is checked against RTL. The author's Interview RTL challenge is a parameterised SystemVerilog output-stationary array with internal skew registers. This site's rtl/check_rtl.py runs it in Verilator 5.020 on 3 problems (including full-range signed INT8) and compares every accumulator after every clock edge with this model: 613 values over 44 edges, 0 mismatches. (The RTL reads its results out in parallel rather than draining them, so after the last product the check requires it to hold .) The model page has the details.
Maths
Output-stationary timing. Row of is delayed cycles and column of is delayed cycles, so enters at cycle and reaches PE after more hops, at ; enters at and reaches PE after hops, at the same cycle. The last product, in PE, happens at cycle , so with an -cycle drain
Counting the movement. Reads: every and enters once, . Operand hops: crosses links, crosses : . Draining moves down rows: . For weight-stationary the activation hops are , each 's partial sum makes hops, , and loading moves down rows, . With both totals are hops. The model counts every hop in every frame, and the tests check the counts against these closed forms for every dataflow and shape.
Code
Output-stationary's PE, cut from src/lib/sa/model.ts: both operands arrive from neighbours (or the edges), and only the accumulator stays.
cell[1] = hIn;
cell[2] = vIn;
let acc = prev[mm]![nn]![3]!;
if (hIn !== null && vIn !== null) {
/* c8 ignore next */
if (hIn[2] !== vIn[1]) throw new Error("operands out of step");
cell[4] = [hIn[0], vIn[0]];
cnt.macs += 1;
cnt.regWrites += 1;
acc = [acc[0] + hIn[0] * vIn[0], mm, nn, acc[3] + 1];
}
cell[3] = acc;
The check that the two operands belong to the same never fires: the skew guarantees it, and the tests run every shape.