Week 5 · Lesson 4

Matmul and conv from movement

A matrix multiply is a few movement ops, one mul and one reduce. So is a convolution. Once your compiler handles movement ops, it can express the two operations that do almost all the work in a neural network, and the syllabus says that from here it can compile any model.

The syllabus GEMM

The public syllabus opens with a .uir program that multiplies two 256x256 matrices. In our format, where broadcasts are written out with expand, it reads:

%a  = load %A                         # 65536 floats
%b  = load %B                         # 65536 floats
%a3 = reshape %a shape=[256 256 1]    # axes: k, m, 1
%b3 = reshape %b shape=[256 1 256]    # axes: k, 1, n
%ax = expand %a3 shape=[256 256 256]
%bx = expand %b3 shape=[256 256 256]
%p  = mul %ax %bx                     # p[k][m][n] = a[k][m] * b[k][n]
%c  = reduce %p op=add pop=1          # c[m][n] = sum over k, in order k = 0, 1, ...

Both inputs are lifted into a cube with axes (k, m, n). Each one is missing an axis, so expand repeats it along that axis: a is the same for every n, b the same for every m. The mul fills the cube with every product a[k][m] · b[k][n], and reduce pop=1 folds away the leading axis, k, leaving c[m][n]. Step through it on a 2x2 by 2x3 case.

The syllabus stores A transposed

reshape %a shape=[K M 1] reads the buffer as K rows of M. For the product to be A @ B, row k of that buffer must hold column k of A: the buffer holds A transposed. Lesson 0.5 met the same fact from the other side. If your A arrives row-major as M x K, the syllabus form silently computes Aᵀ @ B instead, with the right shape and wrong values.

The general form

When A arrives as M x K, row-major, add one permute to move k to the front. The reduce still folds the first axis, so whatever you want to sum over has to be first:

# %A, %B: loaded values (load the buffers first: memory can't be reshaped)
%a3 = reshape %A shape=[M K 1]
%aT = permute %a3 order=[1 0 2]       # K M 1
%ax = expand %aT shape=[K M N]
%b3 = reshape %B shape=[K 1 N]
%bx = expand %b3 shape=[K M N]
%p  = mul %ax %bx
%C  = reduce %p op=add pop=1          # M N

The cube is never stored. It's a description: element (k, m, n) of %p is one multiply of two loaded values. Lesson 5.5 shows how rangeify turns this description into loops, with the cube gone: one loop over the outputs, one over k.

A matmul from movement ops

Slot 0 holds A, 2x3, row-major. Slot 1 holds B, 3x2, row-major. Store C = A @ B (2x2, row-major) into slot 2, using only load, movement ops, expand, one mul, one reduce and a store. No hand-written range. The starter is the syllabus form, which assumes A is stored transposed.

A convolution is shifted windows and a reduce

A 1D convolution (in the machine learning sense, which is a correlation) computes y[i] = x[i-1]·w[0] + x[i]·w[1] + x[i+1]·w[2], with zeros past the ends of x. Build it from movement: pad x so every window is full, cut one shifted window per tap with shrink, stack the windows, and reduce over the tap axis.

%p   = pad %x pads=[1 1]              # 7: one zero each side
%s0  = shrink %p 0 shape=[5]          # x[i-1] for every i
%s1  = shrink %p 1 shape=[5]          # x[i]
%s2  = shrink %p 2 shape=[5]          # x[i+1]
%win = stack %s0 %s1 %s2              # 3 5: one row per tap
%w2  = reshape %w shape=[3 1]
%wx  = expand %w2 shape=[3 5]
%pr  = mul %win %wx
%y   = reduce %pr op=add pop=1        # 5

The stack of shifted windows is the im2col view: it turns the convolution into a multiply and a sum over taps, the same shape of computation as the matmul above. With x = 1..5 and w = [1 0 -1], the oracle gives y[0] = 0·1 + 1·0 + 2·(-1) = -2 and y[4] = 4·1 + 5·0 + 0·(-1) = 4.

A same-size conv1d

Slot 0 holds x (6 f32), slot 1 holds w (3 f32). Store y[i] = x[i-1]·w[0] + x[i]·w[1] + x[i+1]·w[2] into slot 2 (6 f32), with x read as zero outside its 6 elements. The starter pads on the wrong side.

Where this goes

The syllabus says that after week 5 the compiler "is now capable of compiling any model", splitting it into kernels and printing C. What's left is making it learn and making it fast: week 6 adds autodiff and starts timing kernels, weeks 7 and 8 bring threads, GPUs, memory hierarchies and fast GEMMs, and weeks 9 and 10 run real models. Every one of those builds on the ops in this lesson.

Check yourself

In the syllabus GEMM, %p has shape (256, 256, 256). What shape is %c = reduce %p op=add pop=1? Write the sizes separated by a space.

pop=1 drops one leading axis.

(256, 256): the leading axis, k, is folded away, leaving m and n.

The syllabus form reshapes slot 0 to [K M 1] and reduces over the first axis. What must slot 0 hold for the result to be A @ B?

Row k of the buffer becomes the k-th plane of the cube.

Plane k needs A[m][k] for every m, which is column k of A. Stored as rows, that's A transposed.

A = [[1 2] [3 4]] is stored transposed as [1 3 2 4], and B = [[5 6 7] [8 9 10]]. What is c[1][2] from the syllabus form?

It's row 1 of A dotted with column 2 of B.

3·7 + 4·10 = 61. The oracle agrees.

Why does our version of the syllabus program need expand before the mul?

Try mul on shapes (2, 2) and (1, 2).

The oracle rejects it: "ALU operands must share one shape (no implicit broadcast: use expand)". And expand copies nothing: it only changes which index each element reads.

x has 5 elements and the kernel has 3 taps. After pad %x pads=[1 1] and stacking the three length-5 shrinks, what shape is %win?

stack adds a new leading axis of size 3.

(3, 5): one row per tap, one column per output. With x = 1..5, summing over taps gives [3 6 9 12 9].

With x = 1 2 3 4 5, w = 1 0 -1 and pads=[1 1], what is y[0]?

The window for y[0] is 0, 1, 2: one padded zero, then the first two elements.

0·1 + 1·0 + 2·(-1) = -2.