§ 03

§ 03 · Matrix math

Matrix multiplication

Rows times columns, one multiply-accumulate at a time

To multiply two matrices, you pair every row of the first with every column of the second. Each pairing gives one number of the answer.

That simple rule is the most important computation in modern AI, and it’s the work a Pearl miner does. This page shows how it works, how much it costs, and the “slab by slab” view that Pearl’s mining tickets are built on.

Try it

Hover over any cell of the answer to see the row and column that make it. Then step through the whole multiplication.

Matrix multiplication

Each cell of C is one row of A times one column of B

B

A

C = A·B

Hover or focus any cell of C, or press Step, to see which row and column make it.

Multiply-accumulates 27/ 27 n³ = 3 × 3 × 3

Toy sizes Real jobs are thousands of rows wide. A Pearl GPU does exactly this, with at least 1,024 terms per cell and entries from −64 to 64, and every term is one multiply-accumulate.

Runs in your browser · nothing is sent src/lib/miner.ts · matMul

What’s the rule?

To get the cell in row i and column j of C = A·B, walk along row i of A and down column j of B together. Multiply each pair, and add everything up. That’s a dot product.

Cij=∑l=0k−1AilBljC_{ij} \;=\; \sum_{l=0}^{k-1} A_{il}\, B_{lj}
Matrix product
l runs along the shared dimension k: across a row of A, down a column of B.
1−2304−1×21−1340=16−5−812A: 2 × 3B: 3 × 2C: 2 × 21·2 + (−2)·(−1) + 3·4= 2 + 2 + 12 = 16
Row 0 of A and column 0 of B make the top-left cell of C.

The other cells work the same way:

  • Top right: row (1, −2, 3) with column (1, 3, 0): 1 − 6 + 0 = −5.
  • Bottom left: row (0, 4, −1) with column (2, −1, 4): 0 − 4 − 4 = −8.
  • Bottom right: row (0, 4, −1) with column (1, 3, 0): 0 + 12 + 0 = 12.

The shapes have to fit: A’s rows must be as long as B’s columns, k = 3 here. The answer has A’s number of rows and B’s number of columns.

How much work is it?

Each “multiply a pair and add it to the running total” is one multiply-accumulate (MAC). The answer has m·n cells, each a dot product of length k:

work=m⋅n⋅k multiply-accumulates\text{work} \;=\; m \cdot n \cdot k \ \text{multiply-accumulates}

The example above took 2 · 2 · 3 = 12. Real ones are vastly bigger. The certificate of block 130,825 declares its matrices as m = 131,072, k = 8,192 and n = 262,144. A full multiplication of that shape is

131,072×262,144×8,192≈2.8×1014131{,}072 \times 262{,}144 \times 8{,}192 \;\approx\; 2.8 \times 10^{14}
About 281 trillion multiply-accumulates, for one matrix multiplication.

The network as a whole is expected to do about 1.13 × 10²² multiply-accumulates per block at the snapshot difficulty of 40,157,376: some 40 million multiplications of that size. That’s why Pearl’s hashrate is counted in multiply-accumulates per second, not hashes. See hashrate.

Make a guess

Double all three sizes, m, n and k. How much more work is the multiplication?

Think of your answer first, then check it.

Answer Eight times more.

Work is m·n·k, so doubling each one multiplies the work by 2 × 2 × 2 = 8. Matrix multiplication grows with the cube of its size, which is why it dominates the cost of large AI models.

What’s the slab-by-slab view?

The sum along k can be cut into pieces. Split the columns of A and the rows of B into slabs of the same width, multiply each pair of slabs, and add the results. You get exactly the same answer.

A⋅B=A[:,0:r]B[0:r,:]+A[:,r:2r]B[r:2r,:]+⋯A \cdot B \;=\; A_{[:,\,0:r]}\, B_{[0:r,\,:]} \;+\; A_{[:,\,r:2r]}\, B_{[r:2r,\,:]} \;+\; \cdots
Each term multiplies an r-wide slab of A's columns by the matching r-tall slab of B's rows.

A tiny example. Take P = [[1, 2, 0, −1], [3, −1, 2, 1]] and Q = [[2, 1], [0, −1], [1, 3], [−2, 2]], and split k = 4 into two slabs of 2:

Result
First slab (columns 0–1 of P, rows 0–1 of Q) [[2, −1], [6, 4]]
Second slab (columns 2–3 of P, rows 2–3 of Q) [[2, −2], [0, 8]]
Sum = P·Q [[4, −3], [6, 12]]

Why care? Because this is exactly how Pearl watches the work being done. A miner doesn’t just produce the final answer. For each small patch of the output, a tile, it records the running sums after every slab of width r = 128. Those records feed the patch’s lottery ticket.

Here is a tile from Pearl’s toy mining example (k = 16, slabs of 4), with the running sums after each slab and the 32-bit XOR of each set of sums:

after slab 1k = 0…361−6610XORffffffb3→after slab 2k = 4…7−3723−1714XOR0000002d→after slab 3k = 8…11539−695XORffffff9c→after slab 4k = 12…158049−8137XORffffffeb
One 2 × 2 tile of the toy noisy product A′·B′. After each 4-wide slab, the four running sums are XOR-ed into one 32-bit word for the ticket. The last grid is the tile's final answer. Toy numbers, from the research worked example.

Because the ticket depends on the sums along the way, a miner can’t get it by computing the final answer some cheaper way and adjusting it. That’s the subject of rank and noise, and the full ticket recipe is on tickets and target.

Are there faster ways to multiply?

In theory, yes. Mathematicians know algorithms that use fewer than m·n·k multiplications for very large matrices; Strassen’s 1969 method was the first. Pearl’s security argument has to allow for such tricks. They may be one reason for the rank penalty, though Pearl’s own explanation says only that a higher noise rank made each ticket cheaper.

In practice, GPU mining does the plain m·n·k version, split into tiles that fit the chip. That’s the next pages: integers, then GPUs and tiles.

Code: the tile loop in Pearl's prover

Pearl’s prover computes a tile’s running sums in compute_jackpot (zk-pow/src/circuit/chip/jackpot/helper.rs:9-35 at v1.5.2, on GitHub). ll steps through k in slabs of r; the noise is added to each entry before multiplying:

zk-pow/src/circuit/chip/jackpot/helper.rs Rust
for ll in (r..=k).step_by(r) {
    for u in 0..h {
        for v in 0..w {
            for l in ll - r..ll {
                jackpot[u][v] +=
                    (secret_a[u][l] as i32 + noise.a[u][l] as i32) * (secret_b[v][l] as i32 + noise.b[v][l] as i32);
            }
        }
    }
    let xored_tile = jackpot.iter().flatten().fold(0u32, |a, &x| a ^ x as u32);
    let tid = (ll / r - 1) % JACKPOT_SIZE;
    jackpot_msg[tid] = jackpot_msg[tid].rotate_left(LROT_PER_TILE) ^ xored_tile;
}

The innermost line is one multiply-accumulate. Everything after it runs once per slab.

The codex

Suggested

Up and down arrows move through the results, Enter opens one, Escape closes. Anywhere on the site, slash or Command K opens this search, and the square brackets [ and ] go to the previous and next page.