Skip to content

Test: run the shaders on a real GPU, not just their references - #30

Open
m96-chan wants to merge 1 commit into
mainfrom
test/29-run-the-wgsl
Open

Test: run the shaders on a real GPU, not just their references#30
m96-chan wants to merge 1 commit into
mainfrom
test/29-run-the-wgsl

Conversation

@m96-chan

Copy link
Copy Markdown
Owner

Closes #29

What was missing

Twelve shaders, eighteen test files, and nothing executed WGSLrequestAdapter and createShaderModule appear in zero of them. Every test exercises a TypeScript reference, so the shader and the reference were two transcriptions of the same intent with nothing holding them together. One name admits it: refTernaryGemvShaderLogic.

The kernels are not in doubt. The model runs in a browser and produces coherent text, which is a stronger signal than any unit test. What was missing is per-kernel verification, and the reason to want it now is fusion — activationelementwise(multiply) in FFN.forwardGated is two dispatches where one would do, and BitLinear.forward runs RMSNorm and Quantize separately despite a comment calling them fused. "Still coherent" cannot tell a correct fusion from a subtly wrong one.

What this covers

Seven shaders — rmsnorm, activation, elementwise, softmax, rope, quantize, dequantize — with D chosen to span the 256-wide workgroup: below it, exactly on it, and not a multiple of it, since the strided loop and the reduction each depend on which.

Five are left: attention, embedding, f32_matmul, ternary_gemm, ternary_gemv. They need packed I2_S data and embedding tables constructed first, which is its own piece of work rather than a footnote to this one.

Two findings

eps in rmsnorm was untested. Removing it entirely still passed — on ordinary input sumSq/D dwarfs it. There is now an all-but-zero row where it is the only thing standing between inverseSqrt and a division by zero.

rope cannot be checked to f32 precision, and the reason is the hardware. Measured on this GPU:

absolute error
sin / cos up to 1.86e-4
f32 epsilon 1.2e-7
pow (measured separately) 2.8e-7

Three orders of magnitude worse than f32, and RoPE calls both per element — which fully accounts for its 3.37e-4. The shader and the reference compute the same expression; I checked. So the tolerance is loosened on measurement, with the number recorded in the test, rather than quietly widened until it went green.

Worth knowing generally: no shader using sin, cos or exp can be validated tighter than this.

Proven to catch damage

Mutating rmsnorm.wgsl to drop the weight multiply fails 4 tests. Dropping a reduction barrier fails too. A harness that only ever passes would be worse than none.

Kept out of npm test

CI is ubuntu-latest with no GPU, and the suite that gates pull requests should be one that can run there. Separate config, separate script (npm run test:gpu), excluded from the default include. It also skips rather than fails when no adapter is present, so it does not punish a contributor without one.

npm test       → 99 passed | 28 skipped   (no GPU needed)
npm run test:gpu → 18 passed              (this machine)
CHROME_PATH=/nonexistent npm run test:gpu → 18 passed (all skipped)

Two environment notes, since both cost time

  • navigator.gpu is absent on about:blank. WebGPU needs a secure context, so the page is served from 127.0.0.1. With that, headless works — which is usually what "WebGPU is unavailable headless" turns out to mean.
  • Playwright's bundled Chromium does not ship WebGPU. The system Chrome does. CHROME_PATH overrides the default /opt/google/chrome/chrome.

Next

Fusing activation + elementwise(multiply) into one dispatch, and RMSNorm + Quantize in BitLinear. Both can now be shown to match what they replace.

Twelve shaders, eighteen test files, and nothing executed WGSL:
`requestAdapter` and `createShaderModule` appeared in zero of them.
Every test exercised a TypeScript reference, so the shader and the
reference were two transcriptions of the same intent with nothing
holding them together. One name admitted it — refTernaryGemvShaderLogic.

The kernels are not in doubt; the model runs in a browser and produces
coherent text, which is a stronger signal than any unit test. What was
missing is per-kernel verification, and the reason to want it now is
fusion: "still coherent" cannot tell a correct fusion from a subtly
wrong one.

Seven shaders are covered — rmsnorm, activation, elementwise, softmax,
rope, quantize, dequantize — with D chosen to span the 256-wide
workgroup: below it, exactly on it, and not a multiple of it. The five
left out (attention, embedding, f32_matmul, ternary_gemm, ternary_gemv)
need packed I2_S and embedding tables built first.

Two findings came out of it.

The eps in rmsnorm was untested: on ordinary input sumSq/D dwarfs it, so
removing it entirely still passed. There is now an all-but-zero row
where it is the only thing preventing a division by zero.

And rope cannot be checked to f32 precision. This GPU's sin and cos
carry up to 1.86e-4 of absolute error — three orders of magnitude worse
than f32 epsilon. `pow` was measured separately and agrees to 2.8e-7, so
the transcendentals are the whole of it. The tolerance says so and
records the number rather than being quietly widened.

Kept out of the default suite. CI is ubuntu-latest with no GPU, and the
suite that gates pull requests should be one that can run there. It also
skips rather than fails when no adapter is present.

Two things about the environment, since both cost time: navigator.gpu is
absent on about:blank because WebGPU needs a secure context, so the page
is served from 127.0.0.1 — with that, headless works, which is usually
what "WebGPU is unavailable headless" turns out to mean. And Playwright's
bundled Chromium does not ship WebGPU; the system Chrome does.

Closes #29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No test ever runs the WGSL

1 participant