Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion onnxruntime/core/providers/cpu/tensor/upsample.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1398,7 +1398,7 @@ Status Upsample<T>::Compute(OpKernelContext* context) const {
}
}

ComputeROIWithAxes(roi_array, input_dims.size());
ORT_RETURN_IF_ERROR(ComputeROIWithAxes(roi_array, input_dims.size()));
// Get scales data
InlinedVector<float> scales_array(input_dims.size());

Expand Down
20 changes: 19 additions & 1 deletion onnxruntime/core/providers/cpu/tensor/upsamplebase.h
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,15 @@ class UpsampleBase {
// guard against unit tests that can add an attribute
auto axes = info.template GetAttrsOrDefault<int64_t>("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<float>("extrapolation_value", 0.0f);
Expand Down Expand Up @@ -498,6 +507,9 @@ class UpsampleBase {
}

[[nodiscard]] Status ScalesValidation(gsl::span<const float> 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.");
Expand Down Expand Up @@ -598,6 +610,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<int64_t>(output_dims.size());
for (size_t i = 0; i < axes_.size(); i++) {
Expand Down Expand Up @@ -651,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<float>& roi_array, size_t rank) const {
Status ComputeROIWithAxes(InlinedVector<float>& 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(), ").");
Comment thread
yuslepukhin marked this conversation as resolved.
Outdated
InlinedVector<float> roi_tmp(rank * 2, 0);
for (size_t i = rank; i < rank * 2; ++i) {
roi_tmp[i] = 1;
Expand All @@ -666,6 +683,7 @@ class UpsampleBase {
}
roi_array.swap(roi_tmp);
}
return Status::OK();
}

public:
Expand Down
2 changes: 1 addition & 1 deletion onnxruntime/core/providers/cuda/tensor/upsample.cc
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ Status Upsample<T>::ComputeInternal(OpKernelContext* context) const {
}
}

ComputeROIWithAxes(roi_array, input_dims.size());
ORT_RETURN_IF_ERROR(ComputeROIWithAxes(roi_array, input_dims.size()));

InlinedVector<float> scales_array(input_dims.size());
// opset < 10
Expand Down
2 changes: 1 addition & 1 deletion onnxruntime/core/providers/webgpu/tensor/upsample.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<float> scales_array(input_dims.size());
// opset < 10
Expand Down
177 changes: 177 additions & 0 deletions onnxruntime/test/providers/cpu/tensor/resize_op_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

#include <exception>
#include <limits>
#include "gtest/gtest.h"
#include "test/providers/provider_test_utils.h"
#include "test/util/include/default_providers.h"
Expand Down Expand Up @@ -3187,6 +3188,182 @@ 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<float> X(16 * 4);
std::iota(X.begin(), X.end(), 0.f);
std::vector<float> roi{};
std::vector<float> scales{0.75f, 0.75f, 0.75f};
std::vector<int64_t> axes{2, 3, -6};
std::vector<float> Y(16 * 4, 0.0f);

OpTester test("Resize", 18);
test.AddShapeToTensorData(false);
test.AddAttribute("mode", "linear");
test.AddAttribute<std::vector<int64_t>>("axes", axes);

test.AddInput<float>("X", {1, 1, 4, 4, 4}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("scales", {int64_t(scales.size())}, scales);
test.AddOutput<float>("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<float> X(16 * 4);
std::iota(X.begin(), X.end(), 0.f);
std::vector<float> 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<float> roi{};
std::vector<float> scales{3 / 4.0f, 3 / 4.0f, 3 / 4.0f};
std::vector<int64_t> output_shape{1, 1, 3, 3, 3};
std::vector<int64_t> axes{-3, -2, -1};

OpTester test("Resize", 18);
test.AddAttribute<int64_t>("exclude_outside", 0LL);
test.AddAttribute<std::vector<int64_t>>("axes", axes);
test.AddAttribute<int64_t>("antialias", 0LL);
test.AddAttribute("mode", "linear");

test.AddInput<float>("X", {1, 1, 4, 4, 4}, X);
test.AddInput<float>("roi", {int64_t(roi.size())}, roi);
test.AddInput<float>("scales", {int64_t(scales.size())}, scales, true);

test.AddOutput<float>("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<float> X(16 * 4);
std::iota(X.begin(), X.end(), 0.f);
std::vector<float> roi{};
std::vector<float> scales{};
std::vector<int64_t> sizes{3, 3};
std::vector<int64_t> axes{2, 3, 4};
std::vector<float> Y(16 * 4, 0.0f);

OpTester test("Resize", 18);
test.AddShapeToTensorData(false);
test.AddAttribute("mode", "linear");
test.AddAttribute<std::vector<int64_t>>("axes", axes);

test.AddInput<float>("X", {1, 1, 4, 4, 4}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("", {0}, scales);
test.AddInput<int64_t>("sizes", {int64_t(sizes.size())}, sizes);
test.AddOutput<float>("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});
}

// Non-finite scale values must be rejected before being multiplied into output dimensions.
TEST(ResizeOpTest, Scales_NaN_Rejected_18) {
std::vector<float> X(16, 1.0f);
std::vector<float> roi{};
std::vector<float> scales{1.0f, 1.0f, std::numeric_limits<float>::quiet_NaN(), 2.0f};
std::vector<float> Y(16, 0.0f);

OpTester test("Resize", 18);
test.AddShapeToTensorData(false);
test.AddAttribute("mode", "linear");

test.AddInput<float>("X", {1, 1, 4, 4}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("scales", {int64_t(scales.size())}, scales);
test.AddOutput<float>("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<float> X(16, 1.0f);
std::vector<float> roi{};
std::vector<float> scales{1.0f, 1.0f, 2.0f, std::numeric_limits<float>::infinity()};
std::vector<float> Y(16, 0.0f);

OpTester test("Resize", 18);
test.AddShapeToTensorData(false);
test.AddAttribute("mode", "linear");

test.AddInput<float>("X", {1, 1, 4, 4}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("scales", {int64_t(scales.size())}, scales);
test.AddOutput<float>("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<float> X(16 * 4);
std::iota(X.begin(), X.end(), 0.f);
std::vector<float> roi{};
std::vector<float> scales{0.75f, 0.75f, 0.75f};
std::vector<int64_t> axes{2, 3, 2};
std::vector<float> Y(16 * 4, 0.0f);

OpTester test("Resize", 18);
test.AddShapeToTensorData(false);
test.AddAttribute("mode", "linear");
test.AddAttribute<std::vector<int64_t>>("axes", axes);

test.AddInput<float>("X", {1, 1, 4, 4, 4}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("scales", {int64_t(scales.size())}, scales);
test.AddOutput<float>("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<float> 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<float> roi{0.0f, 0.0f, 1.0f, 1.0f};
std::vector<float> scales{0.75f, 0.75f, 0.75f};
std::vector<int64_t> axes{2, 3, 4};
std::vector<float> 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<std::vector<int64_t>>("axes", axes);

test.AddInput<float>("X", {1, 1, 4, 4, 4}, X);
test.AddInput<float>("roi", {int64_t(roi.size())}, roi);
test.AddInput<float>("scales", {int64_t(scales.size())}, scales);
test.AddOutput<float>("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);
Expand Down
Loading