[MLAS] Fix fp16 overflow/NaN bugs in ARM64 NEON Gelu/Erf and add extreme-value test coverage - #32631
Open
kjg0724 (kjg0724) wants to merge 3 commits into
Open
kjg0724 (kjg0724) wants to merge 3 commits into
kjg0724 (kjg0724) wants to merge 3 commits into
Conversation
… inputs MlasNeonGeluFP16Kernel's vector path computed 0.5*(x*(1+t)) in fp16, multiplying x by (1+t) before scaling by 0.5. For x>=32768 this product overflows the fp16 range and rounds to +Inf even though the true result is finite and close to x, since |0.5*(1+t)| never exceeds 1 for either the erf or tanh GELU approximation. The scalar tail computes the equivalent expression in fp32 with a single rounding and does not exhibit this overflow, so the same logical input produces +Inf or a finite value depending only on which SIMD lane processes it. Reassociating the multiplication to x*(0.5*(1+t)) removes the overflow entirely: since |0.5*(1+t)| is bounded by 1, the product with finite x cannot exceed x in magnitude. Verified against all 65536 fp16 inputs on Neoverse-N1 (Oracle Cloud A1, dotprod, no SVE): only the [32768, 65504] range changes (1024 values, all from incorrect +Inf to the correct finite result); accuracy statistics elsewhere are unaffected.
MlasNeonErfFP16Kernel's vector path compares a NaN input against 0 and against the |x|<4 saturation threshold; both comparisons evaluate to false for NaN under IEEE 754 semantics, so the kernel selects the positive-saturation branch and returns +1.0 instead of propagating NaN. The scalar tail and the fp32 MlasComputeErf reference both propagate NaN as expected, so only the vector lanes disagree, and only for NaN inputs. Add a NaN-safe select after computing the vector result: compare each lane against itself (false only for NaN, per IEEE 754) and pass through the original NaN bits on mismatch. Verified against all 65536 fp16 inputs on Neoverse-N1 (Oracle Cloud A1): only the 2046 NaN encodings change output (from +1.0 to NaN); no other values are affected.
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The added test has critical non-ARM compilation and Windows ARM64 linkage issues.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes ARM64 NEON FP16 GELU overflow and Erf NaN propagation, adding boundary and lane-position tests.
Changes:
- Prevents GELU intermediate FP16 overflow.
- Preserves NaN values in vectorized Erf.
- Adds comprehensive extreme-value coverage.
File summaries
| File | Summary | Review Notes |
|---|---|---|
onnxruntime/test/mlas/unittest/test_gelu_erf_neon_fp16.cpp |
Adds GELU/Erf tests. | Critical: headers break non-ARM builds; Windows ARM64 linkage is unresolved (2 votes each). |
onnxruntime/core/mlas/lib/gelu_neon_fp16.cpp |
Prevents intermediate GELU overflow. | No findings. |
onnxruntime/core/mlas/lib/fp16_common.h |
Adds FP16 equality comparison helper. | No findings. |
onnxruntime/core/mlas/lib/erf_neon_fp16.cpp |
Restores NaN propagation. | No findings. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite (auto)
Note
Copilot is running an experiment and ran this review at Lite.
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
kjg0724 (kjg0724)
force-pushed
the
gelu-erf-neon-fp16-fix
branch
from
September 16, 2026 06:00
022afdf to
1a01100
Compare
No test in the repository directly exercises MlasNeonGeluFP16Kernel or MlasNeonErfFP16Kernel; test_fp16_activation.cpp covers Tanh, Logistic and Clip but not Gelu or Erf. Add test_gelu_erf_neon_fp16.cpp with random-input coverage plus fixed and single-lane extreme-value cases (erf saturation boundary at |x|=4, the fp16 overflow boundary around x=32768, subnormal/min-normal magnitudes, and Inf/NaN) for both GELU approximations (erf and tanh). The single-lane cases place one extreme value at varying offsets within an 8-wide vector chunk (and in the scalar tail) so a regression that only affects specific SIMD lanes cannot hide behind an otherwise-benign input array, which is exactly how the two kernel bugs fixed in the preceding commits were found. Verified on Neoverse-N1 (Oracle Cloud A1): new tests pass, and the full onnxruntime_mlas_test suite shows no regressions (37959/37959 non-skipped tests passed).
kjg0724 (kjg0724)
force-pushed
the
gelu-erf-neon-fp16-fix
branch
from
September 16, 2026 06:36
1a01100 to
69c8548
Compare
mirounga
self-requested a review
September 18, 2026 03:24
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.
Summary
No test in the repository directly exercises
MlasNeonGeluFP16KernelorMlasNeonErfFP16Kernel(test_fp16_activation.cppcovers Tanh/Logistic/Clip but not Gelu/Erf). Adding coverage surfaced two lane-position-dependent bugs in the existing kernels, fixed here alongside the new tests.Gelu overflow for large positive inputs (
gelu_neon_fp16.cpp)The vector path computed
0.5 * (x * (1 + t)), multiplyingxby(1 + t)in fp16 before scaling by0.5. Forx >= 32768this intermediate product overflows the fp16 range and rounds to+Inf, even though the true result is finite and close tox(since|0.5*(1+t)|never exceeds 1 for either the erf or tanh GELU approximation). The scalar tail computes the equivalent expression in fp32 with a single rounding and does not overflow, so the same logical input produces+Infor the correct finite value depending only on which SIMD lane processes it.Reassociating to
x * (0.5 * (1 + t))removes the overflow structurally:|0.5*(1+t)|is bounded by 1, so the product with a finitexcannot exceedxin magnitude.Verified against all 65536 fp16 inputs on Neoverse-N1 (Oracle Cloud A1, dotprod, no SVE):
[32768, 65504](1024 values)+InfOverall accuracy statistics (max abs/rel error) are identical before and after outside the overflow range.
Erf NaN propagation (
erf_neon_fp16.cpp)The vector path compares a NaN input against
0and against the|x| < 4saturation threshold; both comparisons evaluate tofalsefor NaN under IEEE 754 semantics, so the kernel selects the positive-saturation branch and returns+1.0instead of propagating NaN. The scalar tail and the fp32MlasComputeErfreference both propagate NaN as expected, so only the vector lanes disagree, and only for NaN inputs.Fix: compare each lane against itself (
vceqq_f16(x, x), false only for NaN) and select the original NaN bits on mismatch, via a newMlasCompareEqualFloat16helper alongside the existingMlasCompareLessThanFloat16.Verified against all 65536 fp16 inputs: only the 2046 NaN encodings change output (from
+1.0to NaN); no other values are affected.New tests (
test_gelu_erf_neon_fp16.cpp)Following the
test_hgemm_neon.cpppattern (MlasTestBase,FloatEqualwith explicit Inf/NaN handling, random + fixed-value + single-lane extreme-value cases): random-input coverage plus extreme-value cases for the erf saturation boundary (|x| == 4, both signs), the fp16 overflow boundary aroundx == 32768, subnormal/min-normal magnitudes, and Inf/NaN, for both GELU approximations (erf and tanh).The single-lane cases place one extreme value at every offset within an 8-wide vector chunk (0-7), the first scalar-tail lane (8), and the last lane of the buffer, so a lane-dependent regression cannot hide behind an otherwise-benign input array — this is exactly how both kernel bugs above were found.
RefGeluTanh's reference round-tripstanh(clamp(inner, -5, 5))through fp16 before combining withx, matching what the real kernel does on both the vector and scalar paths (MlasComputeTanh<MLAS_FP16>); without this, extreme inputs like-65504and-Infdiverge from the kernel becausetanh(±5) ≈ ∓0.9999092in float32 is not exactly∓1.0, but rounds to exactly∓1.0in fp16.SVE (not touched in this PR)
sve/elementwise_sve_fp16.cpphas the same two bug patterns (svmul(OneHalf, svmul(x, ...))overflow atelementwise_sve_fp16.cpp:437, and the same NaN-saturates-to-+1 issue in the SVE Erf implementation). This machine has no SVE hardware to verify against, and the production SVE kernel is linked from a pre-generated assembly file (aarch64/elementwise_sve_asm.S) rather than built from the.cppdirectly, so a real fix needsgen_sve_asm.pyregeneration. Flagging here for mirounga rather than guessing at a fix blind.Testing
Neoverse-N1 (Oracle Cloud A1, Ubuntu):
*NeonErfFP16*/*NeonGeluFP16*: new tests passonnxruntime_mlas_testsuite: 37959/37959 non-skipped tests passed, no regressions