Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
56 changes: 46 additions & 10 deletions onnxruntime/core/providers/cpu/tensor/upsamplebase.h
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ 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());
// 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<float>("extrapolation_value", 0.0f);
Expand Down Expand Up @@ -497,7 +500,31 @@ 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<bool> seen(static_cast<size_t>(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<size_t>(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<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 @@ -571,9 +598,10 @@ class UpsampleBase {
"Number of elements in scales should be equal to number of axes.");

InlinedVector<float> 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<size_t>(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<size_t>(normalized_axes[i])] = scales[i];
}
scales.swap(new_scales);
}
Expand All @@ -598,11 +626,14 @@ 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++) {
const int64_t axis = HandleNegativeAxis(axes_[i], rank);
output_dims[static_cast<size_t>(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<size_t>(normalized_axes[i])] = size_span[i];
}
} else {
std::copy(size_span.begin(), size_span.end(), output_dims.begin());
Expand Down Expand Up @@ -651,21 +682,26 @@ 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;
}
const int64_t rank_i64 = static_cast<int64_t>(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<size_t>(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<size_t>(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);
}
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
201 changes: 201 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,206 @@ 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(32, 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(32, 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. 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<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 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<float> X(16 * 4);
std::iota(X.begin(), X.end(), 0.f);
std::vector<float> roi{};
std::vector<float> scales{0.75f, 0.75f};
std::vector<int64_t> axes{-1, 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>("scales", {int64_t(scales.size())}, scales);
test.AddOutput<float>("Y", {1, 1, 4, 4, 4}, Y);

test.Run(OpTester::ExpectResult::kExpectFailure,
"axes attribute contains duplicate axis 4",
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
}

// 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