Task 3 of 6
The last task launched 90 threads to compute at most 8 cells. Every thread not on the diagonal woke up, read a value and wrote it straight back out — 1,440 thread-launches across the whole sweep to fill 72 cells. Correct, and embarrassing.
The fix is to launch the diagonal itself: output: [len], one thread per cell
that actually exists. The price is that a thread no longer arrives knowing its coordinates.
It gets this.thread.x — a number from 0 to len − 1 —
and has to work out which matrix cell that is. Two lines, and they are the fiddly heart of
every wavefront implementation ever written:
// walk DOWN the diagonal, one row per thread
const i = iStart + this.thread.x;
// and j is whatever makes i + j come to d
const j = d - i;
iStart is the row where the diagonal enters the matrix, and it moves. Early
on it is row 1 — the top edge — and the diagonal runs out at the left edge, where
j would fall below 1. Later the top edge stops mattering and the
right edge takes over: j can never exceed |B|, so the
diagonal cannot start any higher than row d − |B|. Hence
iStart = max(1, d − |B|) and iEnd = min(|A|, d − 1) — handed to you
below, because getting them wrong is a JavaScript bug, while getting the two lines
above wrong is a GPU bug that reads whatever happens to be next to the matrix.
len threads,
each computing exactly one cell of anti-diagonal d — and pull diagonal 10 out
of a half-filled matrix with it.i = iStart + this.thread.xj = d - idynamicOutput: true so setOutput([len]) can resize it per diagonaliStart and len already keep i inside
1 … |A| and j inside 1 … |B|. That is what they
are for: the launch shape does the clipping, so the kernel body is nothing but
the recurrence.
Thread 0 owns the cell nearest the top of the matrix — the smallest row index on
the diagonal, which is iStart. Thread len − 1 owns the one
furthest down. Get that backwards and every value is right but in the wrong order.
dynamicOutput: true is what makes a kernel resizable;
diagonal.setOutput([len]) then sets the thread count for the next call, the
same move the halving ladder in Reductions makes on its way down.
blockIdx * blockDim + threadIdx, WGSL's
global_invocation_id and Metal's thread_position_in_grid all hand
you a flat number and leave the geometry to you. Launching exactly the work that exists —
rather than a rectangle with a guard in it — is the same instinct behind compacted launches,
persistent-thread kernels and indirect dispatch.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.