From 898110cf7cb73f6ffa276b63f9fc7ea0a32c4ba9 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 3 Jun 2026 15:50:28 -0700 Subject: [PATCH 1/5] Add more validation --- .../core/providers/cpu/tensor/upsamplebase.h | 2 + .../providers/cpu/tensor/resize_op_test.cc | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/onnxruntime/core/providers/cpu/tensor/upsamplebase.h b/onnxruntime/core/providers/cpu/tensor/upsamplebase.h index d5d6ea614c688..430ca2cfa62ac 100644 --- a/onnxruntime/core/providers/cpu/tensor/upsamplebase.h +++ b/onnxruntime/core/providers/cpu/tensor/upsamplebase.h @@ -598,6 +598,8 @@ class UpsampleBase { "Resize: input tensor's rank does not match the output tensor's rank."); if (axes_.size()) { + ORT_RETURN_IF_NOT(axes_.size() == size_span.size(), + "Number of elements in sizes should be equal to number of axes."); output_dims.assign(input_dims.begin(), input_dims.end()); const int64_t rank = static_cast(output_dims.size()); for (size_t i = 0; i < axes_.size(); i++) { diff --git a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc index 4b4d9e9ddd1ba..85d726e858fc6 100644 --- a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc @@ -3187,6 +3187,85 @@ TEST(ResizeOpTest, Axes_OutOfRange_18) { {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider}); } +// Negative axis below the valid range must be rejected before being used as a scatter index. +TEST(ResizeOpTest, Axes_NegativeOutOfRange_18) { + std::vector X(16 * 4); + std::iota(X.begin(), X.end(), 0.f); + std::vector roi{}; + std::vector scales{0.75f, 0.75f, 0.75f}; + std::vector axes{2, 3, -6}; + std::vector Y(16 * 4, 0.0f); + + OpTester test("Resize", 18); + test.AddShapeToTensorData(false); + test.AddAttribute("mode", "linear"); + test.AddAttribute>("axes", axes); + + test.AddInput("X", {1, 1, 4, 4, 4}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {int64_t(scales.size())}, scales); + test.AddOutput("Y", {1, 1, 4, 4, 4}, Y); + + // TensorRT, QNN, and DML do not exercise the CPU axes-validation path. + test.Run(OpTester::ExpectResult::kExpectFailure, + "axis -6 is not in valid range [-5,4]", + {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider}); +} + +// Valid negative axes (within [-rank, -1]) must still produce correct output. +TEST(ResizeOpTest, Axes_NegativeInRange_18) { + std::vector X(16 * 4); + std::iota(X.begin(), X.end(), 0.f); + std::vector Y = {3.5f, 4.8333335f, 6.1666665f, 8.833333f, 10.166667f, 11.5f, 14.166667f, + 15.5f, 16.833334f, 24.833334f, 26.166666f, 27.5f, 30.166666f, 31.5f, + 32.833332f, 35.5f, 36.833332f, 38.166668f, 46.166668f, 47.5f, 48.833332f, + 51.5f, 52.833332f, 54.166668f, 56.833332f, 58.166668f, 59.5f}; + std::vector roi{}; + std::vector scales{3 / 4.0f, 3 / 4.0f, 3 / 4.0f}; + std::vector output_shape{1, 1, 3, 3, 3}; + std::vector axes{-3, -2, -1}; + + OpTester test("Resize", 18); + test.AddAttribute("exclude_outside", 0LL); + test.AddAttribute>("axes", axes); + test.AddAttribute("antialias", 0LL); + test.AddAttribute("mode", "linear"); + + test.AddInput("X", {1, 1, 4, 4, 4}, X); + test.AddInput("roi", {int64_t(roi.size())}, roi); + test.AddInput("scales", {int64_t(scales.size())}, scales, true); + + test.AddOutput("Y", output_shape, Y); + test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider, kQnnExecutionProvider}); +} + +// When axes is provided, the sizes input length must match axes length so the scatter +// loop does not read past the end of sizes. +TEST(ResizeOpTest, Axes_and_Sizes_CountMismatch_18) { + std::vector X(16 * 4); + std::iota(X.begin(), X.end(), 0.f); + std::vector roi{}; + std::vector scales{}; + std::vector sizes{3, 3}; + std::vector axes{2, 3, 4}; + std::vector Y(16 * 4, 0.0f); + + OpTester test("Resize", 18); + test.AddShapeToTensorData(false); + test.AddAttribute("mode", "linear"); + test.AddAttribute>("axes", axes); + + test.AddInput("X", {1, 1, 4, 4, 4}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("", {0}, scales); + test.AddInput("sizes", {int64_t(sizes.size())}, sizes); + test.AddOutput("Y", {1, 1, 4, 4, 4}, Y); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "Number of elements in sizes should be equal to number of axes.", + {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider}); +} + TEST(ResizeOpTest, Sizes_RankMismatch_13) { OpTester test("Resize", 13); test.AddShapeToTensorData(false); From 4adf9a813f8e1f7816582dc8b6748ff446a4e766 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 3 Jun 2026 15:57:49 -0700 Subject: [PATCH 2/5] Add more validations and fix UB --- .../core/providers/cpu/tensor/upsample.cc | 2 +- .../core/providers/cpu/tensor/upsamplebase.h | 18 +++- .../core/providers/cuda/tensor/upsample.cc | 2 +- .../core/providers/webgpu/tensor/upsample.cc | 2 +- .../providers/cpu/tensor/resize_op_test.cc | 98 +++++++++++++++++++ 5 files changed, 118 insertions(+), 4 deletions(-) diff --git a/onnxruntime/core/providers/cpu/tensor/upsample.cc b/onnxruntime/core/providers/cpu/tensor/upsample.cc index 18bb38622c8cd..5576e5dd07235 100644 --- a/onnxruntime/core/providers/cpu/tensor/upsample.cc +++ b/onnxruntime/core/providers/cpu/tensor/upsample.cc @@ -1398,7 +1398,7 @@ Status Upsample::Compute(OpKernelContext* context) const { } } - ComputeROIWithAxes(roi_array, input_dims.size()); + ORT_RETURN_IF_ERROR(ComputeROIWithAxes(roi_array, input_dims.size())); // Get scales data InlinedVector scales_array(input_dims.size()); diff --git a/onnxruntime/core/providers/cpu/tensor/upsamplebase.h b/onnxruntime/core/providers/cpu/tensor/upsamplebase.h index 430ca2cfa62ac..c7c93a51696ab 100644 --- a/onnxruntime/core/providers/cpu/tensor/upsamplebase.h +++ b/onnxruntime/core/providers/cpu/tensor/upsamplebase.h @@ -233,6 +233,15 @@ class UpsampleBase { // guard against unit tests that can add an attribute auto axes = info.template GetAttrsOrDefault("axes"); axes_.assign(axes.cbegin(), axes.cend()); + // ONNX spec requires axes entries to be unique. Detect duplicates as raw integers; range + // validation happens later in ParseScalesData / ParseSizesData when the rank is known. + if (axes_.size() > 1) { + TensorShapeVector sorted_axes(axes_); + std::sort(sorted_axes.begin(), sorted_axes.end()); + auto dup = std::adjacent_find(sorted_axes.begin(), sorted_axes.end()); + ORT_ENFORCE(dup == sorted_axes.end(), + "axes attribute must contain unique values, found duplicate ", *dup); + } } extrapolation_value_ = info.template GetAttrOrDefault("extrapolation_value", 0.0f); @@ -498,6 +507,9 @@ class UpsampleBase { } [[nodiscard]] Status ScalesValidation(gsl::span scales, const UpsampleMode mode) const { + for (auto& scale : scales) { + ORT_RETURN_IF_NOT(std::isfinite(scale), "Scale value must be finite."); + } if (!is_resize_) { for (auto& scale : scales) { ORT_RETURN_IF_NOT(scale >= 1, "Scale value should be greater than or equal to 1."); @@ -653,8 +665,11 @@ class UpsampleBase { // Roi is redefined in Opset-18, we have a concept of axes. // So we need to update it accordingly. - void ComputeROIWithAxes(InlinedVector& roi_array, size_t rank) const { + Status ComputeROIWithAxes(InlinedVector& roi_array, size_t rank) const { if (axes_.size()) { + ORT_RETURN_IF_NOT(roi_array.size() >= 2 * axes_.size(), + "roi input length (", roi_array.size(), + ") must be at least 2 * number of axes (", 2 * axes_.size(), ")."); InlinedVector roi_tmp(rank * 2, 0); for (size_t i = rank; i < rank * 2; ++i) { roi_tmp[i] = 1; @@ -668,6 +683,7 @@ class UpsampleBase { } roi_array.swap(roi_tmp); } + return Status::OK(); } public: diff --git a/onnxruntime/core/providers/cuda/tensor/upsample.cc b/onnxruntime/core/providers/cuda/tensor/upsample.cc index 6c75500350e2c..2aa1a7ac34c39 100644 --- a/onnxruntime/core/providers/cuda/tensor/upsample.cc +++ b/onnxruntime/core/providers/cuda/tensor/upsample.cc @@ -426,7 +426,7 @@ Status Upsample::ComputeInternal(OpKernelContext* context) const { } } - ComputeROIWithAxes(roi_array, input_dims.size()); + ORT_RETURN_IF_ERROR(ComputeROIWithAxes(roi_array, input_dims.size())); InlinedVector scales_array(input_dims.size()); // opset < 10 diff --git a/onnxruntime/core/providers/webgpu/tensor/upsample.cc b/onnxruntime/core/providers/webgpu/tensor/upsample.cc index 9b88731832fc4..b7e0d6e6c0fa7 100644 --- a/onnxruntime/core/providers/webgpu/tensor/upsample.cc +++ b/onnxruntime/core/providers/webgpu/tensor/upsample.cc @@ -86,7 +86,7 @@ Status Upsample::ComputeInternal(ComputeContext& context) const { } } - ComputeROIWithAxes(roi_array, input_dims.size()); + ORT_RETURN_IF_ERROR(ComputeROIWithAxes(roi_array, input_dims.size())); InlinedVector scales_array(input_dims.size()); // opset < 10 diff --git a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc index 85d726e858fc6..2778acf3020ce 100644 --- a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include +#include #include "gtest/gtest.h" #include "test/providers/provider_test_utils.h" #include "test/util/include/default_providers.h" @@ -3266,6 +3267,103 @@ TEST(ResizeOpTest, Axes_and_Sizes_CountMismatch_18) { {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider}); } +// Non-finite scale values must be rejected before being multiplied into output dimensions. +TEST(ResizeOpTest, Scales_NaN_Rejected_18) { + std::vector X(16, 1.0f); + std::vector roi{}; + std::vector scales{1.0f, 1.0f, std::numeric_limits::quiet_NaN(), 2.0f}; + std::vector Y(16, 0.0f); + + OpTester test("Resize", 18); + test.AddShapeToTensorData(false); + test.AddAttribute("mode", "linear"); + + test.AddInput("X", {1, 1, 4, 4}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {int64_t(scales.size())}, scales); + test.AddOutput("Y", {1, 1, 4, 8}, Y); + + // EPs that do their own validation or do not exercise the CPU ScalesValidation path. + test.Run(OpTester::ExpectResult::kExpectFailure, "Scale value must be finite.", + {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider, + kOpenVINOExecutionProvider}); +} + +TEST(ResizeOpTest, Scales_PositiveInf_Rejected_18) { + std::vector X(16, 1.0f); + std::vector roi{}; + std::vector scales{1.0f, 1.0f, 2.0f, std::numeric_limits::infinity()}; + std::vector Y(16, 0.0f); + + OpTester test("Resize", 18); + test.AddShapeToTensorData(false); + test.AddAttribute("mode", "linear"); + + test.AddInput("X", {1, 1, 4, 4}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {int64_t(scales.size())}, scales); + test.AddOutput("Y", {1, 1, 8, 4}, Y); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Scale value must be finite.", + {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider, + kOpenVINOExecutionProvider}); +} + +// Duplicate values in the axes attribute violate the ONNX spec and are rejected at construction. +// The constructor uses ORT_ENFORCE, which throws; the test is excluded from no-exception builds. +#if !defined(ORT_NO_EXCEPTIONS) +TEST(ResizeOpTest, Axes_Duplicate_Rejected_18) { + std::vector X(16 * 4); + std::iota(X.begin(), X.end(), 0.f); + std::vector roi{}; + std::vector scales{0.75f, 0.75f, 0.75f}; + std::vector axes{2, 3, 2}; + std::vector Y(16 * 4, 0.0f); + + OpTester test("Resize", 18); + test.AddShapeToTensorData(false); + test.AddAttribute("mode", "linear"); + test.AddAttribute>("axes", axes); + + test.AddInput("X", {1, 1, 4, 4, 4}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {int64_t(scales.size())}, scales); + test.AddOutput("Y", {1, 1, 4, 4, 4}, Y); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "axes attribute must contain unique values, found duplicate 2", + {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider}); +} +#endif + +// When axes is provided in tf_crop_and_resize mode, the roi input must contain at least +// 2 * len(axes) entries so the per-axis start/end pairs can be read safely. +TEST(ResizeOpTest, Roi_TooShortForAxes_18) { + std::vector X(16 * 4); + std::iota(X.begin(), X.end(), 0.f); + // roi length 4 (= 2 * 2), but axes has 3 entries, so 2 * len(axes) = 6 are required. + std::vector roi{0.0f, 0.0f, 1.0f, 1.0f}; + std::vector scales{0.75f, 0.75f, 0.75f}; + std::vector axes{2, 3, 4}; + std::vector Y(16 * 4, 0.0f); + + OpTester test("Resize", 18); + test.AddShapeToTensorData(false); + test.AddAttribute("mode", "linear"); + test.AddAttribute("coordinate_transformation_mode", "tf_crop_and_resize"); + test.AddAttribute>("axes", axes); + + test.AddInput("X", {1, 1, 4, 4, 4}, X); + test.AddInput("roi", {int64_t(roi.size())}, roi); + test.AddInput("scales", {int64_t(scales.size())}, scales); + test.AddOutput("Y", {1, 1, 4, 4, 4}, Y); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "roi input length", + {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider, + kOpenVINOExecutionProvider}); +} + TEST(ResizeOpTest, Sizes_RankMismatch_13) { OpTester test("Resize", 13); test.AddShapeToTensorData(false); From 80d4a6d7ee6a1f5843b7276b3bcb8e5676333996 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 3 Jun 2026 16:22:51 -0700 Subject: [PATCH 3/5] Review comments --- .../core/providers/cpu/tensor/upsamplebase.h | 54 ++++++++++++------- .../providers/cpu/tensor/resize_op_test.cc | 34 ++++++++++-- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/onnxruntime/core/providers/cpu/tensor/upsamplebase.h b/onnxruntime/core/providers/cpu/tensor/upsamplebase.h index c7c93a51696ab..838717a9186b5 100644 --- a/onnxruntime/core/providers/cpu/tensor/upsamplebase.h +++ b/onnxruntime/core/providers/cpu/tensor/upsamplebase.h @@ -233,15 +233,9 @@ class UpsampleBase { // guard against unit tests that can add an attribute auto axes = info.template GetAttrsOrDefault("axes"); axes_.assign(axes.cbegin(), axes.cend()); - // ONNX spec requires axes entries to be unique. Detect duplicates as raw integers; range - // validation happens later in ParseScalesData / ParseSizesData when the rank is known. - if (axes_.size() > 1) { - TensorShapeVector sorted_axes(axes_); - std::sort(sorted_axes.begin(), sorted_axes.end()); - auto dup = std::adjacent_find(sorted_axes.begin(), sorted_axes.end()); - ORT_ENFORCE(dup == sorted_axes.end(), - "axes attribute must contain unique values, found duplicate ", *dup); - } + // Uniqueness of axes is enforced after negative-axis normalization in + // ValidateAndNormalizeAxes, so two raw entries that collide after canonicalization + // (e.g. {-1, rank-1}) are still rejected. } extrapolation_value_ = info.template GetAttrOrDefault("extrapolation_value", 0.0f); @@ -506,6 +500,27 @@ class UpsampleBase { } } + // Resolve negative entries in axes_ against the supplied rank, verify each is in range, + // and verify that no two entries collide after normalization. Populates normalized_axes + // with the canonical non-negative indices in the original order. + Status ValidateAndNormalizeAxes(int64_t rank, TensorShapeVector& normalized_axes) const { + ORT_RETURN_IF_NOT(rank > 0, "Rank must be positive when axes is provided."); + normalized_axes.clear(); + normalized_axes.reserve(axes_.size()); + InlinedVector seen(static_cast(rank), false); + for (int64_t raw_axis : axes_) { + ORT_RETURN_IF_NOT(IsAxisInRange(raw_axis, rank), "axis ", raw_axis, + " is not in valid range [-", rank, ",", rank - 1, "]"); + const int64_t axis = raw_axis < 0 ? raw_axis + rank : raw_axis; + const auto idx = static_cast(axis); + ORT_RETURN_IF(seen[idx], "axes attribute contains duplicate axis ", axis, + " after negative-axis normalization (rank=", rank, ")."); + seen[idx] = true; + normalized_axes.push_back(axis); + } + return Status::OK(); + } + [[nodiscard]] Status ScalesValidation(gsl::span scales, const UpsampleMode mode) const { for (auto& scale : scales) { ORT_RETURN_IF_NOT(std::isfinite(scale), "Scale value must be finite."); @@ -583,9 +598,10 @@ class UpsampleBase { "Number of elements in scales should be equal to number of axes."); InlinedVector new_scales(size_t(rank), 1.0f); - for (size_t i = 0; i < axes_.size(); i++) { - const int64_t axis = HandleNegativeAxis(axes_[i], rank); - new_scales[static_cast(axis)] = scales[i]; + TensorShapeVector normalized_axes; + ORT_RETURN_IF_ERROR(ValidateAndNormalizeAxes(rank, normalized_axes)); + for (size_t i = 0; i < normalized_axes.size(); i++) { + new_scales[static_cast(normalized_axes[i])] = scales[i]; } scales.swap(new_scales); } @@ -614,9 +630,10 @@ class UpsampleBase { "Number of elements in sizes should be equal to number of axes."); output_dims.assign(input_dims.begin(), input_dims.end()); const int64_t rank = static_cast(output_dims.size()); - for (size_t i = 0; i < axes_.size(); i++) { - const int64_t axis = HandleNegativeAxis(axes_[i], rank); - output_dims[static_cast(axis)] = size_span[i]; + TensorShapeVector normalized_axes; + ORT_RETURN_IF_ERROR(ValidateAndNormalizeAxes(rank, normalized_axes)); + for (size_t i = 0; i < normalized_axes.size(); i++) { + output_dims[static_cast(normalized_axes[i])] = size_span[i]; } } else { std::copy(size_span.begin(), size_span.end(), output_dims.begin()); @@ -675,9 +692,10 @@ class UpsampleBase { roi_tmp[i] = 1; } const int64_t rank_i64 = static_cast(rank); - for (size_t i = 0; i < axes_.size(); i++) { - const int64_t axis = HandleNegativeAxis(axes_[i], rank_i64); - auto v_in_axes = static_cast(axis); + TensorShapeVector normalized_axes; + ORT_RETURN_IF_ERROR(ValidateAndNormalizeAxes(rank_i64, normalized_axes)); + for (size_t i = 0; i < normalized_axes.size(); i++) { + const auto v_in_axes = static_cast(normalized_axes[i]); roi_tmp[v_in_axes] = (roi_array[i]); roi_tmp[rank + v_in_axes] = (roi_array[axes_.size() + i]); } diff --git a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc index 2778acf3020ce..dbaf52506680d 100644 --- a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc @@ -3309,9 +3309,9 @@ TEST(ResizeOpTest, Scales_PositiveInf_Rejected_18) { kOpenVINOExecutionProvider}); } -// Duplicate values in the axes attribute violate the ONNX spec and are rejected at construction. -// The constructor uses ORT_ENFORCE, which throws; the test is excluded from no-exception builds. -#if !defined(ORT_NO_EXCEPTIONS) +// Duplicate values in the axes attribute violate the ONNX spec. The check runs after +// negative-axis normalization, so it covers both raw duplicates and pairs that collide +// only after canonicalization (e.g. {-1, rank-1}). TEST(ResizeOpTest, Axes_Duplicate_Rejected_18) { std::vector X(16 * 4); std::iota(X.begin(), X.end(), 0.f); @@ -3331,10 +3331,34 @@ TEST(ResizeOpTest, Axes_Duplicate_Rejected_18) { test.AddOutput("Y", {1, 1, 4, 4, 4}, Y); test.Run(OpTester::ExpectResult::kExpectFailure, - "axes attribute must contain unique values, found duplicate 2", + "axes attribute contains duplicate axis 2", + {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider}); +} + +// Negative and positive axis entries that resolve to the same canonical index must be +// rejected. For rank=5, axes={-1, 4} both map to 4. +TEST(ResizeOpTest, Axes_Duplicate_AfterNormalization_Rejected_18) { + std::vector X(16 * 4); + std::iota(X.begin(), X.end(), 0.f); + std::vector roi{}; + std::vector scales{0.75f, 0.75f}; + std::vector axes{-1, 4}; + std::vector Y(16 * 4, 0.0f); + + OpTester test("Resize", 18); + test.AddShapeToTensorData(false); + test.AddAttribute("mode", "linear"); + test.AddAttribute>("axes", axes); + + test.AddInput("X", {1, 1, 4, 4, 4}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {int64_t(scales.size())}, scales); + test.AddOutput("Y", {1, 1, 4, 4, 4}, Y); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "axes attribute contains duplicate axis 4", {kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider}); } -#endif // When axes is provided in tf_crop_and_resize mode, the roi input must contain at least // 2 * len(axes) entries so the per-axis start/end pairs can be read safely. From e6c6608a165c1a3ae56b0a0ff82ddcebe8d208f8 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 3 Jun 2026 16:24:58 -0700 Subject: [PATCH 4/5] Fix test shapes --- onnxruntime/test/providers/cpu/tensor/resize_op_test.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc index dbaf52506680d..fd8c7b863b6a7 100644 --- a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc @@ -3272,7 +3272,7 @@ TEST(ResizeOpTest, Scales_NaN_Rejected_18) { std::vector X(16, 1.0f); std::vector roi{}; std::vector scales{1.0f, 1.0f, std::numeric_limits::quiet_NaN(), 2.0f}; - std::vector Y(16, 0.0f); + std::vector Y(32, 0.0f); OpTester test("Resize", 18); test.AddShapeToTensorData(false); @@ -3293,7 +3293,7 @@ TEST(ResizeOpTest, Scales_PositiveInf_Rejected_18) { std::vector X(16, 1.0f); std::vector roi{}; std::vector scales{1.0f, 1.0f, 2.0f, std::numeric_limits::infinity()}; - std::vector Y(16, 0.0f); + std::vector Y(32, 0.0f); OpTester test("Resize", 18); test.AddShapeToTensorData(false); From 67f63bc27e9a456e2ab0bfd817715b8cc8806b82 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 4 Jun 2026 10:16:52 -0700 Subject: [PATCH 5/5] Address comments and CI failure --- .../core/providers/cpu/tensor/upsamplebase.h | 45 ++++++++++++++----- .../providers/cpu/tensor/resize_op_test.cc | 5 ++- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/onnxruntime/core/providers/cpu/tensor/upsamplebase.h b/onnxruntime/core/providers/cpu/tensor/upsamplebase.h index 838717a9186b5..1b60580a8c86d 100644 --- a/onnxruntime/core/providers/cpu/tensor/upsamplebase.h +++ b/onnxruntime/core/providers/cpu/tensor/upsamplebase.h @@ -304,6 +304,12 @@ class UpsampleBase { auto tensor_info = type_info.GetTensorTypeAndShapeInfo(); rank = static_cast(tensor_info.GetDimensionsCount()); } + // When axes are present and the input rank is statically known, validate axes and cache + // the normalized form so the inference hot path can skip the bitmap allocation. + if (rank > 0 && !axes_.empty()) { + ORT_THROW_IF_ERROR(ValidateAndNormalizeAxes(rank, normalized_axes_)); + normalized_axes_rank_ = rank; + } if (get_scale && scale->Shape().Size() > 0 && ((opset < 18) || (rank > 0 && opset >= 18))) { ORT_THROW_IF_ERROR(ParseScalesData(scale, scales_, rank)); scales_cached_ = true; @@ -338,6 +344,8 @@ class UpsampleBase { InlinedVector scales_; InlinedVector roi_; TensorShapeVector axes_; + TensorShapeVector normalized_axes_; + int64_t normalized_axes_rank_ = -1; bool scales_cached_; bool roi_cached_; @@ -502,9 +510,15 @@ class UpsampleBase { // Resolve negative entries in axes_ against the supplied rank, verify each is in range, // and verify that no two entries collide after normalization. Populates normalized_axes - // with the canonical non-negative indices in the original order. + // with the canonical non-negative indices in the original order. When the input rank is + // statically known, the constructor pre-populates normalized_axes_ and this call becomes + // a copy on the inference hot path. Status ValidateAndNormalizeAxes(int64_t rank, TensorShapeVector& normalized_axes) const { ORT_RETURN_IF_NOT(rank > 0, "Rank must be positive when axes is provided."); + if (rank == normalized_axes_rank_) { + normalized_axes = normalized_axes_; + return Status::OK(); + } normalized_axes.clear(); normalized_axes.reserve(axes_.size()); InlinedVector seen(static_cast(rank), false); @@ -684,22 +698,29 @@ class UpsampleBase { // So we need to update it accordingly. Status ComputeROIWithAxes(InlinedVector& roi_array, size_t rank) const { if (axes_.size()) { - ORT_RETURN_IF_NOT(roi_array.size() >= 2 * axes_.size(), + // Per-axis ROI is supplied as a flat [start..., end...] of length 2 * len(axes). + // The default-ROI path fills roi_array with [0..., 1...] of length 2 * rank, in which + // case the canonical full-rank ROI is already correct and no scatter is needed. + const bool per_axis_roi = roi_array.size() == 2 * axes_.size(); + ORT_RETURN_IF_NOT(per_axis_roi || roi_array.size() == 2 * rank, "roi input length (", roi_array.size(), - ") must be at least 2 * number of axes (", 2 * axes_.size(), ")."); - InlinedVector roi_tmp(rank * 2, 0); - for (size_t i = rank; i < rank * 2; ++i) { - roi_tmp[i] = 1; - } + ") must be either 2 * rank (", 2 * rank, + ") or 2 * number of axes (", 2 * axes_.size(), ")."); const int64_t rank_i64 = static_cast(rank); TensorShapeVector normalized_axes; ORT_RETURN_IF_ERROR(ValidateAndNormalizeAxes(rank_i64, normalized_axes)); - for (size_t i = 0; i < normalized_axes.size(); i++) { - const auto v_in_axes = static_cast(normalized_axes[i]); - roi_tmp[v_in_axes] = (roi_array[i]); - roi_tmp[rank + v_in_axes] = (roi_array[axes_.size() + i]); + if (per_axis_roi) { + InlinedVector roi_tmp(rank * 2, 0); + for (size_t i = rank; i < rank * 2; ++i) { + roi_tmp[i] = 1; + } + for (size_t i = 0; i < normalized_axes.size(); i++) { + const auto v_in_axes = static_cast(normalized_axes[i]); + roi_tmp[v_in_axes] = roi_array[i]; + roi_tmp[rank + v_in_axes] = roi_array[axes_.size() + i]; + } + roi_array.swap(roi_tmp); } - roi_array.swap(roi_tmp); } return Status::OK(); } diff --git a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc index fd8c7b863b6a7..35d6ee3bbeb14 100644 --- a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc @@ -3237,7 +3237,10 @@ TEST(ResizeOpTest, Axes_NegativeInRange_18) { test.AddInput("scales", {int64_t(scales.size())}, scales, true); test.AddOutput("Y", output_shape, Y); - test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider, kQnnExecutionProvider}); + // OpenVINO EP's Resize importer does not normalize negative axes against the input rank, + // so it rejects models that the ONNX spec accepts. Tracked by GH issue #28788. + test.Run(OpTester::ExpectResult::kExpectSuccess, "", + {kTensorrtExecutionProvider, kQnnExecutionProvider, kOpenVINOExecutionProvider}); } // When axes is provided, the sizes input length must match axes length so the scatter