diff --git a/onnxruntime/core/graph/contrib_ops/contrib_defs.cc b/onnxruntime/core/graph/contrib_ops/contrib_defs.cc index a5537c7d58b05..5e6e63da271d8 100644 --- a/onnxruntime/core/graph/contrib_ops/contrib_defs.cc +++ b/onnxruntime/core/graph/contrib_ops/contrib_defs.cc @@ -56,10 +56,19 @@ void convTransposeWithDynamicPadsShapeInference(InferenceContext& ctx) { } int64_t group = getAttribute(ctx, "group", 1); + if (group <= 0) { + fail_shape_inference("group must be positive"); + } auto input_shape = ctx.getInputType(0)->tensor_type().shape(); if (input_shape.dim_size() < 2) { - return; // Input tensor should have at least two dimensions. + fail_shape_inference("Input X must have at least 2 dimensions. Got: ", input_shape.dim_size()); + } + + // W must also have at least rank 2 (C_in, C_out/group, spatial dims...) + auto w_shape = ctx.getInputType(1)->tensor_type().shape(); + if (w_shape.dim_size() < 2) { + fail_shape_inference("Filter W must have at least 2 dimensions. Got: ", w_shape.dim_size()); } // first dim is the batch axis and the next is the number of channels. @@ -147,7 +156,7 @@ void convTransposeWithDynamicPadsShapeInference(InferenceContext& ctx) { *final_output_shape->add_dim() = input_shape.dim(0); *final_output_shape->add_dim() = - ctx.getInputType(1)->tensor_type().shape().dim(1) * + w_shape.dim(1) * group; // channels should be the second dim of second input multiply // group. @@ -157,9 +166,9 @@ void convTransposeWithDynamicPadsShapeInference(InferenceContext& ctx) { for (int i = 0; i < size_of_output; ++i) { if (input_shape.dim(i + 2).has_dim_value()) { if (output_shape[i] < input_shape.dim(i + 2).dim_value()) { - // TODO: throw exception? - return; // output shape value cannot be smaller than the input shape - // value + fail_shape_inference("output_shape[", i, "] value (", output_shape[i], + ") cannot be smaller than the input spatial dimension (", + input_shape.dim(i + 2).dim_value(), ")"); } } final_output_shape->add_dim()->set_dim_value(output_shape[i]); diff --git a/onnxruntime/core/providers/cpu/nn/conv_transpose_attributes.h b/onnxruntime/core/providers/cpu/nn/conv_transpose_attributes.h index 4ca90a885ea96..9be03853c6ac1 100644 --- a/onnxruntime/core/providers/cpu/nn/conv_transpose_attributes.h +++ b/onnxruntime/core/providers/cpu/nn/conv_transpose_attributes.h @@ -18,6 +18,7 @@ #pragma once +#include "core/common/safeint.h" #include "core/providers/cpu/nn/conv_attributes.h" namespace onnxruntime { @@ -61,6 +62,16 @@ struct ConvTransposeAttributes : public ConvAttributes { const Tensor* B = has_bias ? (dynamic_padding ? context->Input(3) : context->Input(2)) : nullptr; const int rank = static_cast(X->Shape().NumDimensions()); + if (rank < 2) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Input X must have at least 2 dimensions. Got: ", rank); + } + + if (static_cast(F_Shape.NumDimensions()) < 2) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Filter W must have at least 2 dimensions. Got: ", F_Shape.NumDimensions()); + } + TensorShape input_shape = X->Shape().Slice(is_nhwc ? 1 : 2, is_nhwc ? rank - 1 : rank); const int64_t num_input_channels = is_nhwc ? X->Shape()[rank - 1] : X->Shape()[1]; const int64_t N = X->Shape()[0]; @@ -118,12 +129,35 @@ struct ConvTransposeAttributes : public ConvAttributes { TensorShapeVector local_output_padding(output_padding); if (local_output_padding.empty()) { local_output_padding.resize(kernel_shape.size(), 0); + } else if (local_output_padding.size() != kernel_shape.size()) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "output_padding size (", local_output_padding.size(), + ") must match the number of spatial dimensions (", kernel_shape.size(), ")."); } ConvPadVector local_pads; local_pads.reserve(2 * (input_shape.NumDimensions())); if (dynamic_padding) { - for (int64_t i = 0; i < Pads->Shape().SizeFromDimension(0); ++i) { - local_pads.push_back(Pads->Data()[i]); + if (Pads == nullptr) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Pads input is required for dynamic padding mode."); + } + if (Pads->Shape().NumDimensions() != 1) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Pads input must be a 1D tensor. Got rank: ", Pads->Shape().NumDimensions()); + } + const int64_t expected_pads_size = SafeInt(kernel_shape.size()) * 2; + if (Pads->Shape()[0] != expected_pads_size) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Pads input must have ", expected_pads_size, " elements (2 * num_spatial_dims). Got: ", + Pads->Shape()[0]); + } + for (int64_t i = 0; i < Pads->Shape()[0]; ++i) { + const int64_t pad_val = Pads->Data()[i]; + if (pad_val < 0) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Pad values must be non-negative. Got: ", pad_val, " at index ", i); + } + local_pads.push_back(pad_val); } } else { local_pads.assign(pads.begin(), pads.end()); @@ -134,10 +168,30 @@ struct ConvTransposeAttributes : public ConvAttributes { TensorShapeVector local_dilations(dilations); if (local_dilations.empty()) { local_dilations.resize(kernel_shape.size(), 1); + } else if (local_dilations.size() != kernel_shape.size()) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "dilations size (", local_dilations.size(), + ") must match the number of spatial dimensions (", kernel_shape.size(), ")."); } TensorShapeVector local_strides(strides); if (local_strides.empty()) { local_strides.resize(kernel_shape.size(), 1); + } else if (local_strides.size() != kernel_shape.size()) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "strides size (", local_strides.size(), + ") must match the number of spatial dimensions (", kernel_shape.size(), ")."); + } + + // Validate output_padding < stride per ONNX spec: + // "Additional size added to one side of the output shape. This must be less + // than either stride or dilation in each spatial dimension." + for (size_t i = 0; i < local_output_padding.size(); ++i) { + if (local_output_padding[i] >= local_strides[i] && local_output_padding[i] >= local_dilations[i]) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "output_padding[", i, "] (", local_output_padding[i], + ") must be less than stride (", local_strides[i], + ") or dilation (", local_dilations[i], ")."); + } } TensorShapeVector Y_dims; @@ -215,6 +269,11 @@ struct ConvTransposeAttributes : public ConvAttributes { int64_t* pad_head, int64_t* pad_tail, int64_t* out_size) const { + ORT_ENFORCE(in_size > 0, "Input spatial dimension must be positive. Got: ", in_size); + ORT_ENFORCE(stride > 0, "Stride must be positive. Got: ", stride); + ORT_ENFORCE(kernel > 0, "Kernel size must be positive. Got: ", kernel); + ORT_ENFORCE(dilation > 0, "Dilation must be positive. Got: ", dilation); + ORT_ENFORCE(adj >= 0, "Output padding must be non-negative. Got: ", adj); // Output shape is explicitly provided - pad values will have to be computed if (*out_size != -1) { ORT_ENFORCE(*out_size >= 0); @@ -237,7 +296,7 @@ struct ConvTransposeAttributes : public ConvAttributes { } *out_size = - (in_size - 1) * stride + adj + (kernel - 1) * dilation + 1 - *pad_head - *pad_tail; + SafeInt(in_size - 1) * stride + adj + SafeInt(kernel - 1) * dilation + 1 - *pad_head - *pad_tail; } }; diff --git a/onnxruntime/core/providers/cuda/nn/conv_transpose.cc b/onnxruntime/core/providers/cuda/nn/conv_transpose.cc index 808c0352e69c9..ef243d3c69079 100644 --- a/onnxruntime/core/providers/cuda/nn/conv_transpose.cc +++ b/onnxruntime/core/providers/cuda/nn/conv_transpose.cc @@ -5,6 +5,7 @@ #include #include +#include "core/common/safeint.h" #include "conv_transpose.h" #include "core/providers/cuda/tensor/transpose.h" @@ -272,7 +273,9 @@ Status ConvTranspose::UpdateState(OpKernelContext* context, bool dyna bool input_dims_changed = (s_.last_x_dims != x_dims); bool w_dims_changed = (s_.last_w_dims != w_dims); - if (input_dims_changed || w_dims_changed) { + // When dynamic_padding is enabled, Pads may change between calls even if X/W + // shapes are unchanged, so we must always recompute the output shape. + if (input_dims_changed || w_dims_changed || dynamic_padding) { if (input_dims_changed) s_.last_x_dims = gsl::make_span(x_dims); @@ -283,6 +286,16 @@ Status ConvTranspose::UpdateState(OpKernelContext* context, bool dyna // The following code is from ConvTransposeAttributes::PrepareForCompute const int rank = static_cast(X->Shape().NumDimensions()); + if (rank < 2) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Input X must have at least 2 dimensions. Got: ", rank); + } + + if (static_cast(w_shape.NumDimensions()) < 2) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Filter W must have at least 2 dimensions. Got: ", w_shape.NumDimensions()); + } + TensorShape input_shape = X->Shape().Slice(channels_last ? 1 : 2, channels_last ? rank - 1 : rank); const int64_t num_input_channels = channels_last ? X->Shape()[rank - 1] : X->Shape()[1]; const int64_t N = X->Shape()[0]; @@ -335,12 +348,35 @@ Status ConvTranspose::UpdateState(OpKernelContext* context, bool dyna TensorShapeVector local_output_padding(conv_transpose_attrs_.output_padding); if (local_output_padding.empty()) { local_output_padding.resize(kernel_shape.size(), 0); + } else if (local_output_padding.size() != kernel_rank) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "output_padding size (", local_output_padding.size(), + ") must match the number of spatial dimensions (", kernel_rank, ")."); } ConvPadVector pads; pads.reserve(2 * (input_shape.NumDimensions())); if (dynamic_padding) { - for (int64_t i = 0; i < Pads->Shape().SizeFromDimension(0); ++i) { - pads.push_back(Pads->Data()[i]); + if (Pads == nullptr) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Pads input is required for dynamic padding mode."); + } + if (Pads->Shape().NumDimensions() != 1) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Pads input must be a 1D tensor. Got rank: ", Pads->Shape().NumDimensions()); + } + const int64_t expected_pads_size = SafeInt(kernel_shape.size()) * 2; + if (Pads->Shape()[0] != expected_pads_size) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Pads input must have ", expected_pads_size, " elements (2 * num_spatial_dims). Got: ", + Pads->Shape()[0]); + } + for (int64_t i = 0; i < Pads->Shape()[0]; ++i) { + const int64_t pad_val = Pads->Data()[i]; + if (pad_val < 0) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Pad values must be non-negative. Got: ", pad_val, " at index ", i); + } + pads.push_back(pad_val); } } else { pads.assign(conv_transpose_attrs_.pads.begin(), conv_transpose_attrs_.pads.end()); @@ -351,10 +387,28 @@ Status ConvTranspose::UpdateState(OpKernelContext* context, bool dyna TensorShapeVector dilations(conv_transpose_attrs_.dilations); if (dilations.empty()) { dilations.resize(kernel_shape.size(), 1); + } else if (dilations.size() != kernel_rank) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "dilations size (", dilations.size(), + ") must match the number of spatial dimensions (", kernel_rank, ")."); } TensorShapeVector strides(conv_transpose_attrs_.strides); if (strides.empty()) { strides.resize(kernel_shape.size(), 1); + } else if (strides.size() != kernel_rank) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "strides size (", strides.size(), + ") must match the number of spatial dimensions (", kernel_rank, ")."); + } + + // Validate output_padding < stride per ONNX spec + for (size_t i = 0; i < local_output_padding.size(); ++i) { + if (local_output_padding[i] >= strides[i] && local_output_padding[i] >= dilations[i]) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "output_padding[", i, "] (", local_output_padding[i], + ") must be less than stride (", strides[i], + ") or dilation (", dilations[i], ")."); + } } TensorShapeVector y_dims; diff --git a/onnxruntime/test/contrib_ops/conv_transpose_with_dynamic_pads_test.cc b/onnxruntime/test/contrib_ops/conv_transpose_with_dynamic_pads_test.cc index 092d07cc0e9a6..e60a5d0f67a0f 100644 --- a/onnxruntime/test/contrib_ops/conv_transpose_with_dynamic_pads_test.cc +++ b/onnxruntime/test/contrib_ops/conv_transpose_with_dynamic_pads_test.cc @@ -6,6 +6,8 @@ namespace onnxruntime { namespace test { + +// Basic functional test - 2D convolution transpose with dynamic pads TEST(ContribOpTest, ConvTransposeWithDynamicPads) { OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); test.AddAttribute("kernel_shape", std::vector{3, 3}); @@ -19,5 +21,295 @@ TEST(ContribOpTest, ConvTransposeWithDynamicPads) { test.AddOutput("Y", {1, 1, 6, 6}, std::vector{0.07368518f, -0.08925839f, -0.06627201f, 0.06301362f, 0.03732984f, -0.01919658f, -0.00628807f, -0.02817563f, -0.01472169f, 0.04392925f, -0.00689478f, -0.01549204f, 0.07957941f, -0.11459791f, -0.09505399f, 0.07681622f, 0.03604182f, -0.01853423f, -0.0270785f, -0.00680824f, -0.06650258f, 0.08004665f, 0.07918708f, -0.0724144f, 0.06256775f, -0.17838378f, -0.18863615f, 0.20064656f, 0.133717f, -0.06876295f, -0.06398046f, -0.00864975f, 0.19289537f, -0.01490572f, -0.13673618f, 0.01949645f}); test.Run(); } + +// Basic functional test with zero pads +TEST(ContribOpTest, ConvTransposeWithDynamicPads_ZeroPads) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + test.AddAttribute("dilations", std::vector{1, 1}); + + test.AddInput("X", {1, 1, 2, 2}, std::vector{1.0f, 2.0f, 3.0f, 4.0f}); + test.AddInput("W", {1, 1, 3, 3}, std::vector{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}); + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + test.AddOutput("Y", {1, 1, 4, 4}, + std::vector{1.0f, 2.0f, 0.0f, 0.0f, + 3.0f, 5.0f, 2.0f, 0.0f, + 0.0f, 3.0f, 5.0f, 2.0f, + 0.0f, 0.0f, 3.0f, 4.0f}); + test.Run(); +} + +// Test with group > 1 +TEST(ContribOpTest, ConvTransposeWithDynamicPads_Groups) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + test.AddAttribute("dilations", std::vector{1, 1}); + test.AddAttribute("group", static_cast(2)); + + // X: {N=1, C=2, H=2, W=2} + test.AddInput("X", {1, 2, 2, 2}, std::vector{1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f}); + // W: {C_in=2, C_out/group=1, kH=3, kW=3} - each group has center-element filter + test.AddInput("W", {2, 1, 3, 3}, std::vector{0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f}); + test.AddInput("Pads", {4}, std::vector{1, 1, 1, 1}); + // Output: {N=1, C_out=2, H=2, W=2} - center filter with matching pads acts as identity + test.AddOutput("Y", {1, 2, 2, 2}, std::vector{1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f}); + test.Run(); +} + +#ifndef ORT_NO_EXCEPTIONS +// Tests below trigger fail_shape_inference() which throws. +// In no-exception builds, this would abort, so these tests are skipped. + +// Security: Rank-0 W tensor should fail with proper error, not crash. +// This is an MS domain contrib op, so we control the shape inference code. +// Shape inference rejects the model at load time with fail_shape_inference. +TEST(ContribOpTest, ConvTransposeWithDynamicPads_InvalidRank0W) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + + test.AddInput("X", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("W", {}, std::vector{1.0f}); // Rank-0 W + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + test.AddOutput("Y", {1, 1, 5, 5}, std::vector(25, 0.0f)); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Filter W must have at least 2 dimensions"); +} + +// Security: Rank-1 X tensor should fail with proper error, not crash. +// Shape inference rejects the model at load time. +TEST(ContribOpTest, ConvTransposeWithDynamicPads_InvalidRank1X) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3}); + test.AddAttribute("strides", std::vector{1}); + + test.AddInput("X", {3}, std::vector(3, 1.0f)); // Rank-1 X + test.AddInput("W", {1, 1, 3}, std::vector(3, 1.0f)); + test.AddInput("Pads", {2}, std::vector{0, 0}); + test.AddOutput("Y", {1, 1, 5}, std::vector(5, 0.0f)); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Input X must have at least 2 dimensions"); +} + +// Security: Rank-0 X tensor should fail with proper error, not crash. +// Shape inference rejects the model at load time. +TEST(ContribOpTest, ConvTransposeWithDynamicPads_InvalidRank0X) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + + test.AddInput("X", {}, std::vector{1.0f}); // Rank-0 X + test.AddInput("W", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + test.AddOutput("Y", {1, 1, 5, 5}, std::vector(25, 0.0f)); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Input X must have at least 2 dimensions"); +} + +// Security: Pads tensor with wrong number of elements should fail. +// When Pads is available as initializer, shape inference catches this with fail_shape_inference. +// When Pads is only available at runtime, kernel validation catches it. +TEST(ContribOpTest, ConvTransposeWithDynamicPads_InvalidPadsSize) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + + test.AddInput("X", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("W", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("Pads", {3}, std::vector{0, 0, 0}); // Wrong size: should be 4 + test.AddOutput("Y", {1, 1, 5, 5}, std::vector(25, 0.0f)); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Pads input must have"); +} + +// Security: 2D Pads tensor should fail. +// Shape inference catches this (fail_shape_inference) when pads initializer is available. +TEST(ContribOpTest, ConvTransposeWithDynamicPads_InvalidPadsRank) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + + test.AddInput("X", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("W", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("Pads", {2, 2}, std::vector{0, 0, 0, 0}); // 2D instead of 1D + test.AddOutput("Y", {1, 1, 5, 5}, std::vector(25, 0.0f)); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Pads input must be a 1D tensor"); +} + +#endif // !ORT_NO_EXCEPTIONS + +// Security: Negative pad values should fail. +// This is caught by kernel validation (ORT_MAKE_STATUS), not fail_shape_inference, +// so it works in both exception and no-exception builds. +TEST(ContribOpTest, ConvTransposeWithDynamicPads_NegativePads) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + + test.AddInput("X", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("W", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("Pads", {4}, std::vector{-1, 0, 0, 0}); // Negative pad + test.AddOutput("Y", {1, 1, 5, 5}, std::vector(25, 0.0f)); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Pad values must be non-negative"); +} + +// Security: X and W dimension mismatch should fail. +TEST(ContribOpTest, ConvTransposeWithDynamicPads_DimMismatch) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + + test.AddInput("X", {1, 1, 3, 3}, std::vector(9, 1.0f)); // 4D + test.AddInput("W", {1, 1, 3, 3, 3}, std::vector(27, 1.0f)); // 5D - mismatch + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + test.AddOutput("Y", {1, 1, 5, 5}, std::vector(25, 0.0f)); + + test.Run(OpTester::ExpectResult::kExpectFailure, "X num_dims does not match W num_dims"); +} + +// 1D convolution transpose with dynamic pads +TEST(ContribOpTest, ConvTransposeWithDynamicPads_1D) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3}); + test.AddAttribute("strides", std::vector{2}); + test.AddAttribute("dilations", std::vector{1}); + test.AddAttribute("output_padding", std::vector{1}); + + // X: {N=1, C=1, L=3} + test.AddInput("X", {1, 1, 3}, std::vector{1.0f, 2.0f, 3.0f}); + // W: {C_in=1, C_out/group=1, kL=3} + test.AddInput("W", {1, 1, 3}, std::vector{1.0f, 1.0f, 1.0f}); + test.AddInput("Pads", {2}, std::vector{1, 1}); + // Output: stride*(L-1) + output_padding + kernel - pad_begin - pad_end = 2*(3-1) + 1 + 3 - 1 - 1 = 6 + // Conv transpose scatter: input[0]=1 -> pos {0,1}, input[1]=2 -> pos {1,2,3}, input[2]=3 -> pos {3,4,5} + test.AddOutput("Y", {1, 1, 6}, std::vector{1.0f, 3.0f, 2.0f, 5.0f, 3.0f, 3.0f}); + + test.Run(); +} + +// Batch size > 1 +TEST(ContribOpTest, ConvTransposeWithDynamicPads_BatchSize2) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{2, 2}); + test.AddAttribute("strides", std::vector{1, 1}); + + // X: {N=2, C=1, H=2, W=2} - two identical images + test.AddInput("X", {2, 1, 2, 2}, std::vector{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f}); + // W: {C_in=1, C_out/group=1, kH=2, kW=2} - identity-like filter + test.AddInput("W", {1, 1, 2, 2}, std::vector{1.0f, 0.0f, 0.0f, 0.0f}); + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + // Output: {N=2, C=1, H=3, W=3} - each batch element processed independently + test.AddOutput("Y", {2, 1, 3, 3}, std::vector{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}); + test.Run(); +} + +// Multiple output channels (C_out/group > 1) +TEST(ContribOpTest, ConvTransposeWithDynamicPads_MultipleOutputChannels) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{1, 1}); + test.AddAttribute("strides", std::vector{1, 1}); + + // X: {N=1, C_in=1, H=1, W=1} + test.AddInput("X", {1, 1, 1, 1}, std::vector{2.0f}); + // W: {C_in=1, C_out/group=2, kH=1, kW=1} - maps 1 channel to 2 channels + test.AddInput("W", {1, 2, 1, 1}, std::vector{1.0f, 3.0f}); + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + // Output: {N=1, C_out=2, H=1, W=1} + test.AddOutput("Y", {1, 2, 1, 1}, std::vector{2.0f, 6.0f}); + + test.Run(); +} + +// With bias (optional 4th input) +TEST(ContribOpTest, ConvTransposeWithDynamicPads_WithBias) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{1, 1}); + test.AddAttribute("strides", std::vector{1, 1}); + + // X: {N=1, C_in=1, H=2, W=2} + test.AddInput("X", {1, 1, 2, 2}, std::vector{1.0f, 2.0f, 3.0f, 4.0f}); + // W: {C_in=1, C_out/group=1, kH=1, kW=1} + test.AddInput("W", {1, 1, 1, 1}, std::vector{1.0f}); + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + // B: {C_out=1} + test.AddInput("B", {1}, std::vector{10.0f}); + // Output: X * W + B = X + 10 + test.AddOutput("Y", {1, 1, 2, 2}, std::vector{11.0f, 12.0f, 13.0f, 14.0f}); + + test.Run(); +} + +// Dilations > 1 +TEST(ContribOpTest, ConvTransposeWithDynamicPads_Dilations) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{2, 2}); + test.AddAttribute("strides", std::vector{1, 1}); + test.AddAttribute("dilations", std::vector{2, 2}); + + // X: {N=1, C=1, H=2, W=2} + test.AddInput("X", {1, 1, 2, 2}, std::vector{1.0f, 0.0f, 0.0f, 0.0f}); + // W: {C_in=1, C_out/group=1, kH=2, kW=2} + test.AddInput("W", {1, 1, 2, 2}, std::vector{1.0f, 2.0f, 3.0f, 4.0f}); + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + // Output shape: (2-1)*1 + 0 + (2-1)*2 + 1 = 4 in each dim + // With dilation=2, effective kernel size is 3 (positions at 0 and 2) + test.AddOutput("Y", {1, 1, 4, 4}, std::vector{1.0f, 0.0f, 2.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 3.0f, 0.0f, 4.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}); + test.Run(); +} + +// Asymmetric pads (different begin/end values) +TEST(ContribOpTest, ConvTransposeWithDynamicPads_AsymmetricPads) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + + // X: {N=1, C=1, H=1, W=1} + test.AddInput("X", {1, 1, 1, 1}, std::vector{1.0f}); + // W: {C_in=1, C_out/group=1, kH=3, kW=3} + test.AddInput("W", {1, 1, 3, 3}, std::vector{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f}); + // Pads: [pad_h_begin, pad_w_begin, pad_h_end, pad_w_end] = [1, 0, 0, 1] + // Unpadded output would be 3x3 (kernel applied to single pixel) + // After removing 1 row from top and 1 col from right: 2x2 + test.AddInput("Pads", {4}, std::vector{1, 0, 0, 1}); + // Output: (1-1)*1 + 0 + 3 - 1 - 0 = 2 (height), (1-1)*1 + 0 + 3 - 0 - 1 = 2 (width) + test.AddOutput("Y", {1, 1, 2, 2}, std::vector{4.0f, 5.0f, 7.0f, 8.0f}); + test.Run(); +} + +// Security: Input channels not divisible by group should fail. +TEST(ContribOpTest, ConvTransposeWithDynamicPads_ChannelsNotDivisibleByGroup) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{1, 1}); + test.AddAttribute("group", static_cast(2)); + + // X has 3 input channels, not divisible by group=2 + test.AddInput("X", {1, 3, 3, 3}, std::vector(27, 1.0f)); + test.AddInput("W", {3, 1, 3, 3}, std::vector(27, 1.0f)); + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + test.AddOutput("Y", {1, 2, 5, 5}, std::vector(50, 0.0f)); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Input channels is not divisible by group"); +} + +// Spec gap: output_padding must be less than stride or dilation. +TEST(ContribOpTest, ConvTransposeWithDynamicPads_OutputPaddingTooLarge) { + OpTester test("ConvTransposeWithDynamicPads", 1, onnxruntime::kMSDomain); + test.AddAttribute("kernel_shape", std::vector{3, 3}); + test.AddAttribute("strides", std::vector{2, 2}); + test.AddAttribute("output_padding", std::vector{2, 2}); // must be < stride(2) + + test.AddInput("X", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("W", {1, 1, 3, 3}, std::vector(9, 1.0f)); + test.AddInput("Pads", {4}, std::vector{0, 0, 0, 0}); + test.AddOutput("Y", {1, 1, 9, 9}, std::vector(81, 0.0f)); + + test.Run(OpTester::ExpectResult::kExpectFailure, "output_padding"); +} + } // namespace test } // namespace onnxruntime