Task 4 of 6
The cure is a chessboard, and if you have been through Iterative Linear Solvers you have already met it: red-black Gauss-Seidel colours a grid in exactly this way, for exactly this reason. One idea in two costumes. There it rescues a solver that is sequential by construction; here it repairs a Monte Carlo update that is silently wrong when it is parallel. Both times the argument is one sentence — the stencil reads only the four direct neighbours, and on a chessboard every direct neighbour of a red square is black.
So call a cell red when (x + y) is even and
black when it is odd, and update all 8,192 reds at once. No red cell reads
another red cell, so nothing a red thread looked at can move while it is deciding: the
ΔE it computed is exact, not an estimate, and the flip it accepts is the flip
Metropolis meant. Then do the blacks, reading the reds that were just written. Two data-parallel
half-sweeps, and the race is gone — not mitigated, gone.
Black cells are not "skipped" in the red half. Every thread still writes its own cell; a black thread writes back the value it already had, ready for the half-sweep that is about to need it exactly as it is.
(x + y) % 2 === 0 take the Metropolis decision, every other cell comes through
untouched.const parity = (x + y) % 2; — a boolean in a kernel variable does not compile on WebGL1 (black) return their spin unchangeddE ≤ 0 flip unconditionallydE > 0 flip when u < Math.exp(-dE / temperature)The natural spelling is a boolean, and it is the one thing gpu.js cannot do:
const isRed = (x + y) % 2 === 0; // throws on WebGL
The GL backend has no way to store a bool in a kernel variable, so it fails at
shader-compile time with cannot convert from 'bool' to 'lowp float' — while the CPU
backend runs it happily, which is how this reaches production. Keep the number:
const parity = (x + y) % 2; // 0 or 1
if (parity !== 0) return spin;Two exits, in this order:
if (dE <= 0) return -spin;
if (u < Math.exp(-dE / temperature)) return -spin;
return spin;
The first line is technically implied by the second — u is always below 1 and
exp(−dE/T) is at least 1 whenever dE ≤ 0, so the test would pass
anyway. Write it out regardless: it is the rule, and it says that a flip which lowers the
energy is never a gamble.
The parity guard comes first, so a black thread never computes a neighbour sum it is not going to use:
const spin = s[y][x];
const parity = (x + y) % 2;
if (parity !== 0) return spin;
const sum = s[y][(x + 1) % n] + s[y][(x + n - 1) % n]
+ s[(y + 1) % n][x] + s[(y + n - 1) % n][x];
const dE = 2 * spin * sum;
Then the hash (already written for you) and the two exits above.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.