Repository navigation
fix(simd,vml): simd_exp_f32 and vstanh within 1 ULP; tanh keeps ±0 and subnormals - #369
Conversation
…/inf/NaN, tail == chunk Fails on the current kernel: 2·sigmoid(2x) − 1 cancels for |x| below ~1e-7 (785,499,064 ULP at x = -9.5e-11), and -0.0 returns +0.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg
Fails today: the degree-5 Taylor polynomial is 39 ULP off at reduced argument |r| = ln2/2 (r^6/720 ≈ 2.4e-6 relative), against the doc comment's '~2 ULP'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg
simd_exp_f32: replace the degree-5 Taylor polynomial (39 ULP at reduced argument |r| = ln2/2) with the Cephes expf minimax 1 + r + r²·P(r). Plain * and + as before, so the scalar reference still matches bit for bit on every backend. Measured: max 1 ULP on 4,194,304 points over [-87, 88.5], against exp in f64 rounded once (AVX-512 and AVX2). vstanh: 2·sigmoid(2x) − 1 cancelled for small |x| (every |x| below ~1e-7 returned 0, up to 868,220,004 ULP) and turned -0.0 into +0.0. The 16-lane kernel now works on |x|: - |x| < 0.625: a + a·z·P(z), z = a² (Cephes tanhf polynomial); - otherwise 1 − 2/(exp(2a) + 1); then copies the sign bit of x back. The tail and the strided path go through the same kernel, so a value gets the same bits anywhere in the array. Measured: max 1 ULP in every band from 1e-38 to 100, both signs; ±0, subnormals, ±inf and NaN exact. Users of simd_exp_f32 (vsexp, softmax, GELU, layer kernels) get the more accurate exp too. Full lib suite: 2590 passed, 0 failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg
…p reference The NEON U32x16 has no & / |, so the sign-bit copy did not compile for aarch64. tanh now negates where x < 0 and returns x where x == 0 (keeps -0.0), using only F32x16 compares and select, which every backend has. Measured under qemu-aarch64 (NEON): exp max 1 ULP, tanh max 1 ULP in every band, the same as AVX-512 and AVX2. wasm-simd-parity restated the old Taylor polynomial as its bit-for-bit exp reference; it now restates the Cephes coefficients. wasm parity: OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
…saturation examples/vstanh_exhaustive.rs evaluates every f32 (each non-negative bit pattern with its negation) against tanh in f64 rounded once, and runs the previous 2*sigmoid(2x)-1 kernel as a baseline arm. Measured on AVX-512: - repaired: max 1 ULP over all 2^32 inputs; 0 zeroed nonzero inputs; 0 bitwise odd-symmetry violations; 0 outside [-1, 1]; 0 monotonicity violations; every NaN stays NaN; 1 reference flip-risk input. - previous kernel: max 868,220,927 ULP; 1,728,053,246 nonzero inputs returned 0; 975,101,106 odd-symmetry violations. - 3.06 ns/elem (old kernel 2.46, scalar f32::tanh 11.66). The flip-risk input and every band's worst input agree with mpmath at 60 digits, and the f64 reference equals mpmath correct rounding on 200,000 log-uniform inputs (0 differ). New unit tests: every length 0..65 around the 16-lane chunk matches the one-element result; 2-D, transposed and strided views match contiguous; 4001 consecutive floats across the 0.625 branch threshold, both signs, within 2 ULP, monotone and odd; MAX/-MAX and large inputs saturate to ±1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Why
ONNX
Tanhis meant to map directly ontondarray::hpc::vml::vstanh, sovstanhhas to be accurate. Measured against the correctly rounded result (f64tanh/exprounded once tof32), it was not:vstanh, |x| in 1e-38 … 1e-10vstanh, |x| in 1e-10 … 1e-4vstanh, |x| in 1e-4 … 0.1vstanh, |x| in 0.1 … 4vstanh(-0.0)+0.0-0.0vstanhon subnormalssimd_exp_f32, [-87, 88.5]Causes
vstanhcancelled. It computed2·sigmoid(2x) − 1, and below |x| ≈ 1e-7 the sigmoid rounds to exactly 0.5. The kernel now works ona = |x|:a + a·z·P(z)withz = a²fora < 0.625(the Cephestanhfpolynomial);1 − 2/(exp(2a) + 1)otherwise;x < 0, andxreturned wherex == 0.It uses only
F32x16compares andselect, because the NEONU32x16has no&/|. The tail and the strided path run through the same 16-lane kernel, so a value gets the same bits wherever it sits in the array.simd_exp_f32used a degree-5 Taylor series. Its r⁶/720 term is about 2.4e-6 relative at |r| = ln2/2. It now uses the Cephesexpfminimax1 + r + r²·P(r), still with plain*and+, so the bit-for-bit scalar reference keeps every backend identical. Every user ofsimd_exp_f32(vsexp, softmax, GELU, the model layers) gets the more accurateexptoo.No public API changes and no new public functions.
Evidence
Tests first. Commits
0e176e1(the tanh gate) and9650bc5(the exp gate) failed on the old kernels before the fix landed.4f6c132adds four more tests: every length from 0 to 65 agrees with a single lane; strided and 2-D views match the contiguous result; the neighbourhood of the 0.625 branch point is monotone and odd; saturation and extremes behave.Exhaustive sweep (
examples/vstanh_exhaustive.rs, all 2³² f32 inputs, AVX-512):NaN is preserved. The oracle (f64
tanhrounded once) agrees with mpmath's correctly rounded value on 200,000 of 200,000 sampled inputs, and on every worst case. Exactly one input lies near a rounding boundary in f64; it also agrees with mpmath.Throughput, same run:
vstanh3.06 ns/elem, the old kernel 2.46 ns/elem, scalarf32::tanh11.66 ns/elem.Disable runs. Each was applied with an asserted anchor, and each turned its tests red:
x == 0select-0.0→+0.0)Backends. The same ULP probe gives identical numbers on four builds:
x86-64-v4);x86-64-v3);qemu-aarch64-static.That is exp ≤ 1 ULP on 4,194,304 points, and tanh ≤ 1 ULP on 2·2¹⁸ points per band.
wasm.
scripts/wasm-parity.shpasses. Its harness restated the old exp polynomial as its bit-for-bit reference and now restates the new one.Native gates.
cargo test --release --lib: 2590 passed, 0 failed.cargo clippy --lib --tests -- -D warningsandcargo fmt --checkare clean.Not covered
vstanhaccuracy on wasm. The harness checksexpthere, nottanh.core::simdarm.Downstream: the error decomposition in AdaWorldAPI/lance-graph#1757 shows the tanh stage adding about 1e-8 to a Fisher-Z round trip, against about 1e-3 for i8 quantization.
Generated by Claude Code