Task 2 of 6

Randomness Without a Random Number Generator

The rule that makes this a model of temperature rather than a downhill slide needs a coin toss per spin per sweep. On a CPU you reach for Math.random() without thinking. Think about it here: an ordinary generator is a stream — one hidden state, advanced once per call, so call number 900 depends on call number 899. That is a sequential dependency, and it is the same one Thinking in Parallel rules out. There is no ordering between 16,384 threads and nothing they can share. gpu.js is blunt about it too: the WebGPU backend refuses Math.random at compile time, and on WebGL a kernel that calls it is no longer reproducible.

So stop asking for a stream and ask for a function: u = hash(x, y, seed). Same thread, same sweep, same number — every time, on every backend. Different thread, unrelated number. No state, no ordering, and a run you can replay exactly, which is what makes a stochastic simulation debuggable at all. This is not a compromise for gpu.js's benefit; it is what production GPU Monte Carlo does.

A hash has one job: make the output look nothing like the input. Three moves do it. The fold multiplies x, y and seed by unrelated odd constants and adds them, which spreads the three inputs across sixteen bits. The squaring step is the only nonlinear one, and without it a hash is just an affine map: neighbouring cells would come out a fixed distance apart, which is not randomness, it is a ramp. The byte swap moves the high eight bits down where the next multiply can reach them. Every intermediate stays under 2²⁴ — the largest integer a 32-bit float holds exactly — so WebGPU, WebGL and the CPU compute the identical value and nothing about your run depends on which one you got.

Goal: fill a 128×128 grid with hash(x, y, seed) in [0, 1), using no Math.random anywhere.

Requirements

Hint 1 — the fold, and why the seed belongs in it

Three odd constants, one modulo:

let h = (x * 1103 + y * 2749 + seed * 3571) % 65536;

Leave seed out and the field is the same 16,384 numbers on every sweep — the simulation would take one step and then repeat it forever. The seed is what makes this a sequence of fields rather than one field.

Hint 2 — the two mixing moves

Squaring is the nonlinear part. h % 2048 keeps the value small enough that q * q stays under 2²⁴ and the arithmetic is still exact:

let q = h % 2048;
h = (h + q * q) % 65536;

The swap exchanges the high and low bytes. Note which side gets the multiply — h % 256 is the LOW byte, so it is the one that has to move UP:

h = (h % 256) * 256 + Math.floor(h / 256);
Hint 3 — the whole body
let h = (x * 1103 + y * 2749 + seed * 3571) % 65536;
let q = h % 2048;
h = (h + q * q) % 65536;
h = (h % 256) * 256 + Math.floor(h / 256);
h = (h * 253 + 30011) % 65536;
q = h % 2048;
h = (h + q * q) % 65536;
return h / 65536;

Two rounds, not one — and the reason is not where you would look for it. Inside a single field one round is already fine: neighbouring cells measure 0.021 against the two-round hash's 0.032, which is the same noise. The damage is between consecutive seeds. One squaring plus one multiply is still so nearly affine that hash(x, y, k) and hash(x+1, y+1, k+1) correlate at 0.30, against 0.0070.021 with two rounds — and that is precisely the pair the next task puts side by side, a red cell drawing at seed 2k and the black neighbour that reads it drawing at 2k + 1.

Same idea elsewhere

Counter-based randomness — hash a coordinate instead of advancing a stream — is the standard on every parallel platform: Random123 / Philox in CUDA, curand's counter-based generators, and essentially every shader that needs noise. The reason is the same everywhere: a stream forces an order on things that have none, while a hash gives every thread an independent draw for free and makes the whole run reproducible. Keeping the arithmetic inside the exactly-representable integer range is the other half of the trick, and it is why real GPU hashes are written in integer types rather than floats.

All tasks in The Ising Model: Colour to Break the Race

  1. What a Flip Would Cost
  2. Randomness Without a Random Number Generator
  3. Everyone at Once Is Wrong
  4. Colour the Lattice
  5. Two Halves Make a Sweep
  6. Drag the Temperature Across Tc

This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.