Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
91 changes: 74 additions & 17 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 @@ -301,6 +304,12 @@ class UpsampleBase {
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
rank = static_cast<int64_t>(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;
Expand Down Expand Up @@ -335,6 +344,8 @@ class UpsampleBase {
InlinedVector<float> scales_;
InlinedVector<float> roi_;
TensorShapeVector axes_;
TensorShapeVector normalized_axes_;
int64_t normalized_axes_rank_ = -1;

bool scales_cached_;
bool roi_cached_;
Expand Down Expand Up @@ -497,7 +508,37 @@ 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. 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<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 +612,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 +640,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 +696,33 @@ 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()) {
InlinedVector<float> roi_tmp(rank * 2, 0);
for (size_t i = rank; i < rank * 2; ++i) {
roi_tmp[i] = 1;
}
// 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 either 2 * rank (", 2 * rank,
") or 2 * number of axes (", 2 * axes_.size(), ").");
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);
roi_tmp[v_in_axes] = (roi_array[i]);
roi_tmp[rank + v_in_axes] = (roi_array[axes_.size() + i]);
TensorShapeVector normalized_axes;
ORT_RETURN_IF_ERROR(ValidateAndNormalizeAxes(rank_i64, normalized_axes));
if (per_axis_roi) {
InlinedVector<float> 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<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);
}
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
204 changes: 204 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,209 @@ 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);
// 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
// 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