erf: serve f16 from a table instead of converting the whole tensor each eval - #2579
Open
czoli1976 wants to merge 1 commit into
Open
erf: serve f16 from a table instead of converting the whole tensor each eval#2579czoli1976 wants to merge 1 commit into
czoli1976 wants to merge 1 commit into
Conversation
…ch eval The f16 arm allocated a fresh Vec<f32> the size of the input on every eval, ran the f32 kernel over it and converted back. Map the 65536 f16 bit patterns through the registered f32 kernel once and keep the result, so the activation is a single load per element and nothing is allocated per call. Building the table from the registered kernel rather than from a formula keeps the output identical to what the host dispatched to before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 3, 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.
Erf's f16 arm allocated a freshVec<f32>the size of the input on every eval, ran the f32 kernel over it, then converted back. Since erf is a unary f16 -> f16 map, the whole thing collapses into a 2^16-entry table: one load per element, nothing allocated per call.Why it is safe
The table is built by running the registered
ops().erf_f32over all 65536 f16 values, not from a formula. So it reproduces whatever kernel this host would have dispatched to — generic on aarch64, AVX-512 on x86 — and the output is bit-identical on each.erf_f16_lut_matches_the_f32_kernel_on_every_f16checks that over the full domain.It is built lazily behind a
OnceLock, so a model with no f16 erf never allocates the 128 KiB, and the one-time build is 65536 kernel evaluations.Numbers
f16
Erfthrougheval_in_place, criterion against a saved baseline on the merge-base:p = 0.00 throughout.
What I tried first
The obvious reading of "remove the allocation" is to keep the convert-run-convert shape and use a fixed stack scratch instead of a
Vec. That is a net regression and I would rather record it than have someone repeat it: at a 256-element chunk the per-chunk kernel dispatch costs more than themallocsaved (+5.7% at 4096, +6.7% at 65536), and at a 4096-element chunk the[0f32; N]literal memsets 16 KiB on every eval regardless of input size (+43% at 256 elements).MaybeUninitwould fix the second half but this is core, not linalg. The allocation was never the real cost; the per-element compute was.Relation to #2568
That PR introduces the same table technique for f16 GELU in linalg. The two are independent — different op, different crate, and this one has to build from the registered kernel because
erf_f32differs per arch, whereas the GELU table is built from a fixed scalar. If both land, happy to factor out a shared builder in whichever goes second.Tests
tract-core 270, test-f16 2378, test-unit-core 816 — green.
cargo fmt --allclean;cargo clippyclean (the two remaining warnings are pre-existing on main, insoftmax/mod.rsandoptim/propagate_roi.rs).🍍