diff --git a/cmake/onnxruntime_unittests.cmake b/cmake/onnxruntime_unittests.cmake index bd12b50b7af43..7f361aa63921f 100644 --- a/cmake/onnxruntime_unittests.cmake +++ b/cmake/onnxruntime_unittests.cmake @@ -1025,7 +1025,8 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") endif() list(REMOVE_ITEM all_tests "${TEST_SRC_DIR}/providers/cpu/reduction/reduction_ops_test.cc" - "${TEST_SRC_DIR}/providers/cpu/tensor/grid_sample_test.cc") + "${TEST_SRC_DIR}/providers/cpu/tensor/grid_sample_test.cc" + "${TEST_SRC_DIR}/providers/cpu/tensor/grid_sample_test_custom.cc") endif() if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR IOS) diff --git a/onnxruntime/core/providers/cpu/tensor/grid_sample.cc b/onnxruntime/core/providers/cpu/tensor/grid_sample.cc index df0bc235a4f9e..34135ca62e4a8 100644 --- a/onnxruntime/core/providers/cpu/tensor/grid_sample.cc +++ b/onnxruntime/core/providers/cpu/tensor/grid_sample.cc @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include +#include #include #include "core/common/safeint.h" @@ -63,9 +65,16 @@ T GsReflect(T x, T x_min, T x_max) { T dx = {}; T fx = static_cast(x); T range = x_max - x_min; + // Guard against NaN, Inf, or non-positive range (e.g. dim==1 with align_corners=true) + // which would otherwise produce wild indices via division by zero or UB float->int casts. + if (!std::isfinite(fx) || !(range > T{0})) { + return x_min; + } if (fx < x_min) { dx = x_min - fx; - int n = static_cast(dx / range); + // Use int64_t rather than int: for extreme (but finite) inputs, dx / range can exceed + // INT_MAX, making a float->int cast undefined behavior. + int64_t n = static_cast(dx / range); T r = dx - n * range; if (n % 2 == 0) { fx = x_min + r; @@ -74,7 +83,7 @@ T GsReflect(T x, T x_min, T x_max) { } } else if (fx > x_max) { dx = fx - x_max; - int n = static_cast(dx / range); + int64_t n = static_cast(dx / range); T r = dx - n * range; if (n % 2 == 0) { fx = x_max - r; @@ -86,6 +95,15 @@ T GsReflect(T x, T x_min, T x_max) { return static_cast(fx); } +// Returns true when v is finite and its magnitude is small enough that converting it to +// int64_t via std::floor / std::nearbyint is well-defined. 2^62 is far below INT64_MAX +// (~9.22e18) and leaves ample margin for any realistic image dimension. +template +inline bool IsSafeForInt64Conversion(T v) { + constexpr T kSafeBound = static_cast(int64_t{1} << 62); + return std::isfinite(v) && v >= -kSafeBound && v <= kSafeBound; +} + // Calculate cubic convolution interpolation coefficients // ROBERT G. KEYS https://ieeexplore.ieee.org/document/1163711 template @@ -124,6 +142,11 @@ T GridSample::PixelAtGrid(const T* image, int64_t r, int64_t c, int64_t H, in } else { // (padding_mode_ == Reflection) c = static_cast(GsReflect(static_cast(c), border[0], border[2])); r = static_cast(GsReflect(static_cast(r), border[1], border[3])); + // Safety clamp: GsReflect is computed in floating point and casts back to int64_t. + // Extreme grid coordinates can overflow that cast, so clamp the resulting indices + // back into the image range before indexing. + c = std::clamp(c, 0, W - 1); + r = std::clamp(r, 0, H - 1); pixel = image[r * W + c]; } return pixel; @@ -145,6 +168,12 @@ T GridSample::PixelAtGrid3D(const T* image, int64_t d, int64_t h, int64_t w, w = static_cast(GsReflect(static_cast(w), border[0], border[3])); h = static_cast(GsReflect(static_cast(h), border[1], border[4])); d = static_cast(GsReflect(static_cast(d), border[2], border[5])); + // Safety clamp: GsReflect is computed in floating point and casts back to int64_t. + // Extreme grid coordinates can overflow that cast, so clamp the resulting indices + // back into the image range before indexing. + w = std::clamp(w, 0, W - 1); + h = std::clamp(h, 0, H - 1); + d = std::clamp(d, 0, D - 1); pixel = image[d * H * W + h * W + w]; } return pixel; @@ -186,8 +215,19 @@ void PrecomputeBilinearSamplePlan2D(const T* grid_data, auto& plan = plans[idx]; const T nx = grid_data[idx * 2]; const T ny = grid_data[idx * 2 + 1]; - const T x = GsDenormalize(nx, W_in, false); - const T y = GsDenormalize(ny, H_in, false); + T x = GsDenormalize(nx, W_in, false); + T y = GsDenormalize(ny, H_in, false); + + // Sanitize coordinates that are non-finite or whose magnitude is too large + // for a safe float->int64 conversion via std::floor. The fast path is only used + // for zeros padding without align_corners, so substituting the lower border (-0.5) + // produces an out-of-bounds floor index that the mask logic correctly rejects. + if (!IsSafeForInt64Conversion(x)) { + x = static_cast(-0.5f); + } + if (!IsSafeForInt64Conversion(y)) { + y = static_cast(-0.5f); + } const int64_t x1 = static_cast(std::floor(x)); const int64_t y1 = static_cast(std::floor(y)); @@ -398,6 +438,17 @@ Status GridSample::Compute(OpKernelContext* context) const { auto x = GsDenormalize(nx, W_in, align_corners_); // actual location auto y = GsDenormalize(ny, H_in, align_corners_); + // Sanitize coordinates that are non-finite or whose magnitude is too large + // for a safe float->int64 conversion via std::floor / std::nearbyint. + // Substituting the in-range border value keeps the subsequent casts + // well-defined while still producing a defined output for each padding mode. + if (!IsSafeForInt64Conversion(x)) { + x = x_min; + } + if (!IsSafeForInt64Conversion(y)) { + y = y_min; + } + if (mode_ == Nearest) { x = static_cast(std::nearbyint(static_cast(x))); y = static_cast(std::nearbyint(static_cast(y))); @@ -496,6 +547,20 @@ Status GridSample::Compute(OpKernelContext* context) const { auto y = GsDenormalize(ny, H_in, align_corners_); auto z = GsDenormalize(nz, D_in, align_corners_); + // Sanitize coordinates that are non-finite or whose magnitude is too large + // for a safe float->int64 conversion via std::floor / std::nearbyint. + // Substituting the in-range border value keeps the subsequent casts + // well-defined while still producing a defined output for each padding mode. + if (!IsSafeForInt64Conversion(x)) { + x = x_min; + } + if (!IsSafeForInt64Conversion(y)) { + y = y_min; + } + if (!IsSafeForInt64Conversion(z)) { + z = z_min; + } + if (mode_ == Nearest) { x = static_cast(std::nearbyint(static_cast(x))); y = static_cast(std::nearbyint(static_cast(y))); diff --git a/onnxruntime/test/providers/cpu/tensor/grid_sample_test.cc b/onnxruntime/test/providers/cpu/tensor/grid_sample_test.cc index f10aa5a49c120..4dc3aa9b652a7 100755 --- a/onnxruntime/test/providers/cpu/tensor/grid_sample_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/grid_sample_test.cc @@ -38,8 +38,8 @@ void RunTests(T& test, std::vector>&& execut execution_providers.clear(); } -// Custom tests not generated by grid_sample_test_gen.py. -#include "test/providers/cpu/tensor/grid_sample_test_custom.inc" +// Custom tests not generated by grid_sample_test_gen.py live in +// grid_sample_test_custom.cc. // DO NOT edit following tests. They are generated by: // onnxruntime/test/providers/cpu/tensor/grid_sample_test_gen.py diff --git a/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.cc b/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.cc new file mode 100644 index 0000000000000..dd23b76d8275d --- /dev/null +++ b/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.cc @@ -0,0 +1,582 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Custom GridSample tests not generated by grid_sample_test_gen.py. +// These cover edge cases (extreme coordinates, NaN/Inf, dim==1, border, 3D) +// that exercise the float->int64 cast hardening in +// onnxruntime/core/providers/cpu/tensor/grid_sample.cc. + +#include +#include +#include +#include + +#include "gtest/gtest.h" + +#include "core/framework/execution_provider.h" +#include "test/providers/provider_test_utils.h" +#include "test/util/include/default_providers.h" + +namespace onnxruntime { +namespace test { +namespace { + +std::vector> GetCpuExecutionProviders() { + std::vector> execution_providers; + execution_providers.emplace_back(DefaultCpuExecutionProvider()); + return execution_providers; +} + +std::vector> GetExecutionProviders() { + std::vector> execution_providers; + + execution_providers.emplace_back(DefaultCpuExecutionProvider()); + +#ifdef USE_CUDA + execution_providers.emplace_back(DefaultCudaExecutionProvider()); +#ifdef ENABLE_CUDA_NHWC_OPS + execution_providers.push_back(DefaultCudaNHWCExecutionProvider()); +#endif +#endif + +#if defined(USE_COREML) + execution_providers.push_back(DefaultCoreMLExecutionProvider(/*use_mlprogram*/ true)); +#endif + +#ifdef USE_WEBGPU + execution_providers.push_back(DefaultWebGpuExecutionProvider()); +#endif + + return execution_providers; +} + +template +void RunTests(T& test, std::vector>&& execution_providers) { + for (size_t idx = 0; idx < execution_providers.size(); ++idx) { + test.ConfigEp(std::move(execution_providers[idx])).RunWithConfig(); + } + execution_providers.clear(); +} + +template +void RunCpuOnly(T& test) { + RunTests(test, GetCpuExecutionProviders()); +} + +} // namespace + +template +class GridSampleCustomTest : public ::testing::Test { +}; + +using GridSampleCustomTestTypes = ::testing::Types; +TYPED_TEST_SUITE(GridSampleCustomTest, GridSampleCustomTestTypes); + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_linear_zeros_mixed_bounds_right_bottom) { + // Crafts grid points that mix fully in-bounds sampling with cases where either the right, bottom, + // or both neighbors fall outside the source image so zero padding must be applied. This ensures + // the optimized bilinear fast path matches the generic implementation for boundary handling. + OpTester test("GridSample", 20); + std::string mode = "linear"; + std::string padding_mode = "zeros"; + int64_t align_corners = 0; + std::initializer_list X_shape{1, 1, 2, 2}; + std::initializer_list X_data{TypeParam(1.0f), TypeParam(2.0f), TypeParam(3.0f), TypeParam(4.0f)}; + std::initializer_list Grid_shape{1, 2, 2, 2}; + // (nx, ny) pairs: center (in-bounds), right edge (x out), bottom edge (y out), corner (both out) + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), // center (all neighbors in bounds) + TypeParam(0.9f), TypeParam(0.0f), // near right edge (right neighbors out of bounds) + TypeParam(0.0f), TypeParam(0.9f), // near bottom edge (bottom neighbors out) + TypeParam(0.9f), TypeParam(0.9f)}; // near bottom-right corner (both right and bottom neighbors out) + std::initializer_list Y_shape{1, 1, 2, 2}; + std::initializer_list Y_data{ + TypeParam(2.5f), // all neighbors in bounds + TypeParam(1.8f), // right neighbors partially out-of-bounds + TypeParam(2.1f), // bottom neighbors partially out-of-bounds + TypeParam(1.44f)}; // both right and bottom neighbors out-of-bounds + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddAttribute("mode", mode); + test.AddAttribute("padding_mode", padding_mode); + test.AddAttribute("align_corners", align_corners); + test.AddOutput("Y", Y_shape, Y_data); + RunTests(test, GetExecutionProviders()); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_linear_zeros_mixed_bounds_left_top) { + // Similar to test_grid_sample_20_4D_linear_zeros_mixed_bounds_right_bottom but focuses on left/top boundary cases, + // where the left and/or top neighbors fall outside the source image and zero padding must be applied. + // This ensures the optimized bilinear fast path correctly handles left/top boundary conditions. + OpTester test("GridSample", 20); + std::string mode = "linear"; + std::string padding_mode = "zeros"; + int64_t align_corners = 0; + std::initializer_list X_shape{1, 1, 2, 2}; + std::initializer_list X_data{TypeParam(1.0f), TypeParam(2.0f), TypeParam(3.0f), TypeParam(4.0f)}; + std::initializer_list Grid_shape{1, 2, 2, 2}; + // (nx, ny) pairs: center (in-bounds), left edge (x out), top edge (y out), corner (both out) + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), // center (all neighbors in bounds) + TypeParam(-0.9f), TypeParam(0.0f), // near left edge (left neighbors out of bounds) + TypeParam(0.0f), TypeParam(-0.9f), // near top edge (top neighbors out of bounds) + TypeParam(-0.9f), TypeParam(-0.9f)}; // near top-left corner (both left and top neighbors out of bounds) + std::initializer_list Y_shape{1, 1, 2, 2}; + std::initializer_list Y_data{ + TypeParam(2.5f), // all neighbors in bounds + TypeParam(1.2f), // left neighbors partially out-of-bounds + TypeParam(0.9f), // top neighbors partially out-of-bounds + TypeParam(0.36f)}; // both left and top neighbors out-of-bounds + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddAttribute("mode", mode); + test.AddAttribute("padding_mode", padding_mode); + test.AddAttribute("align_corners", align_corners); + test.AddOutput("Y", Y_shape, Y_data); + RunTests(test, GetExecutionProviders()); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_reflection_extreme_coords) { + // Regression test: large grid coordinates previously produced wild indices in the + // reflection branch of PixelAtGrid. The internal division-and-int-cast inside + // GsReflect now uses int64_t, and PixelAtGrid clamps the resulting index back into + // the image range, so sampling stays in bounds. Using "nearest" mode keeps the test + // focused on the reflection index path (no bilinear weight arithmetic), and using a + // constant-valued input makes the expected output deterministic regardless of which + // in-range pixel is hit. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 2}; + std::initializer_list X_data{TypeParam(1.0f), TypeParam(1.0f), + TypeParam(1.0f), TypeParam(1.0f)}; + + std::initializer_list Grid_shape{1, 1, 2, 2}; + // 1e+10 exceeds INT_MAX, so the pre-fix `int n = static_cast(dx / range)` + // inside GsReflect would invoke undefined behavior; 1e+10 still fits comfortably + // in int64_t. For MLFloat16 the value overflows to +/-Inf, exercising the same + // safety-clamp fallback in PixelAtGrid. + std::initializer_list Grid_data{ + TypeParam(1.0e+10f), TypeParam(1.0e+10f), + TypeParam(-1.0e+10f), TypeParam(-1.0e+10f)}; + + std::initializer_list Y_shape{1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_reflection_nan_coords) { + // Regression test: NaN grid coordinates would otherwise propagate into + // static_cast(std::nearbyint(NaN)), which is undefined behavior. + // The Compute loop now sanitizes such coordinates to the in-range border + // before performing the cast, producing defined sampling. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 2}; + std::initializer_list X_data{TypeParam(1.0f), TypeParam(1.0f), + TypeParam(1.0f), TypeParam(1.0f)}; + + std::initializer_list Grid_shape{1, 1, 2, 2}; + const float nan_val = std::numeric_limits::quiet_NaN(); + std::initializer_list Grid_data{ + TypeParam(nan_val), TypeParam(nan_val), + TypeParam(0.0f), TypeParam(nan_val)}; + + std::initializer_list Y_shape{1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // With a constant-valued image any reflected pixel returns 1.0. + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_bilinear_reflection_infinity_coords) { + // Regression test: Inf grid coordinates would otherwise reach + // static_cast(std::floor(Inf)), which is undefined behavior. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("linear")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 2}; + std::initializer_list X_data{TypeParam(1.0f), TypeParam(1.0f), + TypeParam(1.0f), TypeParam(1.0f)}; + + std::initializer_list Grid_shape{1, 1, 2, 2}; + const float inf_val = std::numeric_limits::infinity(); + std::initializer_list Grid_data{ + TypeParam(inf_val), TypeParam(-inf_val), + TypeParam(-inf_val), TypeParam(inf_val)}; + + std::initializer_list Y_shape{1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // Constant-valued image: any reflected sample returns 1.0. + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_bilinear_reflection_extreme_coords) { + // Bilinear interpolation path: the four corner indices are computed via + // static_cast(std::floor(x)). With a constant-valued image the + // expected output is 1.0 regardless of which reflected pixel is hit. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("linear")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 2}; + std::initializer_list X_data{TypeParam(1.0f), TypeParam(1.0f), + TypeParam(1.0f), TypeParam(1.0f)}; + + std::initializer_list Grid_shape{1, 1, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(1.0e+20f), TypeParam(1.0e+20f), + TypeParam(-1.0e+20f), TypeParam(-1.0e+20f)}; + + std::initializer_list Y_shape{1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // With a constant-valued image any sampled pixel is 1.0. + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TEST(GridSampleCustomTestFloatOnly, test_grid_sample_20_4D_cubic_reflection_extreme_coords) { + // Cubic interpolation path: the 4x4 neighborhood is computed via + // static_cast(std::floor(x)) - 1, exercising the same UB-prone cast. + // Cubic only supports float (T1 type constraint), so this test is float-only. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("cubic")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 4, 4}; + std::initializer_list X_data{ + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f}; + + std::initializer_list Grid_shape{1, 1, 1, 2}; + std::initializer_list Grid_data{1.0e+10f, -1.0e+10f}; + + std::initializer_list Y_shape{1, 1, 1, 1}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // Constant-valued image: cubic interpolation of all-1.0 neighborhood is 1.0. + test.AddOutput("Y", Y_shape, {1.0f}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_5D_nearest_reflection_extreme_coords) { + // 3D / 5-D input: the reflection branch in PixelAtGrid3D received the same + // safety clamp as PixelAtGrid; this test exercises that path. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 2, 2}; + std::initializer_list X_data{ + TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f)}; + + std::initializer_list Grid_shape{1, 1, 1, 2, 3}; + std::initializer_list Grid_data{ + TypeParam(1.0e+10f), TypeParam(1.0e+10f), TypeParam(1.0e+10f), + TypeParam(-1.0e+10f), TypeParam(-1.0e+10f), TypeParam(-1.0e+10f)}; + + std::initializer_list Y_shape{1, 1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_reflection_dim1_align_corners) { + // With a 1x1 image and align_corners=true, x_min == x_max so the reflection + // range is zero. GsReflect must not divide by zero on any out-of-range coord. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{1}); + + std::initializer_list X_shape{1, 1, 1, 1}; + std::initializer_list X_data{TypeParam(7.0f)}; + + std::initializer_list Grid_shape{1, 1, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(0.5f), TypeParam(0.5f), + TypeParam(-0.5f), TypeParam(-0.5f)}; + + std::initializer_list Y_shape{1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // The single input pixel is the only valid sample for any reflected coordinate. + test.AddOutput("Y", Y_shape, {TypeParam(7.0f), TypeParam(7.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_bilinear_border_extreme_coords) { + // Border padding mode is independently safe via std::clamp; this regression + // test ensures it stays safe with extreme grid coordinates. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("linear")); + test.AddAttribute("padding_mode", std::string("border")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 2}; + std::initializer_list X_data{TypeParam(1.0f), TypeParam(1.0f), + TypeParam(1.0f), TypeParam(1.0f)}; + + std::initializer_list Grid_shape{1, 1, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(1.0e+20f), TypeParam(1.0e+20f), + TypeParam(-1.0e+20f), TypeParam(-1.0e+20f)}; + + std::initializer_list Y_shape{1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // Constant-valued image: any clamped border sample is 1.0. + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_linear_zeros_extreme_nan_inf_coords) { + // Zeros padding is safe by design, but the IsSafeForInt64Conversion sanitization + // (in PrecomputeBilinearSamplePlan2D) replaces NaN/Inf/extreme values with the + // lower border (-0.5) before the floor cast. Verify the optimized bilinear + // zeros-padding fast path still produces a deterministic, well-defined output + // for those inputs. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("linear")); + test.AddAttribute("padding_mode", std::string("zeros")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 2}; + std::initializer_list X_data{TypeParam(1.0f), TypeParam(1.0f), + TypeParam(1.0f), TypeParam(1.0f)}; + + std::initializer_list Grid_shape{1, 1, 3, 2}; + std::initializer_list Grid_data{ + TypeParam(std::numeric_limits::quiet_NaN()), + TypeParam(std::numeric_limits::quiet_NaN()), + TypeParam(std::numeric_limits::infinity()), + TypeParam(-std::numeric_limits::infinity()), + TypeParam(1.0e+20f), TypeParam(-1.0e+20f)}; + + std::initializer_list Y_shape{1, 1, 1, 3}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // Each unsafe coord is sanitized to (-0.5, -0.5); only the bottom-right neighbor + // (pixel(0, 0) = 1.0) is in-bounds with weight 0.5*0.5 = 0.25, others are zeros. + test.AddOutput("Y", Y_shape, {TypeParam(0.25f), TypeParam(0.25f), TypeParam(0.25f)}); + RunCpuOnly(test); +} + +TEST(GridSampleCustomTestFloatOnly, test_grid_sample_20_4D_cubic_reflection_nan_inf_coords) { + // Cubic interpolation also goes through static_cast(std::floor(x)) - 1. + // Cover the NaN / +Inf / -Inf paths through the cubic kernel; constant-valued + // image makes the output trivially 1.0 regardless of which 4x4 neighborhood is + // selected after sanitization. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("cubic")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 4, 4}; + std::initializer_list X_data{ + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f}; + + std::initializer_list Grid_shape{1, 1, 3, 2}; + std::initializer_list Grid_data{ + std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + std::numeric_limits::infinity()}; + + std::initializer_list Y_shape{1, 1, 1, 3}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, {1.0f, 1.0f, 1.0f}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_5D_linear_reflection_extreme_coords) { + // 3D trilinear path: exercises std::floor cast for x, y, and z with extreme + // finite coordinates. Sanitization replaces the unsafe components with the + // lower borders before any int64 cast. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("linear")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 2, 2}; + std::initializer_list X_data{ + TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f)}; + + std::initializer_list Grid_shape{1, 1, 1, 2, 3}; + std::initializer_list Grid_data{ + TypeParam(1.0e+20f), TypeParam(1.0e+20f), TypeParam(1.0e+20f), + TypeParam(-1.0e+20f), TypeParam(-1.0e+20f), TypeParam(-1.0e+20f)}; + + std::initializer_list Y_shape{1, 1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // Constant-valued image: trilinear interpolation of all-1.0 neighborhood is 1.0. + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_5D_nearest_reflection_nan_inf_coords) { + // 3D path with NaN / Inf / -Inf in different spatial dimensions. Verifies + // that the 3D sanitization branch handles non-finite values along any axis. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 2, 2}; + std::initializer_list X_data{ + TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f)}; + + std::initializer_list Grid_shape{1, 1, 1, 2, 3}; + std::initializer_list Grid_data{ + TypeParam(std::numeric_limits::quiet_NaN()), + TypeParam(std::numeric_limits::infinity()), + TypeParam(-std::numeric_limits::infinity()), + TypeParam(std::numeric_limits::infinity()), + TypeParam(std::numeric_limits::quiet_NaN()), + TypeParam(-std::numeric_limits::infinity())}; + + std::initializer_list Y_shape{1, 1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_reflection_align_corners_extreme) { + // align_corners=1 on a non-degenerate (3x3) image. With align_corners=1 the + // lower border becomes 0 (not -0.5), so unsafe coords are sanitized to (0, 0) + // and resolve to the top-left pixel of the image. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{1}); + + std::initializer_list X_shape{1, 1, 3, 3}; + std::initializer_list X_data{ + TypeParam(1.0f), TypeParam(2.0f), TypeParam(3.0f), + TypeParam(4.0f), TypeParam(5.0f), TypeParam(6.0f), + TypeParam(7.0f), TypeParam(8.0f), TypeParam(9.0f)}; + + std::initializer_list Grid_shape{1, 1, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(1.0e+20f), TypeParam(1.0e+20f), + TypeParam(std::numeric_limits::quiet_NaN()), + TypeParam(-std::numeric_limits::infinity())}; + + std::initializer_list Y_shape{1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // Both unsafe coords sanitize to (0, 0) -> pixel(0, 0) = 1.0. + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_reflection_negative_only_extreme) { + // Asymmetry: all extreme coordinates are negative. Sanitization replaces them + // with x_min/y_min = -0.5, and nearbyint(-0.5) rounds to 0 (banker's rounding), + // so each output samples pixel(0, 0) of a non-constant image. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 3, 3}; + std::initializer_list X_data{ + TypeParam(1.0f), TypeParam(2.0f), TypeParam(3.0f), + TypeParam(4.0f), TypeParam(5.0f), TypeParam(6.0f), + TypeParam(7.0f), TypeParam(8.0f), TypeParam(9.0f)}; + + std::initializer_list Grid_shape{1, 1, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(-1.0e+20f), TypeParam(-1.0e+20f), + TypeParam(-std::numeric_limits::infinity()), + TypeParam(-std::numeric_limits::infinity())}; + + std::initializer_list Y_shape{1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + // Sanitized to (-0.5, -0.5); nearbyint -> (0, 0); pixel(0, 0) = 1.0. + test.AddOutput("Y", Y_shape, {TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_reflection_mixed_normal_pathological) { + // Mixed normal and pathological grid coordinates in the same call. Validates + // that the sanitization only replaces unsafe values and leaves normal grid + // points untouched, sampling them correctly from a non-constant image. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{1}); + + std::initializer_list X_shape{1, 1, 3, 3}; + std::initializer_list X_data{ + TypeParam(1.0f), TypeParam(2.0f), TypeParam(3.0f), + TypeParam(4.0f), TypeParam(5.0f), TypeParam(6.0f), + TypeParam(7.0f), TypeParam(8.0f), TypeParam(9.0f)}; + + // align_corners=1 on a 3x3 image maps normalized [-1, 1] -> pixel coords [0, 2]. + // Mix four points: center, top-left, NaN (sanitized -> (0,0)), and asymmetric + // extreme (sanitized -> (0,0)). + std::initializer_list Grid_shape{1, 1, 4, 2}; + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), // -> pixel(1, 1) = 5 + TypeParam(-1.0f), TypeParam(-1.0f), // -> pixel(0, 0) = 1 + TypeParam(std::numeric_limits::quiet_NaN()), + TypeParam(std::numeric_limits::quiet_NaN()), // sanitized -> pixel(0, 0) = 1 + TypeParam(1.0e+20f), TypeParam(-1.0e+20f)}; // sanitized -> pixel(0, 0) = 1 + + std::initializer_list Y_shape{1, 1, 1, 4}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, + {TypeParam(5.0f), TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f)}); + RunCpuOnly(test); +} + +} // namespace test +} // namespace onnxruntime diff --git a/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.inc b/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.inc deleted file mode 100644 index 2423d7f120b20..0000000000000 --- a/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.inc +++ /dev/null @@ -1,73 +0,0 @@ - -// Custom tests are kept in a separate include to avoid regenerating the main file. - -template -class GridSampleCustomTest : public ::testing::Test { -}; - -using GridSampleCustomTestTypes = ::testing::Types; -TYPED_TEST_SUITE(GridSampleCustomTest, GridSampleCustomTestTypes); - -TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_linear_zeros_mixed_bounds_right_bottom) { - // Crafts grid points that mix fully in-bounds sampling with cases where either the right, bottom, - // or both neighbors fall outside the source image so zero padding must be applied. This ensures - // the optimized bilinear fast path matches the generic implementation for boundary handling. - OpTester test("GridSample", 20); - std::string mode = "linear"; - std::string padding_mode = "zeros"; - int64_t align_corners = 0; - std::initializer_list X_shape{1, 1, 2, 2}; - std::initializer_list X_data{TypeParam(1.0f), TypeParam(2.0f), TypeParam(3.0f), TypeParam(4.0f)}; - std::initializer_list Grid_shape{1, 2, 2, 2}; - // (nx, ny) pairs: center (in-bounds), right edge (x out), bottom edge (y out), corner (both out) - std::initializer_list Grid_data{ - TypeParam(0.0f), TypeParam(0.0f), // center (all neighbors in bounds) - TypeParam(0.9f), TypeParam(0.0f), // near right edge (right neighbors out of bounds) - TypeParam(0.0f), TypeParam(0.9f), // near bottom edge (bottom neighbors out) - TypeParam(0.9f), TypeParam(0.9f)}; // near bottom-right corner (both right and bottom neighbors out) - std::initializer_list Y_shape{1, 1, 2, 2}; - std::initializer_list Y_data{ - TypeParam(2.5f), // all neighbors in bounds - TypeParam(1.8f), // right neighbors partially out-of-bounds - TypeParam(2.1f), // bottom neighbors partially out-of-bounds - TypeParam(1.44f)}; // both right and bottom neighbors out-of-bounds - test.AddInput("X", X_shape, X_data); - test.AddInput("Grid", Grid_shape, Grid_data); - test.AddAttribute("mode", mode); - test.AddAttribute("padding_mode", padding_mode); - test.AddAttribute("align_corners", align_corners); - test.AddOutput("Y", Y_shape, Y_data); - RunTests(test, GetExecutionProviders()); -} - -TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_linear_zeros_mixed_bounds_left_top) { - // Similar to test_grid_sample_20_4D_linear_zeros_mixed_bounds_right_bottom but focuses on left/top boundary cases, - // where the left and/or top neighbors fall outside the source image and zero padding must be applied. - // This ensures the optimized bilinear fast path correctly handles left/top boundary conditions. - OpTester test("GridSample", 20); - std::string mode = "linear"; - std::string padding_mode = "zeros"; - int64_t align_corners = 0; - std::initializer_list X_shape{1, 1, 2, 2}; - std::initializer_list X_data{TypeParam(1.0f), TypeParam(2.0f), TypeParam(3.0f), TypeParam(4.0f)}; - std::initializer_list Grid_shape{1, 2, 2, 2}; - // (nx, ny) pairs: center (in-bounds), left edge (x out), top edge (y out), corner (both out) - std::initializer_list Grid_data{ - TypeParam(0.0f), TypeParam(0.0f), // center (all neighbors in bounds) - TypeParam(-0.9f), TypeParam(0.0f), // near left edge (left neighbors out of bounds) - TypeParam(0.0f), TypeParam(-0.9f), // near top edge (top neighbors out of bounds) - TypeParam(-0.9f), TypeParam(-0.9f)}; // near top-left corner (both left and top neighbors out of bounds) - std::initializer_list Y_shape{1, 1, 2, 2}; - std::initializer_list Y_data{ - TypeParam(2.5f), // all neighbors in bounds - TypeParam(1.2f), // left neighbors partially out-of-bounds - TypeParam(0.9f), // top neighbors partially out-of-bounds - TypeParam(0.36f)}; // both left and top neighbors out-of-bounds - test.AddInput("X", X_shape, X_data); - test.AddInput("Grid", Grid_shape, Grid_data); - test.AddAttribute("mode", mode); - test.AddAttribute("padding_mode", padding_mode); - test.AddAttribute("align_corners", align_corners); - test.AddOutput("Y", Y_shape, Y_data); - RunTests(test, GetExecutionProviders()); -}