Repository navigation
Hold each GPU result to the accuracy its specification allows its built-in calls: fixes #344 - #355
Closed
bigmistqke wants to merge 15 commits into
Closed
bigmistqke wants to merge 15 commits into
bigmistqke wants to merge 15 commits into
Conversation
The canon claimed that a CPU target's float result differs from a GPU's by at most a millionth of its size. WGSL allows a built-in function far more than that: 4096 units in the last place for atan and atan2, an absolute error of 2^-11 for sin and cos, 3 + 2|x| units for exp, and no bound at all for determinant or for sin outside [-pi, pi]. Dawn on Mesa's software Vulkan driver gives atan2(1, 1) 55 units from the exact result, which conforms to WGSL and failed the suite. Two facts record the accuracy tables of WGSL, section 15.7.4.1, and of GLSL ES 3.00, section 4.5.1, which leaves trigonometry and determinant without a precision. The exception for 64-bit CPU targets now allows the error WGSL allows each built-in a program calls. A new spec says the evaluation harness passes a GPU whose built-in calls are as accurate as WGSL requires, on GLSL as on WGSL. Its leaves give the bound of each built-in, the carry of each call's bound to the result by running the program again with that call moved, and the rule that a program with an unbounded call runs on the CPU targets alone under the reason "unbounded-builtin". The existing tolerance spec becomes the leaf for the tolerance before any call widens it. Units: fact-wgsl-bounds-the-error-of-each-float-built-in, fact-glsl-es-300-leaves-trigonometry-and-determinant-without-a-precision, exception-a-cpu-target-computes-floats-in-64-bits, spec-a-gpu-as-accurate-as-wgsl-requires-passes-the-evaluation, spec-the-float-tolerance-allows-a-few-units-in-the-last-place, spec-atan-and-atan2-widen-the-tolerance-by-4096-units-in-the-last-place, spec-asin-and-acos-widen-the-tolerance-by-the-worse-of-their-absolute-error-and-atan2s, spec-sin-and-cos-widen-the-tolerance-by-2-to-the-minus-11, spec-tan-widens-the-tolerance-by-the-error-of-sin-over-cos, spec-exp-and-exp2-widen-the-tolerance-by-3-plus-twice-the-argument-in-units-in-the-last-place, spec-log-and-log2-widen-the-tolerance-by-their-absolute-or-relative-bound, spec-pow-widens-the-tolerance-by-the-error-of-exp2-of-y-times-log2-x, spec-sinh-and-cosh-widen-the-tolerance-by-the-error-of-their-exponentials, spec-tanh-widens-the-tolerance-by-the-worse-of-1e-5-and-the-error-of-sinh-over-cosh, spec-the-inverse-hyperbolic-functions-widen-the-tolerance-by-the-error-of-their-logarithm, spec-each-call-widens-the-tolerance-by-what-its-error-moves-the-result, spec-a-call-whose-value-is-not-finite-has-no-bound, spec-a-determinant-has-no-bound, spec-an-unbounded-program-that-names-no-reason-fails-the-run, spec-a-bounded-program-that-names-the-unbounded-reason-fails-the-run See #344
The evaluation harness compared every GPU result with the JavaScript one within a millionth of its size. A GPU that computes atan2 within the 4096 units in the last place WGSL allows failed that, as Dawn on Mesa's software Vulkan driver does for atan2(1, 1). The harness now runs the JavaScript program through a Math that records each call of a built-in such as atan, sin, exp or pow, with its arguments and its value. float-accuracy.ts gives each call the error WGSL's table allows it, as an absolute error at its value, checking each range on the arguments rounded to 32 bits. Each call's bound reaches the result by running the program again with that call's value moved up and down by it, and the tolerance of each component is the millionth of its size plus those moves summed over the calls. A call WGSL gives no bound, a determinant, or a move that leaves a component not finite makes the program unbounded: it fails the run unless it runs on the CPU targets alone, under the new reason "unbounded-builtin" where it has no other, and a program that names that reason without such a call fails too. The result the harness returns is copied before the extra runs, because the JavaScript target writes a vector into the same buffer on every run. The gpu job of the CI workflow no longer continues on error, since the failure it waited on is gone. Units: spec-a-gpu-as-accurate-as-wgsl-requires-passes-the-evaluation, spec-atan-and-atan2-widen-the-tolerance-by-4096-units-in-the-last-place, spec-asin-and-acos-widen-the-tolerance-by-the-worse-of-their-absolute-error-and-atan2s, spec-sin-and-cos-widen-the-tolerance-by-2-to-the-minus-11, spec-tan-widens-the-tolerance-by-the-error-of-sin-over-cos, spec-exp-and-exp2-widen-the-tolerance-by-3-plus-twice-the-argument-in-units-in-the-last-place, spec-log-and-log2-widen-the-tolerance-by-their-absolute-or-relative-bound, spec-pow-widens-the-tolerance-by-the-error-of-exp2-of-y-times-log2-x, spec-sinh-and-cosh-widen-the-tolerance-by-the-error-of-their-exponentials, spec-tanh-widens-the-tolerance-by-the-worse-of-1e-5-and-the-error-of-sinh-over-cosh, spec-the-inverse-hyperbolic-functions-widen-the-tolerance-by-the-error-of-their-logarithm, spec-each-call-widens-the-tolerance-by-what-its-error-moves-the-result, spec-a-call-whose-value-is-not-finite-has-no-bound, spec-a-determinant-has-no-bound, spec-an-unbounded-program-that-names-no-reason-fails-the-run, spec-a-bounded-program-that-names-the-unbounded-reason-fails-the-run, spec-evaluate-recording-returns-the-cpu-result-at-once Fixes #344
The owner of the design ruled on the scope of axiom-a-cpu-target-gives-what-webgpu-gives: WebGPU decides what the CPU targets compute and what rmsl emits, where TSL has no opinion, and bounds no GPU driver. Each GPU target is held to the guarantees of its own specification, WGSL's on WGSL and GLSL ES's on GLSL. The axiom now says so. GLSL ES 3.00 bounds a division, pow, exp, exp2, log, log2, sqrt and inversesqrt as WGSL does, defines sinh, cosh and tanh by formulas, and leaves sin, cos, tan, asin, acos, atan, asinh, acosh, atanh and determinant without a precision. The GLSL fact is renamed to say that it bounds some built-ins, and the evaluation harness spec gains three leaves: the GLSL comparison takes the bounds GLSL ES shares with WGSL, tanh on GLSL has no floor of 1e-5, and a program with a call GLSL ES leaves without a precision is not compared on GLSL. WGSL defines a correctly rounded result as either of the two nearest floats, and GLSL ES leaves the rounding mode undefined, so a GPU need not give an addition the result a CPU target gives at 32 bits, and a division or square root need not be correctly rounded at all. The spec of the float option said "exactly the result a GPU gives"; it now says the CPU target rounds to the nearest float, a result WGSL allows. The exception for 64-bit CPU targets and the body of the f32 rounding spec follow. Two probes read the specifications the facts rest on and check each claim: probes/float-accuracy/wgsl-accuracy-table.ts reads WGSL sections 15.7.4 and 15.7.4.1, and glsl-es-300-precision.ts reads GLSL ES 3.00 sections 4.5.1 and 8.1 from the PDF, pinned by its SHA-256. Each fact names its source, its probe and the probe's result. Units: axiom-a-cpu-target-gives-what-webgpu-gives, fact-wgsl-bounds-the-error-of-each-float-built-in, fact-glsl-es-300-bounds-the-error-of-some-float-built-ins, exception-a-cpu-target-computes-floats-in-64-bits, spec-a-cpu-compile-can-run-a-program-at-64-bit-precision, spec-a-cpu-target-at-f32-rounds-every-float-value-it-computes, spec-a-gpu-as-accurate-as-wgsl-requires-passes-the-evaluation, spec-a-call-glsl-es-bounds-as-wgsl-does-widens-the-glsl-tolerance-alike, spec-tanh-on-glsl-widens-the-tolerance-without-the-floor-of-1e-5, spec-a-program-with-a-call-glsl-es-leaves-without-a-precision-is-not-compared-on-glsl, spec-an-unbounded-program-that-names-no-reason-fails-the-run, spec-a-bounded-program-that-names-the-unbounded-reason-fails-the-run See #344
The harness computed one tolerance per recorded program from WGSL's table and applied it to GLSL and WGSL alike. builtinCallBound now takes the target: on GLSL, a call of sin, cos, tan, asin, acos, atan, atan2, asinh, acosh or atanh has no bound, and tanh takes the error of sinh / cosh without WGSL's floor of 1e-5. accuracyOf carries the bounds of each target separately and returns a tolerance per target, null where the target leaves the result open. A program WGSL leaves unbounded still has to run on the CPU targets alone. One GLSL ES leaves unbounded is evaluated on GLSL, so a refused shader is still reported, and its result there is not compared. A test with a stand-in GLSL compiler that gives 0.25 for sin(0.5) passes the run, and the same stand-in for WGSL fails it. Units: spec-a-call-glsl-es-bounds-as-wgsl-does-widens-the-glsl-tolerance-alike, spec-tanh-on-glsl-widens-the-tolerance-without-the-floor-of-1e-5, spec-a-program-with-a-call-glsl-es-leaves-without-a-precision-is-not-compared-on-glsl See #344
… GLSL inverse open The exception for 64-bit CPU targets allowed the error of each built-in call, where the evaluation harness spec carries that error through the rest of the program: atan(x) * 1000 - 785 differs by a thousand times the bound of atan. The exception now says the error reaches the result as the program carries it. GLSL ES 3.00 describes inverse only as the inverse of its matrix, with no equation, and undefined where the matrix is nearly singular, so by the rule of section 4.5.1 it has no precision, as asinh has none. The GLSL fact and the leaf that keeps such a program from the GLSL comparison now name it. The geometric functions such as normalize stay bounded: the section names them among the functions that inherit their error. The GLSL probe reads section 8.6 too and checks the wording of inverse. Units: exception-a-cpu-target-computes-floats-in-64-bits, fact-glsl-es-300-bounds-the-error-of-some-float-built-ins, spec-a-program-with-a-call-glsl-es-leaves-without-a-precision-is-not-compared-on-glsl See #344
Three defects a review of the harness found. A bound that comes out NaN, as for pow(0, 2), asin(2) or acosh(0.5), passed the check for a missing bound, which compared with Infinity only. Where the rest of the program hid the NaN, as step(-1, pow(a, 2)) does at a = 0, the program was compared with the base tolerance, though WGSL gives the call no bound. A bound that is not below Infinity is now no bound. The bounds of asin, acos and the inverse hyperbolic functions counted each correctly rounded step of their formula as half a unit in the last place. WGSL lets such a step give either neighbouring float, so it may be off by a whole unit, and a conforming acosh near 1 fell outside the tolerance. Each such step now counts as one unit. A program that reads inverse is now left open on GLSL, which gives it no precision. The tests pin the bounds of asin, asinh, acosh and atanh to the formula each spec states, check a conforming acosh near 1, and find a determinant inside a Fn with a layout, which the walk reaches only through the call's definition. Units: spec-pow-widens-the-tolerance-by-the-error-of-exp2-of-y-times-log2-x, spec-a-call-whose-value-is-not-finite-has-no-bound, spec-asin-and-acos-widen-the-tolerance-by-the-worse-of-their-absolute-error-and-atan2s, spec-the-inverse-hyperbolic-functions-widen-the-tolerance-by-the-error-of-their-logarithm, spec-a-determinant-has-no-bound, spec-a-program-with-a-call-glsl-es-leaves-without-a-precision-is-not-compared-on-glsl See #344
…s no exp2 JavaScript's Math has no exp2, so the JavaScript target computes exp2(x) as Math.pow(2, x), the same call pow(2, x) makes. The evaluation harness learns the built-in calls of a program from its JavaScript run, so in a program that calls both exp2 and pow it cannot tell the two apart, and gives such a call the bound of pow(2, x), the wider one, so that a GPU that meets WGSL still passes. A fact records the platform, with a probe that lists the functions of Math in Node 24 and compares Math.pow(2, x) with 2 ** x. An exception under the exp and exp2 spec states the wider bound, and a new leaf holds the basic case: exp, and exp2 in a program without pow, widen the tolerance by 3 + 2|x| units in the last place. Units: fact-javascript-math-has-no-exp2, exception-exp2-takes-the-bound-of-pow-in-a-program-that-calls-pow, spec-exp-or-exp2-without-pow-widens-the-tolerance-by-3-plus-twice-the-argument See #344
The test of exp and exp2 now cites the leaf for their basic case. A new test checks that exp2(a) + pow(b, 2) is widened by the bounds of pow(2, a) and pow(b, 2), wider than the bound of exp2 alone, and that exp2(a) by itself keeps its own bound. Units: exception-exp2-takes-the-bound-of-pow-in-a-program-that-calls-pow, spec-exp-or-exp2-without-pow-widens-the-tolerance-by-3-plus-twice-the-argument See #344
…he bug it shows A GPU computes the argument of a built-in in 32 bits, rounding at every step, where the evaluation harness checks the range of a bound on the argument of the JavaScript run, computed in 64 bits and rounded once. The two can lie on opposite sides of a range's end. A probe in plain WGSL on Dawn on Metal, on an Apple M1, computes 3.0004 + 0.1411925918 as 3.14159274, above pi, where the sum rounded once is 3.14159250, below it. A fact records the measurement and its probe, and a bug inside the sin and cos spec records that the harness then gives sin of that sum the bound 2^-11, though WGSL gives it none. The evaluation harness spec now says a bound's range is a range of the argument the GPU computes. A program WGSL leaves unbounded is kept off the GPU targets, as the existing spec for CPU-only reasons words it: such a program runs on the JavaScript target only. The GLSL fact names the sections its probe reads, 4.5.1, 8.1, 8.5 and 8.6, and states that section 8.5 gives normalize by an equation; the probe checks that too. The exp2 exception says the JavaScript target computes exp2 with Math.pow as Math has no exp2, as its fact shows. Units: fact-a-gpu-computes-an-argument-in-32-bits-at-each-step, bug-the-harness-checks-the-range-of-sin-on-the-cpus-argument, spec-a-gpu-as-accurate-as-wgsl-requires-passes-the-evaluation, spec-an-unbounded-program-that-names-no-reason-fails-the-run, fact-glsl-es-300-bounds-the-error-of-some-float-built-ins, exception-exp2-takes-the-bound-of-pow-in-a-program-that-calls-pow See #353 See #344
The test asserts what the sin and cos spec claims: sin(a + b) at a = 3.0004 and b = 0.1411925918 has no bound, because a GPU computes the argument as 3.14159274, above pi. The harness gives it a bound, so the test is marked to fail until #353 is fixed. The testing README says that "unbounded-builtin" keeps a program off the GPU targets. Units: bug-the-harness-checks-the-range-of-sin-on-the-cpus-argument See #353 See #344
…d the bug that it is not The test suite evaluates every program it records on every target, and a reason a test names keeps a program off the GPU targets only. WASM needs no GPU, so a new leaf states that such a program is compared on WASM as any other is, under the same rule for programs WASM refuses. The harness compares on WASM only the programs that name no reason, which a bug inside the leaf records. Units: spec-a-program-kept-off-the-gpu-is-compared-on-wasm, bug-the-harness-skips-wasm-for-a-program-kept-off-the-gpu-targets See #354
… test The test records a program kept off the GPU targets whose JavaScript and WASM compiles give results of opposite signs, and expects the harness to report the difference. The harness skips WASM for such a program, so the test is marked to fail until #354 is fixed. Units: bug-the-harness-skips-wasm-for-a-program-kept-off-the-gpu-targets See #354
commit: |
The probes for the WGSL and GLSL ES accuracy facts read their sources with extractors written for them: a PDF reader that walked the page tree and the fonts' ToUnicode maps, and regular expressions over the HTML. They now read the GLSL ES 3.00 PDF with unpdf 1.8.1, a packaging of pdf.js, and the WGSL specification with cheerio 1.2.0, both added as development dependencies, so the probes keep only locating the passages and checking each claim of their facts. The GLSL probe finds a passage whatever the PDF puts between the characters of its first and last words, so it no longer depends on how one extractor spaces them, and records the version of unpdf. Every check of both probes holds as before. The WGSL table reads the same 92 rows, and their text no longer carries the heading that followed the last row. Units: fact-wgsl-bounds-the-error-of-each-float-built-in, fact-glsl-es-300-bounds-the-error-of-some-float-built-ins See #344
A review of the evaluation harness found that a call GLSL ES gives no precision, such as sin, kept the whole result from the GLSL comparison, components the call cannot reach included. The owner of the design ruled that only the components such a call moves go uncompared on GLSL, and the rest keep their tolerance. A component moves when it changes as the JavaScript run gives the call NaN, positive infinity or negative infinity in place of its value: NaN reaches what the arithmetic carries, and the infinities reach what a comparison decides. The GLSL leaf is restated and renamed for it, and the parent spec follows. Whether the GLSL target emits the right built-in for such a call is a question of the emitted source. The carry leaf now says that a move which turns a finite component into one that is not finite, or changes one that is not finite, leaves the program without a bound, and that a component that is not finite and does not change is compared by equality, as before. The parent spec's body says the harness learns determinant and inverse from the helper functions the JavaScript target computes them with, so only the calls a run reaches count. The harness now compares a program kept off the GPU targets on WASM, so the bug that recorded the opposite is retired. Units: spec-a-gpu-as-accurate-as-wgsl-requires-passes-the-evaluation, spec-each-call-widens-the-tolerance-by-what-its-error-moves-the-result, spec-the-components-a-call-without-a-glsl-es-precision-moves-are-not-compared-on-glsl, spec-a-program-kept-off-the-gpu-is-compared-on-wasm, bug-the-harness-skips-wasm-for-a-program-kept-off-the-gpu-targets See #344 See #354
Fixes from a review of the evaluation harness. The JavaScript target computes determinant and inverse with helper functions such as _mat2x2det. The harness now renames each helper in the compiled source and calls it through a recorder, as it calls Math, so a determinant or inverse counts as a call only where the run reaches it. A determinant in a branch the run does not take no longer makes the program unbounded. On GLSL, a call GLSL ES gives no precision, which now includes inverse, is replaced in turn by NaN, positive infinity and negative infinity, and each component that changes is left open, with an infinite tolerance. The other components are compared with their tolerance. An open component is not compared at all, so a NaN from the driver passes there. On WGSL, such a call still leaves the whole program unbounded. A component that is not finite before a move is no longer counted as moved to a value that is not finite. It leaves the program unbounded only where the move changes it. The WASM comparison covers every recorded program, a program kept off the GPU targets included, as does the check of the list of known WASM refusals. The failure summary reads "Checked N recorded programs; M failed", as a failure may be a missing reason rather than a disagreement with the CPU. The harness also does less work. Moved runs are shared between the two targets, count the calls rather than record them, and are skipped for a program kept off the GPU for a reason other than "unbounded-builtin". The program is built once, by the compile, whose roots the harness walks to tell pow from exp2, and evaluateJS and the recording run share one compile path. Units: spec-each-call-widens-the-tolerance-by-what-its-error-moves-the-result, spec-a-determinant-has-no-bound, spec-the-components-a-call-without-a-glsl-es-precision-moves-are-not-compared-on-glsl, spec-a-program-kept-off-the-gpu-is-compared-on-wasm, spec-an-unbounded-program-that-names-no-reason-fails-the-run Fixes #354 See #344
This was referenced Oct 16, 2026
Contributor
Author
This was referenced Oct 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
The test suite evaluates every program a test records on every target, and compares each GPU result with the JavaScript one. Every result is allowed a base tolerance of a millionth of its size, with a floor of 1e-6 near zero: 8 to 17 units in the last place (ULP) of a 32-bit float. The GPU specifications allow their built-in functions more:
atanandatan2an error of 4096 ULP,sinandcosan absolute error of 2⁻¹¹ on [−π, π], andexp3 + 2·|x| ULP (WGSL §15.7.4.1). It gives some calls no bound at all, such assinoutside [−π, π],atan2(0, 1)anddeterminant.determinant. The inverse hyperbolic functions andinversefall under the same rule.So a GPU that conforms to its specification can exceed the base tolerance and fail the suite. Dawn on Mesa's software Vulkan driver (lavapipe), which the
gpujob of CI runs WGSL on, givesatan2(1, 1)as 0.7853949069976807 (#344). That is 55 ULP from the exact result: within WGSL's 4096, and outside the base tolerance.What changes
The evaluation harness (
src/testing/shader-eval.ts) holds each GPU result to the accuracy its own target's specification allows the program's built-in calls: WGSL's on WGSL, and GLSL ES's on GLSL.rmsl's two-argument
atanis WGSL'satan2.Bounding each call
The harness runs the program compiled to the JavaScript target through a
Maththat records each call of a built-in such asatan,sin,exporpow, with its arguments and its value.src/testing/float-accuracy.tsgives each call the error its target's specification allows, as an absolute error bound at that call. Only built-ins the JavaScript target computes withMathwiden the tolerance. A built-in it computes from basic operations, such asnormalize, keeps the base tolerance (#351). The JavaScript target computesdeterminantandinversewith helper functions such as_mat2x2det, which the specifications leave without a bound. The harness calls each helper through a recorder too, so a determinant or an inverse counts only where the run reaches it.On GLSL,
exp,exp2,log,log2,pow,sinhandcoshtake the same bound as on WGSL.tanhtakes the error ofsinh(x) / cosh(x)without the absolute floor of 1e-5 that WGSL adds.Carrying each call's error to the result
The harness runs the program again twice per call, with that call's value moved up and then down by its absolute error bound. For each result component, it takes the larger of the two changes. The tolerance of the component is the base tolerance plus those changes summed over the calls. So
atan(x) * 1000 - 785gets a thousand times the bound ofatan, andsin(x) + 5gets the bound ofsin. Calls are moved one at a time, so two calls whose errors combine non-linearly can exceed the tolerance.Unbounded programs
A program WGSL leaves unbounded fails the run unless it is kept off the GPU targets. It is unbounded when it has a call:
sin(4), oratan2(0, 1), whose y is not normal;determinant, where the run reaches it;or when moving a call's value turns a finite result component into one that is not finite, or changes one that is not finite.
A program is kept off the GPU targets by the third argument of
evaluateRecording, aCpuOnlyReason, and then runs on the JavaScript and WASM targets only. WASM needs no GPU, so it compares every recorded program. The reason"unbounded-builtin"is for a program that names no otherCpuOnlyReason. A program that names"unbounded-builtin"without such a call also fails the run.Programs GLSL ES leaves without a precision
A program that calls
sin,cos,tan,asin,acos,atan,asinh,acosh,atanh,determinantorinverseis evaluated on GLSL, so a shader the driver refuses is still reported. The components of its result that such a call moves are not compared there, and the other components keep their tolerance. A component moves when it changes as the JavaScript run gives the call NaN, positive infinity or negative infinity in place of its value: NaN reaches what the arithmetic carries, and the infinities reach what a comparison such asstepdecides.exp2besidepowJavaScript's
Mathhas noexp2, so the JavaScript target computesexp2(x)asMath.pow(2, x). That is the same callpow(2, x)makes, so a recordedMath.pow(2, x)cannot tell which of the two made it. In a program that calls both, a call ofexp2takes the bound ofpow(2, x). WGSL definespow(x, y)asexp2(y * log2(x)), so that bound is the bound ofexp2plus the error oflog2(2)carried through it, and is the wider one.CI
A failure in the
gpujob of.github/workflows/test.ymlnow fails CI.Migration
A test that records a program WGSL leaves unbounded now fails its run, unless the program names a
CpuOnlyReason, any of them. No program the suite records today is unbounded: the full run below passes. The failure names the test:The fix is to name the reason:
A program that names
"unbounded-builtin"and makes no unbounded call fails withnames "unbounded-builtin" but makes no built-in call without a bound. The fix is to remove the reason.Code outside the test harness is unaffected.
The canon
At 32 bits, a CPU target gives an addition, subtraction, multiplication, division or square root its exact result rounded to the nearest float. That is one of the results WGSL allows a GPU, and a GPU may round the other way: WGSL §15.7.4 defines a correctly rounded result as either neighbouring float. The base tolerance covers that one unit.
axiom-a-cpu-target-gives-what-webgpu-givesstates that WebGPU decides what the CPU targets compute and what rmsl emits, and places no bound on a GPU driver.Units added:
fact-wgsl-bounds-the-error-of-each-float-built-infact-glsl-es-300-bounds-the-error-of-some-float-built-insfact-javascript-math-has-no-exp2fact-a-gpu-computes-an-argument-in-32-bits-at-each-stepspec-a-gpu-as-accurate-as-wgsl-requires-passes-the-evaluation, under which these specs sit:spec-atan-and-atan2-widen-the-tolerance-by-4096-units-in-the-last-placespec-asin-and-acos-widen-the-tolerance-by-the-worse-of-their-absolute-error-and-atan2sspec-sin-and-cos-widen-the-tolerance-by-2-to-the-minus-11, withbug-the-harness-checks-the-range-of-sin-on-the-cpus-argument(The evaluation harness checks a built-in's range on the CPU's argument, which can lie on the other side of the range than the GPU's #353)spec-tan-widens-the-tolerance-by-the-error-of-sin-over-cosspec-exp-and-exp2-widen-the-tolerance-by-3-plus-twice-the-argument-in-units-in-the-last-place, under which sitspec-exp-or-exp2-without-pow-widens-the-tolerance-by-3-plus-twice-the-argumentandexception-exp2-takes-the-bound-of-pow-in-a-program-that-calls-powspec-log-and-log2-widen-the-tolerance-by-their-absolute-or-relative-boundspec-pow-widens-the-tolerance-by-the-error-of-exp2-of-y-times-log2-xspec-sinh-and-cosh-widen-the-tolerance-by-the-error-of-their-exponentialsspec-tanh-widens-the-tolerance-by-the-worse-of-1e-5-and-the-error-of-sinh-over-coshspec-the-inverse-hyperbolic-functions-widen-the-tolerance-by-the-error-of-their-logarithmspec-each-call-widens-the-tolerance-by-what-its-error-moves-the-resultspec-a-call-whose-value-is-not-finite-has-no-boundspec-a-determinant-has-no-boundspec-an-unbounded-program-that-names-no-reason-fails-the-runspec-a-bounded-program-that-names-the-unbounded-reason-fails-the-runspec-a-call-glsl-es-bounds-as-wgsl-does-widens-the-glsl-tolerance-alikespec-tanh-on-glsl-widens-the-tolerance-without-the-floor-of-1e-5spec-the-components-a-call-without-a-glsl-es-precision-moves-are-not-compared-on-glslspec-a-program-kept-off-the-gpu-is-compared-on-wasmUnits changed:
axiom-a-cpu-target-gives-what-webgpu-givesexception-a-cpu-target-computes-floats-in-64-bitsspec-a-cpu-compile-can-run-a-program-at-64-bit-precisionspec-a-cpu-target-at-f32-rounds-every-float-value-it-computesspec-the-float-tolerance-allows-a-few-units-in-the-last-place, which covers the base tolerance and sits underspec-a-gpu-as-accurate-as-wgsl-requires-passes-the-evaluationEvidence
Probes
Each fact has a probe in
probes/float-accuracy/that reads its source or measures its platform again, and checks every claim the fact makes. Each probe's output is committed inprobes/float-accuracy/results/, andnode probes/float-accuracy/<probe>.tsruns it again.wgsl-accuracy-table.tsreads WGSL §15.7.4 and §15.7.4.1, in the Candidate Recommendation Draft of 21 September 2026.glsl-es-300-precision.tsreads GLSL ES 3.00 §4.5.1, §8.1, §8.5 and §8.6 from the Khronos PDF, pinned by its SHA-256.javascript-math-has-no-exp2.tslistsMathin Node 24.12.0.f32-argument-crosses-pi.tscomputes3.0004 + 0.1411925918in plain WGSL on Dawn on Metal (Apple M1) and reads back 3.14159274, above π. The same sum computed in 64 bits and rounded once is 3.14159250, below π.Tests
In
src/canon/units-tools.test.ts:atan2(1, 1), and the run passes. With a value 5000 ULP off, the run fails.sin(0.5), and the run passes. A fake WGSL compiler that returns 0.25 fails it.In
src/testing/shader-eval.test.ts, under "tolerance of a built-in call":acoshnear 1, rounding every step away from the exact result, stays within the tolerance.vec2(sin(a), a.mul(4))leaves only its first component open, and that each of NaN, +∞ and −∞ alone reaches a component the other two miss.Runs
pnpm test, with WGSL on Dawn on Metal (Apple M1) and GLSL on WebGL 2 through SwiftShader: 3319 passed, 12 expected failures, 56 skipped.gpujob of CI, with WGSL on Dawn on lavapipe and GLSL on WebGL 2 through SwiftShader, passes (run 38901418550): 3265 passed, 12 expected failures, 110 skipped. "computes math free functions", the test lavapipe'satan2(1, 1)failed, passes there.pnpm test:fastpasses, and so does thechecksjob of CI.pnpm canon checkreports the same 4 findings as main (Should canon check run in CI, given the GPU exceptions no test can pin? #346), andpnpm canon logis clean.Follow-ups
normalizeandrefract, keep the base tolerance, though WGSL lets each take the error of its formula, which can exceed it on some inputs.sinoratan(y, x), is not checked by comparing values; it can be checked on the emitted source.Fixes #344
Fixes #354