Task 2 of 5
To walk downhill you need the direction of steepest ascent — the gradient — and then you go the other way. Differentiating the loss is two lines of calculus, and for a model this small there is no reason to reach for anything cleverer than writing them out:
rᵢ = m·xᵢ + c − yᵢ
∂L/∂m = (2/n) · Σ rᵢ · xᵢ
∂L/∂c = (2/n) · Σ rᵢ
Two sums, over the same residuals, in the same order. There is no reason to walk the
data twice: one kernel computes both, holding each residual in a register and pushing it
into two accumulators. What comes back is a 2D output — output: [64, 2],
64 threads wide and 2 rows tall — where row 0 holds the
Σ r·x partials and row 1 the Σ r partials.
Notice where the branch that picks the row goes: after the loop, never
inside it. Every thread then runs the identical straight-line body and only the last
statement differs. At m = c = 0 the gradient must come out
(−6, −8), because on this data ∇L = (2(m − 3), 2(c − 4)).
(−6, −8) at
m = 0, c = 0.sm accumulates r * xs[at], sc accumulates rsm, row 1 returns sc — branch on this.thread.y after the loop2 / 4096Read the residual once and use it twice, exactly as the fused loss kernel reused it:
const r = m * xs[at] + c - ys[at];
sm += r * xs[at];
sc += r;this.thread.y is 0 for the first row and 1 for the second. Put
the choice after the loop so the loop body stays identical for every thread:
if (this.thread.y === 0) {
return sm;
}
return sc;A 2D result is indexed rows[y][x], so rows[0] is
the 64 slope partials and rows[1] the 64 intercept partials. Both
totals then need the same scaling: (2 * total) / 4096.
BlockReduce per accumulator but reads the tile
once, and WGSL does the same with two workgroup arrays. The 2D output is just gpu.js's
spelling of one launch, two results.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.