Skip to content

3. Reductions: many values become one

Time: 60 minutes. Prerequisite: offsets and strides. Edit: reduction.py.

An elementwise add reads two values and writes one value at the same position. A reduction combines an entire group into fewer values: sum, maximum and mean are examples. Values must cooperate because one output depends on many inputs.

Trace the arithmetic

Start with [1,2,3,4,5]. Pad to eight elements, then combine neighbors:

level 0:  [1, 2, 3, 4, 5, 0, 0, 0]
level 1:  [  3,    7,    5,    0  ]
level 2:  [      10,          5     ]
level 3:  [             15          ]

Eight values need three pairwise levels (log2(8)). Several pairs at a level can be computed simultaneously. This is a conceptual tree: Triton's actual GPU tree depends on layout, dtype and compiler choices.

Padding has an algebraic meaning

An identity leaves the operation unchanged. Adding zero does not change a sum. Taking max(x, -infinity) leaves any finite x unchanged.

Reduction Identity for padding Why the wrong value fails
sum 0 Padding with 1 changes the sum
maximum negative infinity Padding negative values with 0 invents a larger maximum
minimum positive infinity Padding positive values with 0 invents a smaller minimum
product 1 Padding with 0 makes the entire product zero

For the mean, sum padded values but divide by the logical count. The mean of [1,2,3,4,5] is 15/5=3, not 15/8. This detail will matter again in RMSNorm.

What FP32 accumulation means

A dtype limits which values are representable. In FP16, converting intermediate partial sums back to FP16 can lose information or overflow. The row-sum kernel loads the input, converts values to FP32, reduces in FP32 and returns FP32. Input storage can remain FP16; storage dtype and computation dtype need not match.

Even FP32 is not exact. In FP32 arithmetic, adding 1 to 100,000,000 rounds back to 100,000,000. Then subtracting 100,000,000 yields zero. Reordering the first addition to 100,000,000 + (-100,000,000) yields zero, after which adding 1 yields one. Different reduction orders can therefore produce different answers. Python's ordinary float has more precision, so your Python exercise is about the structure of the tree, not an exact simulation of FP32 rounding.

Implement the exercise

Return every stage, including the padded input and the final one-element list. Copy the input before padding so the caller's list stays unchanged. A one-element input already has its final stage and needs no pairwise step.

python -m learning.explain reduction
python -m learning.check reduction

Translate the idea to GPU code

In _row_sum, tl.program_id(0) chooses a row and tl.arange chooses its columns. tl.sum(values, axis=0) reduces the one-dimensional vector of loaded values into a scalar. The output pointer is OUT + row, not OUT + row * width, because there is only one output per row.

The CUDA example makes the cooperation explicit: warp shuffles combine thread partials; eight warp leaders write into shared memory; the block synchronizes; one warp combines the partials and broadcasts the result. Read that code after completing the Triton version, rather than memorizing shuffle syntax first.

Exit check: draw all stages for [2,-1,4]. State the sum identity, max identity, and correct divisor for a padded mean. Next: softmax.