From b5c3969f0b5fa2dba7675f404e01e22963942cf8 Mon Sep 17 00:00:00 2001 From: Stephen Jia Date: Mon, 12 Oct 2026 12:45:45 -0700 Subject: [PATCH] [ET-VK][test] Allow FP16 underflow in SwiGLU comparisons Pull Request resolved: https://github.com/pytorch/executorch/pull/23716 ## Ulterior Motive Keep `VulkanSwiGLUTest` correctness checks usable on Vulkan devices that flush FP16 subnormal values to zero. ## Rationale **What**: Increase the absolute error allowances in `MatchesUnfusedAcrossResizes` to account for FP16 underflow. **Why**: On stable, all 28 FP16 configurations fail on Adreno 750 while FP32 passes. The first expected output is `5.30033e-6`, below FP16's minimum normal value, but the shader returns zero. The unfused path can also flush subnormal inputs and intermediate tensors before later multiplications amplify the difference. ## Details - Fused versus CPU reference: - **Before**: Absolute allowance `1e-7`. - **After**: Absolute allowance `2^-14`, FP16's minimum normal value; relative allowance remains `1e-3`. - Fused versus unfused graph: - **Before**: Absolute allowance `6e-8 * (abs(x*y) + abs(y) + 1)` assumes subnormal preservation. - **After**: Keep the original rounding allowance and add `(1 + 1e-3) * abs(reference)` only when an FP16 input is subnormal or a computed intermediate/output can round below the minimum normal value. Use the existing `3e-3` relative tolerance as the rounding margin at that boundary. - If the unfused result flushes to zero, its relative tolerance vanishes. `abs(reference)` covers the lost output, and the extra `1e-3` covers the fused result's relative error. - Cases without underflow keep the small original absolute allowance: `x=20, y=2` gives `2.58e-6`; the original tiny-output case gives about `6.63e-6`. - FP32 uses its existing assertions. Both FP16 comparisons keep `ASSERT_NEAR`. Differential Revision: [D124833698](https://our.internmc.facebook.com/intern/diff/D124833698/) ghstack-source-id: 446951678 --- .../vulkan/test/vulkan_compute_api_test.cpp | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/backends/vulkan/test/vulkan_compute_api_test.cpp b/backends/vulkan/test/vulkan_compute_api_test.cpp index d77b8e8c95c..d56fee3713a 100644 --- a/backends/vulkan/test/vulkan_compute_api_test.cpp +++ b/backends/vulkan/test/vulkan_compute_api_test.cpp @@ -4316,15 +4316,35 @@ TEST_P(VulkanSwiGLUTest, MatchesUnfusedAcrossResizes) { const float y = float(up_data[broadcast ? i % width : i]); const float reference = (x / (1.0f + std::exp(-x))) * y; if (dtype == vkapi::kHalf) { + constexpr float fp16_min_normal = 0x1p-14f; + constexpr float unfused_rtol = 3e-3f; ASSERT_NEAR( - float(actual[i]), reference, 1e-3f * std::abs(reference) + 1e-7f) + float(actual[i]), + reference, + 1e-3f * std::abs(reference) + fp16_min_normal) << "element " << i; - // The unfused path rounds twice more, including subnormal sigmoid. - const float atol = 6e-8f * (std::abs(x * y) + std::abs(y) + 1.0f); + const float sigmoid_reference = 1.0f / (1.0f + std::exp(-x)); + const auto is_subnormal = [](const float value) { + return value != 0.0f && std::abs(value) < fp16_min_normal; + }; + const auto may_underflow = [](const float value) { + return value != 0.0f && + std::abs(value) < fp16_min_normal * (1.0f + unfused_rtol); + }; + float atol = 6e-8f * (std::abs(x * y) + std::abs(y) + 1.0f); + if (is_subnormal(x) || is_subnormal(y) || + may_underflow(sigmoid_reference) || + may_underflow(x * sigmoid_reference) || + may_underflow(reference)) { + // Flushing the unfused path to zero can lose the full reference + // magnitude. Its relative tolerance then vanishes, so the extra + // 1e-3 covers the fused result's relative error. + atol += (1.0f + 1e-3f) * std::abs(reference); + } ASSERT_NEAR( float(actual[i]), float(expected[i]), - 3e-3f * std::abs(float(expected[i])) + atol) + unfused_rtol * std::abs(float(expected[i])) + atol) << "element " << i; } else { ASSERT_NEAR(