Skip to content

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
mainfrom
run/issue-344
Closed

bigmistqke wants to merge 15 commits into
mainfrom
run/issue-344

Conversation

@bigmistqke

@bigmistqke bigmistqke commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

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:

  • WGSL allows atan and atan2 an error of 4096 ULP, sin and cos an absolute error of 2⁻¹¹ on [−π, π], and exp 3 + 2·|x| ULP (WGSL §15.7.4.1). It gives some calls no bound at all, such as sin outside [−π, π], atan2(0, 1) and determinant.
  • GLSL ES 3.00 gives no precision to a built-in it neither lists in its precision table nor defines by an equation (GLSL ES 3.00 §4.5.1). It names the trigonometric functions and determinant. The inverse hyperbolic functions and inverse fall 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 gpu job of CI runs WGSL on, gives atan2(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.

evaluateRecording((a, b) => atan(a, b), [1, 1]); // within 4096 ULP on WGSL; evaluated on GLSL, not compared
evaluateRecording((a) => vec2(sin(a), a.mul(4)), [0.5]); // on GLSL, component 0 open, component 1 compared
evaluateRecording((a) => a.sin(), [4]); // fails the run: WGSL gives sin(4) no bound
evaluateRecording((a) => a.sin(), [4], "unbounded-builtin"); // runs on the JavaScript and WASM targets only

rmsl's two-argument atan is WGSL's atan2.

Bounding each call

The harness runs the program compiled to the JavaScript target through a Math that records each call of a built-in such as atan, sin, exp or pow, with its arguments and its value. src/testing/float-accuracy.ts gives each call the error its target's specification allows, as an absolute error bound at that call. Only built-ins the JavaScript target computes with Math widen the tolerance. A built-in it computes from basic operations, such as normalize, keeps the base tolerance (#351). The JavaScript target computes determinant and inverse with 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, sinh and cosh take the same bound as on WGSL. tanh takes the error of sinh(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 - 785 gets a thousand times the bound of atan, and sin(x) + 5 gets the bound of sin. 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:

  • with arguments outside those for which WGSL bounds that function, such as sin(4), or atan2(0, 1), whose y is not normal;
  • with a value that is not finite as a 32-bit float;
  • of 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, a CpuOnlyReason, 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 other CpuOnlyReason. 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, determinant or inverse is 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 as step decides.

exp2 beside pow

JavaScript's Math has no exp2, so the JavaScript target computes exp2(x) as Math.pow(2, x). That is the same call pow(2, x) makes, so a recorded Math.pow(2, x) cannot tell which of the two made it. In a program that calls both, a call of exp2 takes the bound of pow(2, x). WGSL defines pow(x, y) as exp2(y * log2(x)), so that bound is the bound of exp2 plus the error of log2(2) carried through it, and is the wider one.

CI

A failure in the gpu job of .github/workflows/test.yml now 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:

  <test name>
      makes a built-in call WGSL gives no bound, so a GPU may give any value — name "unbounded-builtin"

The fix is to name the reason:

// before
evaluateRecording((a) => a.sin(), [4]);
// after
evaluateRecording((a) => a.sin(), [4], "unbounded-builtin");

A program that names "unbounded-builtin" and makes no unbounded call fails with names "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-gives states 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-in
  • fact-glsl-es-300-bounds-the-error-of-some-float-built-ins
  • fact-javascript-math-has-no-exp2
  • fact-a-gpu-computes-an-argument-in-32-bits-at-each-step
  • spec-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-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, with bug-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-cos
    • spec-exp-and-exp2-widen-the-tolerance-by-3-plus-twice-the-argument-in-units-in-the-last-place, under which sit spec-exp-or-exp2-without-pow-widens-the-tolerance-by-3-plus-twice-the-argument and exception-exp2-takes-the-bound-of-pow-in-a-program-that-calls-pow
    • 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-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-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

Units changed:

  • axiom-a-cpu-target-gives-what-webgpu-gives
  • 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-the-float-tolerance-allows-a-few-units-in-the-last-place, which covers the base tolerance and sits under spec-a-gpu-as-accurate-as-wgsl-requires-passes-the-evaluation

Evidence

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 in probes/float-accuracy/results/, and node probes/float-accuracy/<probe>.ts runs it again.

  • wgsl-accuracy-table.ts reads WGSL §15.7.4 and §15.7.4.1, in the Candidate Recommendation Draft of 21 September 2026.
  • glsl-es-300-precision.ts reads 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.ts lists Math in Node 24.12.0.
  • f32-argument-crosses-pi.ts computes 3.0004 + 0.1411925918 in 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:

  • "passes a GPU atan2 within 4096 units in the last place, and fails one outside" replaces the GLSL and WGSL compilers with fake ones that return lavapipe's atan2(1, 1), and the run passes. With a value 5000 ULP off, the run fails.
  • "does not compare on GLSL a program GLSL ES gives no precision, and does on WGSL" uses a fake GLSL compiler that returns NaN for sin(0.5), and the run passes. A fake WGSL compiler that returns 0.25 fails it.
  • "reports a WASM result of a program kept off the GPU targets that differs from the JS one" compiles a program kept off the GPU targets to opposite signs on JavaScript and WASM, and the run fails.

In src/testing/shader-eval.test.ts, under "tolerance of a built-in call":

  • One test per spec checks each built-in's bound against the formula its spec states.
  • "widens the inverse hyperbolic functions by the error of their logarithm" checks that a simulated conforming WGSL acosh near 1, rounding every step away from the exact result, stays within the tolerance.
  • "has no bound for sin of an argument a GPU computes outside [-π, π]" is marked to fail and pins the bug of 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.
  • "leaves open on GLSL the components a call GLSL ES gives no precision moves" checks that 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.
  • The gpu job 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's atan2(1, 1) failed, passes there.
  • pnpm test:fast passes, and so does the checks job of CI.
  • pnpm canon check reports the same 4 findings as main (Should canon check run in CI, given the GPU exceptions no test can pin? #346), and pnpm canon log is clean.

Follow-ups

Fixes #344
Fixes #354

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
@pkg-pr-new

pkg-pr-new Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@random-mesh/rmsl@355

commit: 8c761b5

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
@bigmistqke

Copy link
Copy Markdown
Contributor Author

Replaced by #357. This branch modelled each built-in call's accuracy (recorded Math, a perturbation carry, then per-component GLSL skipping), and each mechanism opened new edge cases; #357 holds a program that calls a transcendental built-in to one working tolerance instead.

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

Labels

None yet

Projects

None yet

1 participant