linalg/wasm: fused simd128 f32 kernels for gelu, silu and erf - #2591
linalg/wasm: fused simd128 f32 kernels for gelu, silu and erf#2591czoli1976 wants to merge 2 commits into
Conversation
|
Pushed a fix: both kernels were returning the wrong sign on the negative tail, which
Magnitude is tiny in both cases, so Verified on |
On plain +simd128 builds the gelu, silu and erf slots fall back to generic kernels that go through libm per element; give them fused single-pass f32x4 kernels reusing the scalar coefficients, with gelu's tanh saturated to exactly -1 on low-clamped lanes so it decays to zero like the scalar path. Sigmoid and tanh keep the generic polynomial kernels, which LLVM auto-vectorizes into faster code than a handwritten simd128 loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…egative tail Both kernels built their result from a factor that cancels through zero on the negative tail: gelu's Pade quotient overshoots past -1 before the argument reaches the low clamp, so the pinned-lane substitution never fires and 1 + tanh turns negative, and silu's sigmoid sum was left unclamped where ssigmoid clamps. Either one returns a positive value for a negative input. Saturate both factors, which also subsumes the pinned-lane substitution. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Rebasing this onto current main; resolving a real conflict against the matmul dispatch refactor that landed since. Full workspace suite running before I push. |
5425c87 to
1d252ca
Compare
|
Rebased onto current main and pushed. Found and fixed a real latent bug in the process: the activation-kernel wiring in plug() referenced WasmErf4/WasmSilu4/WasmGelu4 unconditionally, but those types are gated behind #[cfg(target_feature = "simd128")] — main has since started compiling this module on native hosts too (via the foreign-inventory feature tract-cli enables), which surfaced it. Gated the wiring to match. The metal-kernel test failures I initially saw were GPU contention from a concurrent test run on my end, not real — confirmed clean in isolation (same known MFA probe-test failure as #2546, nothing else). |
On plain
+simd128builds (stable toolchain, no relaxed-simd) thegelu_f32,silu_f32anderf_f32slots fall back to generic kernels that go through libm (tanh,exp) per element; this adds fused single-pass f32x4 kernels for those three, reusing the scalar kernels' coefficients, and wires them inwasm::plug.Numbers
Through
ElementWiseImplon 64k elements, wasmtime on x86-64, values in [-3, 3) refilled from a source buffer every iteration (iterated in-place application decays gelu/silu into subnormals and poisons the timing):gelu and silu win big because their generic kernels call libm per element. erf's generic kernel is a pure polynomial that LLVM already auto-vectorizes, so the fused kernel only shaves the
powi(16)/store schedule.What is deliberately absent
No simd128 sigmoid or tanh. I wrote them, measured them, and deleted them: the generic polynomial loops auto-vectorize under
+simd128and beat the handwritten kernels (sigmoid 1.43 vs 2.54 ns/elem, tanh 1.30 vs 2.14 — the auto-vectorized loop unrolls wider and hides thef32x4.divlatency). Only the relaxed-simd FMA variants inact.rsbeat the generic baseline, and those already exist and keep their override.The gelu tail
The kernel clamps the pre-tanh argument to [-8.9, 8.9] and the Padé polynomial lands one ulp short of -1 at the low bound, the same unbounded-error tail #2582 fixes on arm64. Rather than ship the bug and fix it later, the lanes the low clamp pinned substitute an exact
-1.0(f32x4_eq+v128_bitselect, two extra ops);gelu_saturates_to_zero_below_the_tanh_clampcovers it out to ±1e6.Tests
Frame tests for all three kernels plus the tail test, 489 green on
wasm32-wasip1 +simd128under wasmtime; the+relaxed-simdconfiguration still installs its sigmoid/tanh overrides on top and itsactsuite is green; native andwasm32-unknown-unknown(no simd) builds are unaffected (linalg::wasmis feature-gated out).cargo fmt/clippyclean.🍍