From 1d39f2681ced3bfbff5426a9f7e3ac6d7b656c21 Mon Sep 17 00:00:00 2001 From: Arsalan Shakil Date: Thu, 18 Jun 2026 16:18:40 +0300 Subject: [PATCH 1/3] Add int8/uint8 CPU support for SpaceToDepth and int8 for DepthToSpace SpaceToDepth previously supported only float/double on CPU, and DepthToSpace supported float/double/uint8. Extend the CPU kernels so both ops also accept 8-bit integer tensors, which is needed for running quantized models. uint8_t and int8_t are routed through a single template instantiation: the ops only shuffle 8-bit elements, so the signedness of the data is irrelevant and we avoid the binary-size cost of a second instantiation. SpaceDepthOpCpuImpl now takes raw pointers so the shared branch can feed it via DataRaw(). Adds typed unit tests covering uint8/int8 for SpaceToDepth and int8 for DepthToSpace, and updates docs/OperatorKernels.md. Fixes #21287 --- docs/OperatorKernels.md | 8 +- .../providers/cpu/tensor/space_depth_ops.cc | 55 ++++++++---- .../cpu/tensor/space_depth_ops_test.cc | 88 ++++++++++++++++++- onnxruntime/test/unittest_util/conversion.h | 6 ++ 4 files changed, 137 insertions(+), 20 deletions(-) diff --git a/docs/OperatorKernels.md b/docs/OperatorKernels.md index cc3c30a2c3a7a..ac00b213d3551 100644 --- a/docs/OperatorKernels.md +++ b/docs/OperatorKernels.md @@ -115,8 +115,8 @@ The **OpSet Version** column uses the following notation: |||[17, 19]|**T1** = tensor(double), tensor(float)
**T2** = tensor(int32), tensor(int64)| |DeformConv|*in* X:**T**
*in* W:**T**
*in* offset:**T**
*in* B:**T**
*in* mask:**T**
*out* Y:**T**|22+|**T** = tensor(double), tensor(float)| |||[19, 21]|**T** = tensor(double), tensor(float)| -|DepthToSpace|*in* input:**T**
*out* output:**T**|13+|**T** = tensor(double), tensor(float), tensor(uint8)| -|||[11, 12]|**T** = tensor(double), tensor(float), tensor(uint8)| +|DepthToSpace|*in* input:**T**
*out* output:**T**|13+|**T** = tensor(double), tensor(float), tensor(int8), tensor(uint8)| +|||[11, 12]|**T** = tensor(double), tensor(float), tensor(int8), tensor(uint8)| |||[1, 10]|**T** = tensor(double), tensor(float)| |DequantizeLinear|*in* x:**T**
*in* x_scale:**tensor(float)**
*in* x_zero_point:**T**
*out* y:**tensor(float)**

or

*in* x:**T1**
*in* x_scale:**T2**
*in* x_zero_point:**T1**
*out* y:**T2**

or

*in* x:**T1**
*in* x_scale:**T2**
*in* x_zero_point:**T1**
*out* y:**T3**|25+|**T1** = tensor(float8e4m3fn), tensor(float8e4m3fnuz), tensor(float8e5m2), tensor(float8e5m2fnuz), tensor(int16), tensor(int2), tensor(int32), tensor(int4), tensor(int8), tensor(uint16), tensor(uint2), tensor(uint4), tensor(uint8)
**T2** = tensor(float), tensor(float16)| |||24|**T1** = tensor(float8e4m3fn), tensor(float8e4m3fnuz), tensor(float8e5m2), tensor(float8e5m2fnuz), tensor(int16), tensor(int32), tensor(int4), tensor(int8), tensor(uint16), tensor(uint4), tensor(uint8)
**T2** = tensor(float), tensor(float16)| @@ -477,8 +477,8 @@ The **OpSet Version** column uses the following notation: |||[1, 21]|**T** = tensor(float)| |Softsign|*in* input:**T**
*out* output:**T**|22+|**T** = tensor(float)| |||[1, 21]|**T** = tensor(float)| -|SpaceToDepth|*in* input:**T**
*out* output:**T**|13+|**T** = tensor(double), tensor(float)| -|||[1, 12]|**T** = tensor(double), tensor(float)| +|SpaceToDepth|*in* input:**T**
*out* output:**T**|13+|**T** = tensor(double), tensor(float), tensor(int8), tensor(uint8)| +|||[1, 12]|**T** = tensor(double), tensor(float), tensor(int8), tensor(uint8)| |Split|*in* input:**T**
*in* split:**T**
*out* outputs...:**T**

or

*in* input:**T**
*in* split:**tensor(int64)**
*out* outputs:**T**

or

*in* input:**T**
*out* outputs:**T**|18+|**T** = tensor(bfloat16), tensor(bool), tensor(double), tensor(float), tensor(float16), tensor(int16), tensor(int32), tensor(int64), tensor(int8), tensor(string), tensor(uint16), tensor(uint32), tensor(uint64), tensor(uint8)| |||[13, 17]|**T** = tensor(bfloat16), tensor(bool), tensor(double), tensor(float), tensor(float16), tensor(int16), tensor(int32), tensor(int64), tensor(int8), tensor(string), tensor(uint16), tensor(uint32), tensor(uint64), tensor(uint8)| |||[11, 12]|**T** = tensor(bfloat16), tensor(bool), tensor(double), tensor(float), tensor(float16), tensor(int16), tensor(int32), tensor(int64), tensor(int8), tensor(string), tensor(uint16), tensor(uint32), tensor(uint64), tensor(uint8)| diff --git a/onnxruntime/core/providers/cpu/tensor/space_depth_ops.cc b/onnxruntime/core/providers/cpu/tensor/space_depth_ops.cc index 7e1049c402210..3f5d07cef4b7c 100644 --- a/onnxruntime/core/providers/cpu/tensor/space_depth_ops.cc +++ b/onnxruntime/core/providers/cpu/tensor/space_depth_ops.cc @@ -18,7 +18,9 @@ ONNX_CPU_OPERATOR_VERSIONED_KERNEL( 12, KernelDefBuilder() .TypeConstraint("T", {DataTypeImpl::GetTensorType(), - DataTypeImpl::GetTensorType()}), + DataTypeImpl::GetTensorType(), + DataTypeImpl::GetTensorType(), + DataTypeImpl::GetTensorType()}), SpaceToDepth); ONNX_CPU_OPERATOR_KERNEL( @@ -26,7 +28,9 @@ ONNX_CPU_OPERATOR_KERNEL( 13, KernelDefBuilder() .TypeConstraint("T", {DataTypeImpl::GetTensorType(), - DataTypeImpl::GetTensorType()}), + DataTypeImpl::GetTensorType(), + DataTypeImpl::GetTensorType(), + DataTypeImpl::GetTensorType()}), SpaceToDepth); ONNX_CPU_OPERATOR_VERSIONED_KERNEL( @@ -44,7 +48,8 @@ ONNX_CPU_OPERATOR_VERSIONED_KERNEL( KernelDefBuilder() .TypeConstraint("T", {DataTypeImpl::GetTensorType(), DataTypeImpl::GetTensorType(), - DataTypeImpl::GetTensorType()}), + DataTypeImpl::GetTensorType(), + DataTypeImpl::GetTensorType()}), DepthToSpace); ONNX_CPU_OPERATOR_KERNEL( @@ -53,7 +58,8 @@ ONNX_CPU_OPERATOR_KERNEL( KernelDefBuilder() .TypeConstraint("T", {DataTypeImpl::GetTensorType(), DataTypeImpl::GetTensorType(), - DataTypeImpl::GetTensorType()}), + DataTypeImpl::GetTensorType(), + DataTypeImpl::GetTensorType()}), DepthToSpace); // intermediate tensor shapes are: @@ -70,15 +76,15 @@ using ConstEigenTensorMap = Eigen::TensorMap -static void SpaceDepthOpCpuImpl(const Tensor& input, Tensor& output, +static void SpaceDepthOpCpuImpl(const T* input_data, T* output_data, const std::array& permutation, const Eigen::DenseIndex batch_size, // dim0 in both input and output const Eigen::DenseIndex in_dim1, const Eigen::DenseIndex in_dim2, const Eigen::DenseIndex in_dim3, const Eigen::DenseIndex in_dim4, const Eigen::DenseIndex in_dim5, const Eigen::DenseIndex out_dim1, const Eigen::DenseIndex out_dim2, const Eigen::DenseIndex out_dim3, const Eigen::DenseIndex out_dim4, const Eigen::DenseIndex out_dim5) { - EigenTensorMap(output.MutableData(), batch_size, out_dim1, out_dim2, out_dim3, out_dim4, out_dim5) = - ConstEigenTensorMap(input.Data(), batch_size, + EigenTensorMap(output_data, batch_size, out_dim1, out_dim2, out_dim3, out_dim4, out_dim5) = + ConstEigenTensorMap(input_data, batch_size, in_dim1, in_dim2, in_dim3, in_dim4, in_dim5) .shuffle(permutation); } @@ -109,7 +115,7 @@ Status SpaceToDepth::Compute(OpKernelContext* context) const { std::array permutation{{0, 3, 5, 1, 2, 4}}; if (input.IsDataType()) { - SpaceDepthOpCpuImpl(input, output, permutation, + SpaceDepthOpCpuImpl(input.Data(), output.MutableData(), permutation, onnxruntime::narrow(batch), onnxruntime::narrow(input_depth), onnxruntime::narrow(input_height / blocksize_), @@ -122,7 +128,7 @@ Status SpaceToDepth::Compute(OpKernelContext* context) const { onnxruntime::narrow(input_height / blocksize_), onnxruntime::narrow(input_width / blocksize_)); } else if (input.IsDataType()) { - SpaceDepthOpCpuImpl(input, output, permutation, + SpaceDepthOpCpuImpl(input.Data(), output.MutableData(), permutation, onnxruntime::narrow(batch), onnxruntime::narrow(input_depth), onnxruntime::narrow(input_height / blocksize_), @@ -134,8 +140,24 @@ Status SpaceToDepth::Compute(OpKernelContext* context) const { onnxruntime::narrow(input_depth), onnxruntime::narrow(input_height / blocksize_), onnxruntime::narrow(input_width / blocksize_)); + } else if (input.IsDataType() || input.IsDataType()) { + // uint8_t and int8_t share a single implementation: the op only moves 8-bit + // elements around, so the signedness of the data does not matter. + SpaceDepthOpCpuImpl(static_cast(input.DataRaw()), + static_cast(output.MutableDataRaw()), permutation, + onnxruntime::narrow(batch), + onnxruntime::narrow(input_depth), + onnxruntime::narrow(input_height / blocksize_), + onnxruntime::narrow(blocksize_), + onnxruntime::narrow(input_width / blocksize_), + onnxruntime::narrow(blocksize_), + onnxruntime::narrow(blocksize_), + onnxruntime::narrow(blocksize_), + onnxruntime::narrow(input_depth), + onnxruntime::narrow(input_height / blocksize_), + onnxruntime::narrow(input_width / blocksize_)); } else { - // user will not see this as the kernel doesn't claim support for types other than float and double + // user will not see this as the kernel doesn't claim support for types other than float, double, uint8_t and int8_t return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "Unsupported input type in SpaceToDepth op: ", input.DataType()); } @@ -173,7 +195,7 @@ Status DepthToSpace::Compute(OpKernelContext* context) const { : std::array{{0, 1, 4, 2, 5, 3}}; if (input.IsDataType()) { - SpaceDepthOpCpuImpl(input, output, permutation, + SpaceDepthOpCpuImpl(input.Data(), output.MutableData(), permutation, onnxruntime::narrow(batch), onnxruntime::narrow(dim1), onnxruntime::narrow(blocksize_), @@ -186,7 +208,7 @@ Status DepthToSpace::Compute(OpKernelContext* context) const { onnxruntime::narrow(input_width), onnxruntime::narrow(blocksize_)); } else if (input.IsDataType()) { - SpaceDepthOpCpuImpl(input, output, permutation, + SpaceDepthOpCpuImpl(input.Data(), output.MutableData(), permutation, onnxruntime::narrow(batch), onnxruntime::narrow(dim1), onnxruntime::narrow(blocksize_), @@ -198,8 +220,11 @@ Status DepthToSpace::Compute(OpKernelContext* context) const { onnxruntime::narrow(blocksize_), onnxruntime::narrow(input_width), onnxruntime::narrow(blocksize_)); - } else if (input.IsDataType()) { - SpaceDepthOpCpuImpl(input, output, permutation, + } else if (input.IsDataType() || input.IsDataType()) { + // uint8_t and int8_t share a single implementation: the op only moves 8-bit + // elements around, so the signedness of the data does not matter. + SpaceDepthOpCpuImpl(static_cast(input.DataRaw()), + static_cast(output.MutableDataRaw()), permutation, onnxruntime::narrow(batch), onnxruntime::narrow(dim1), onnxruntime::narrow(blocksize_), @@ -212,7 +237,7 @@ Status DepthToSpace::Compute(OpKernelContext* context) const { onnxruntime::narrow(input_width), onnxruntime::narrow(blocksize_)); } else { - // user will not see this as the kernel doesn't claim support for types other than float and double + // user will not see this as the kernel doesn't claim support for types other than float, double, uint8_t and int8_t return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "Unsupported input type in DepthToSpace op: ", input.DataType()); } diff --git a/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc b/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc index f97de7a54bc99..0a8990f8e6a66 100644 --- a/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc @@ -13,7 +13,7 @@ template class TensorOpTest : public ::testing::Test { }; -using TensorOpTestTypes = ::testing::Types; +using TensorOpTestTypes = ::testing::Types; TYPED_TEST_SUITE(TensorOpTest, TensorOpTestTypes); TEST(TensorOpTest, SpaceToDepthTest_1) { @@ -159,6 +159,71 @@ TEST(TensorOpTest, SpaceToDepthTest_3) { test.Run(); } +TYPED_TEST(TensorOpTest, SpaceToDepthTest_int) { + // Same data as SpaceToDepthTest_2 (values 0..107 fit in both int8_t and uint8_t), + // exercised across the supported element types. + OpTester test("SpaceToDepth"); + constexpr int64_t blocksize = 3; + test.AddAttribute("blocksize", blocksize); + constexpr int64_t N = 2, C = 3, H = 3, W = 6; + const std::vector X = { + 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., + 11., 12., 13., 14., 15., 16., 17., 18., 19., 20., 21., + 22., 23., 24., 25., 26., 27., 28., 29., 30., 31., 32., + 33., 34., 35., 36., 37., 38., 39., 40., 41., 42., 43., + 44., 45., 46., 47., 48., 49., 50., 51., 52., 53., 54., + 55., 56., 57., 58., 59., 60., 61., 62., 63., 64., 65., + 66., 67., 68., 69., 70., 71., 72., 73., 74., 75., 76., + 77., 78., 79., 80., 81., 82., 83., 84., 85., 86., 87., + 88., 89., 90., 91., 92., 93., 94., 95., 96., 97., 98., + 99., 100., 101., 102., 103., 104., 105., 106., 107.}; + + const std::vector result = { + 0., 3., 18., 21., 36., 39., 1., 4., 19., 22., 37., + 40., 2., 5., 20., 23., 38., 41., 6., 9., 24., 27., + 42., 45., 7., 10., 25., 28., 43., 46., 8., 11., 26., + 29., 44., 47., 12., 15., 30., 33., 48., 51., 13., 16., + 31., 34., 49., 52., 14., 17., 32., 35., 50., 53., 54., + 57., 72., 75., 90., 93., 55., 58., 73., 76., 91., 94., + 56., 59., 74., 77., 92., 95., 60., 63., 78., 81., 96., + 99., 61., 64., 79., 82., 97., 100., 62., 65., 80., 83., + 98., 101., 66., 69., 84., 87., 102., 105., 67., 70., 85., + 88., 103., 106., 68., 71., 86., 89., 104., 107.}; + + const std::vector output_shape = {2, 27, 1, 2}; + + if constexpr (std::is_same::value) { + test.AddInput("input", {N, C, H, W}, X); + test.AddOutput("output", output_shape, result); + } else if constexpr (std::is_same::value) { + std::vector X_fp16(X.size()); + std::vector result_fp16(result.size()); + ConvertFloatToMLFloat16(X.data(), X_fp16.data(), X.size()); + ConvertFloatToMLFloat16(result.data(), result_fp16.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_fp16); + test.AddOutput("output", output_shape, result_fp16); + } else if constexpr (std::is_same::value) { + std::vector X_u8(X.size()); + std::vector result_u8(result.size()); + ConvertFloatToUint8_t(X.data(), X_u8.data(), X.size()); + ConvertFloatToUint8_t(result.data(), result_u8.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_u8); + test.AddOutput("output", output_shape, result_u8); + } else if constexpr (std::is_same::value) { + std::vector X_i8(X.size()); + std::vector result_i8(result.size()); + ConvertFloatToInt8_t(X.data(), X_i8.data(), X.size()); + ConvertFloatToInt8_t(result.data(), result_i8.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_i8); + test.AddOutput("output", output_shape, result_i8); + } else { + ORT_THROW("Type not supported"); + } + + // type not supported by QNN EP: MLFloat16 and unsigned char + test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); +} + TEST(TensorOpTest, DepthToSpaceTest_1) { OpTester test("DepthToSpace", 7); // create an opset 7 model constexpr int64_t blocksize = 2; @@ -319,6 +384,13 @@ TYPED_TEST(TensorOpTest, DepthToSpaceTest_3) { ConvertFloatToUint8_t(result.data(), result_u8.data(), result.size()); test.AddInput("input", {N, C, H, W}, X_u8); test.AddOutput("output", {2, 3, 6, 4}, result_u8); + } else if constexpr (std::is_same::value) { + std::vector X_i8(X.size()); + std::vector result_i8(result.size()); + ConvertFloatToInt8_t(X.data(), X_i8.data(), X.size()); + ConvertFloatToInt8_t(result.data(), result_i8.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_i8); + test.AddOutput("output", {2, 3, 6, 4}, result_i8); } else { ORT_THROW("Type not supported"); } @@ -383,6 +455,13 @@ TYPED_TEST(TensorOpTest, DepthToSpaceTest_4) { ConvertFloatToUint8_t(result.data(), result_u8.data(), result.size()); test.AddInput("input", {N, C, H, W}, X_u8); test.AddOutput("output", {2, 3, 6, 4}, result_u8); + } else if constexpr (std::is_same::value) { + std::vector X_i8(X.size()); + std::vector result_i8(result.size()); + ConvertFloatToInt8_t(X.data(), X_i8.data(), X.size()); + ConvertFloatToInt8_t(result.data(), result_i8.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_i8); + test.AddOutput("output", {2, 3, 6, 4}, result_i8); } else { ORT_THROW("Type not supported"); } @@ -429,6 +508,13 @@ TYPED_TEST(TensorOpTest, DepthToSpaceTest_5) { ConvertFloatToUint8_t(result.data(), result_u8.data(), result.size()); test.AddInput("input", {N, C, H, W}, X_u8); test.AddOutput("output", {1, 1, 4, 6}, result_u8); + } else if constexpr (std::is_same::value) { + std::vector X_i8(X.size()); + std::vector result_i8(result.size()); + ConvertFloatToInt8_t(X.data(), X_i8.data(), X.size()); + ConvertFloatToInt8_t(result.data(), result_i8.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_i8); + test.AddOutput("output", {1, 1, 4, 6}, result_i8); } else { ORT_THROW("Type not supported"); } diff --git a/onnxruntime/test/unittest_util/conversion.h b/onnxruntime/test/unittest_util/conversion.h index bb270e4c697d5..b0f0663f33dd7 100644 --- a/onnxruntime/test/unittest_util/conversion.h +++ b/onnxruntime/test/unittest_util/conversion.h @@ -22,6 +22,12 @@ inline void ConvertFloatToUint8_t(const float* f_datat, uint8_t* u8_data, size_t output_vector = in_vector.template cast(); } +inline void ConvertFloatToInt8_t(const float* f_datat, int8_t* i8_data, size_t input_size) { + auto in_vector = ConstEigenVectorMap(f_datat, input_size); + auto output_vector = EigenVectorMap(static_cast(static_cast(i8_data)), input_size); + output_vector = in_vector.template cast(); +} + inline void ConvertMLFloat16ToFloat(const MLFloat16* h_data, float* f_data, size_t input_size) { auto in_vector = ConstEigenVectorMap(static_cast(static_cast(h_data)), input_size); From feaa3d666c33c2ee4f674bee802237e6d8fc6202 Mon Sep 17 00:00:00 2001 From: Arsalan Shakil Date: Sat, 20 Jun 2026 21:25:39 +0300 Subject: [PATCH 2/3] Address review feedback for SpaceToDepth/DepthToSpace int8 tests - Add opset 13 typed tests for SpaceToDepth and DepthToSpace so the opset 13 integer kernel registrations are exercised (existing tests only ran at opset 7/11). - Note the intentional out-of-range int8_t wrap in DepthToSpaceTest_3/4 (the op is a pure permutation, so the same conversion on input and expected output cancels out). - Reword the element-type and QNN-exclusion comments to avoid implying the CPU kernel supports MLFloat16. --- .../cpu/tensor/space_depth_ops_test.cc | 105 +++++++++++++++++- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc b/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc index 0a8990f8e6a66..4c05c48352dfb 100644 --- a/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc @@ -160,8 +160,9 @@ TEST(TensorOpTest, SpaceToDepthTest_3) { } TYPED_TEST(TensorOpTest, SpaceToDepthTest_int) { - // Same data as SpaceToDepthTest_2 (values 0..107 fit in both int8_t and uint8_t), - // exercised across the supported element types. + // Same data as SpaceToDepthTest_2 (values 0..107 fit in both int8_t and uint8_t). + // The typed suite covers float, MLFloat16, uint8_t and int8_t. The CPU kernel under + // test supports float/uint8_t/int8_t; MLFloat16 only runs on EPs that support it (e.g. CUDA). OpTester test("SpaceToDepth"); constexpr int64_t blocksize = 3; test.AddAttribute("blocksize", blocksize); @@ -220,7 +221,97 @@ TYPED_TEST(TensorOpTest, SpaceToDepthTest_int) { ORT_THROW("Type not supported"); } - // type not supported by QNN EP: MLFloat16 and unsigned char + // Exclude the QNN EP, which does not support all of the tested element types (e.g. MLFloat16 and 8-bit integer). + test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); +} + +// Explicitly exercises the opset 13 SpaceToDepth kernel registration with the supported element types. +// Uses small data within int8_t's range [-128, 127] so the float -> int8_t conversion is exact. +TYPED_TEST(TensorOpTest, SpaceToDepthTest_int_opset13) { + OpTester test("SpaceToDepth", 13); // create an opset 13 model + constexpr int64_t blocksize = 2; + test.AddAttribute("blocksize", blocksize); + constexpr int64_t N = 1, C = 2, H = 2, W = 4; + const std::vector X = {0., 1., 2., 3., 4., 5., 6., 7., + 8., 9., 10., 11., 12., 13., 14., 15.}; + const std::vector result = {0., 2., 8., 10., 1., 3., 9., 11., + 4., 6., 12., 14., 5., 7., 13., 15.}; + const std::vector output_shape = {N, C * blocksize * blocksize, H / blocksize, W / blocksize}; + + if constexpr (std::is_same::value) { + test.AddInput("input", {N, C, H, W}, X); + test.AddOutput("output", output_shape, result); + } else if constexpr (std::is_same::value) { + std::vector X_fp16(X.size()); + std::vector result_fp16(result.size()); + ConvertFloatToMLFloat16(X.data(), X_fp16.data(), X.size()); + ConvertFloatToMLFloat16(result.data(), result_fp16.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_fp16); + test.AddOutput("output", output_shape, result_fp16); + } else if constexpr (std::is_same::value) { + std::vector X_u8(X.size()); + std::vector result_u8(result.size()); + ConvertFloatToUint8_t(X.data(), X_u8.data(), X.size()); + ConvertFloatToUint8_t(result.data(), result_u8.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_u8); + test.AddOutput("output", output_shape, result_u8); + } else if constexpr (std::is_same::value) { + std::vector X_i8(X.size()); + std::vector result_i8(result.size()); + ConvertFloatToInt8_t(X.data(), X_i8.data(), X.size()); + ConvertFloatToInt8_t(result.data(), result_i8.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_i8); + test.AddOutput("output", output_shape, result_i8); + } else { + ORT_THROW("Type not supported"); + } + + // Exclude the QNN EP, which does not support all of the tested element types (e.g. MLFloat16 and 8-bit integer). + test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); +} + +// Explicitly exercises the opset 13 DepthToSpace kernel registration with the supported element types. +// Uses small data within int8_t's range [-128, 127] so the float -> int8_t conversion is exact. +TYPED_TEST(TensorOpTest, DepthToSpaceTest_int_opset13) { + OpTester test("DepthToSpace", 13); // create an opset 13 model (default mode = "DCR") + constexpr int64_t blocksize = 2; + test.AddAttribute("blocksize", blocksize); + constexpr int64_t N = 1, C = 8, H = 1, W = 2; + const std::vector X = {0., 1., 2., 3., 4., 5., 6., 7., + 8., 9., 10., 11., 12., 13., 14., 15.}; + const std::vector result = {0., 4., 1., 5., 8., 12., 9., 13., + 2., 6., 3., 7., 10., 14., 11., 15.}; + const std::vector output_shape = {N, C / (blocksize * blocksize), H * blocksize, W * blocksize}; + + if constexpr (std::is_same::value) { + test.AddInput("input", {N, C, H, W}, X); + test.AddOutput("output", output_shape, result); + } else if constexpr (std::is_same::value) { + std::vector X_fp16(X.size()); + std::vector result_fp16(result.size()); + ConvertFloatToMLFloat16(X.data(), X_fp16.data(), X.size()); + ConvertFloatToMLFloat16(result.data(), result_fp16.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_fp16); + test.AddOutput("output", output_shape, result_fp16); + } else if constexpr (std::is_same::value) { + std::vector X_u8(X.size()); + std::vector result_u8(result.size()); + ConvertFloatToUint8_t(X.data(), X_u8.data(), X.size()); + ConvertFloatToUint8_t(result.data(), result_u8.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_u8); + test.AddOutput("output", output_shape, result_u8); + } else if constexpr (std::is_same::value) { + std::vector X_i8(X.size()); + std::vector result_i8(result.size()); + ConvertFloatToInt8_t(X.data(), X_i8.data(), X.size()); + ConvertFloatToInt8_t(result.data(), result_i8.data(), result.size()); + test.AddInput("input", {N, C, H, W}, X_i8); + test.AddOutput("output", output_shape, result_i8); + } else { + ORT_THROW("Type not supported"); + } + + // Exclude the QNN EP, which does not support all of the tested element types (e.g. MLFloat16 and 8-bit integer). test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); } @@ -385,6 +476,10 @@ TYPED_TEST(TensorOpTest, DepthToSpaceTest_3) { test.AddInput("input", {N, C, H, W}, X_u8); test.AddOutput("output", {2, 3, 6, 4}, result_u8); } else if constexpr (std::is_same::value) { + // Note: this shared float data runs up to 143, outside int8_t's range [-128, 127], so the + // float -> int8_t conversion of those values is implementation-defined. That is intentional and + // harmless here: DepthToSpace is a pure permutation and the same conversion is applied to both the + // input and the expected output, so the values wrap identically on both sides and still match. std::vector X_i8(X.size()); std::vector result_i8(result.size()); ConvertFloatToInt8_t(X.data(), X_i8.data(), X.size()); @@ -456,6 +551,10 @@ TYPED_TEST(TensorOpTest, DepthToSpaceTest_4) { test.AddInput("input", {N, C, H, W}, X_u8); test.AddOutput("output", {2, 3, 6, 4}, result_u8); } else if constexpr (std::is_same::value) { + // Note: this shared float data runs up to 143, outside int8_t's range [-128, 127], so the + // float -> int8_t conversion of those values is implementation-defined. That is intentional and + // harmless here: DepthToSpace is a pure permutation and the same conversion is applied to both the + // input and the expected output, so the values wrap identically on both sides and still match. std::vector X_i8(X.size()); std::vector result_i8(result.size()); ConvertFloatToInt8_t(X.data(), X_i8.data(), X.size()); From e4be642955a0090789a93ca295ffa0bb89c0cf26 Mon Sep 17 00:00:00 2001 From: Arsalan Shakil Date: Sun, 21 Jun 2026 12:06:21 +0300 Subject: [PATCH 3/3] Exclude TensorRT EP from int8 SpaceToDepth/DepthToSpace tests The TensorRT CI pipelines failed on the new int8 (signed char) typed test cases. Unlike uint8, which TensorRT rejects up front and falls back to CPU, TensorRT accepts the int8 node and then fails at engine build time because int8 I/O without Q/DQ layers requires a calibrator or a set dynamic range. These ops' 8-bit integer support targets the CPU EP, so exclude the TensorRT EP for the int8 type parameter only (via if constexpr), preserving the existing float/uint8 TensorRT coverage. The CPU kernel itself is unchanged and all 38 SpaceToDepth/DepthToSpace tests pass. --- .../cpu/tensor/space_depth_ops_test.cc | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc b/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc index 4c05c48352dfb..05c4374df1df8 100644 --- a/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/space_depth_ops_test.cc @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include +#include + #include "gtest/gtest.h" #include "test/providers/provider_test_utils.h" #include "core/providers/cpu/tensor/space_depth_ops.h" @@ -222,7 +225,14 @@ TYPED_TEST(TensorOpTest, SpaceToDepthTest_int) { } // Exclude the QNN EP, which does not support all of the tested element types (e.g. MLFloat16 and 8-bit integer). - test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); + std::unordered_set excluded_eps = {kQnnExecutionProvider}; + if constexpr (std::is_same::value) { + // TensorRT does not reject int8 input up front (as it does for uint8); instead it accepts the node + // and then fails at engine build time because int8 I/O without Q/DQ layers needs a calibrator or a + // set dynamic range. This op's 8-bit integer support targets the CPU EP, so exclude TensorRT here. + excluded_eps.insert(kTensorrtExecutionProvider); + } + test.Run(OpTester::ExpectResult::kExpectSuccess, "", excluded_eps); } // Explicitly exercises the opset 13 SpaceToDepth kernel registration with the supported element types. @@ -267,7 +277,14 @@ TYPED_TEST(TensorOpTest, SpaceToDepthTest_int_opset13) { } // Exclude the QNN EP, which does not support all of the tested element types (e.g. MLFloat16 and 8-bit integer). - test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); + std::unordered_set excluded_eps = {kQnnExecutionProvider}; + if constexpr (std::is_same::value) { + // TensorRT does not reject int8 input up front (as it does for uint8); instead it accepts the node + // and then fails at engine build time because int8 I/O without Q/DQ layers needs a calibrator or a + // set dynamic range. This op's 8-bit integer support targets the CPU EP, so exclude TensorRT here. + excluded_eps.insert(kTensorrtExecutionProvider); + } + test.Run(OpTester::ExpectResult::kExpectSuccess, "", excluded_eps); } // Explicitly exercises the opset 13 DepthToSpace kernel registration with the supported element types. @@ -312,7 +329,14 @@ TYPED_TEST(TensorOpTest, DepthToSpaceTest_int_opset13) { } // Exclude the QNN EP, which does not support all of the tested element types (e.g. MLFloat16 and 8-bit integer). - test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); + std::unordered_set excluded_eps = {kQnnExecutionProvider}; + if constexpr (std::is_same::value) { + // TensorRT does not reject int8 input up front (as it does for uint8); instead it accepts the node + // and then fails at engine build time because int8 I/O without Q/DQ layers needs a calibrator or a + // set dynamic range. This op's 8-bit integer support targets the CPU EP, so exclude TensorRT here. + excluded_eps.insert(kTensorrtExecutionProvider); + } + test.Run(OpTester::ExpectResult::kExpectSuccess, "", excluded_eps); } TEST(TensorOpTest, DepthToSpaceTest_1) { @@ -491,7 +515,14 @@ TYPED_TEST(TensorOpTest, DepthToSpaceTest_3) { } // type not supported by QNN EP: MLFloat16 and unsigned char - test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); + std::unordered_set excluded_eps = {kQnnExecutionProvider}; + if constexpr (std::is_same::value) { + // TensorRT does not reject int8 input up front (as it does for uint8); instead it accepts the node + // and then fails at engine build time because int8 I/O without Q/DQ layers needs a calibrator or a + // set dynamic range. This op's 8-bit integer support targets the CPU EP, so exclude TensorRT here. + excluded_eps.insert(kTensorrtExecutionProvider); + } + test.Run(OpTester::ExpectResult::kExpectSuccess, "", excluded_eps); } TYPED_TEST(TensorOpTest, DepthToSpaceTest_4) { @@ -566,7 +597,14 @@ TYPED_TEST(TensorOpTest, DepthToSpaceTest_4) { } // type not supported by QNN EP: MLFloat16 and unsigned char - test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); + std::unordered_set excluded_eps = {kQnnExecutionProvider}; + if constexpr (std::is_same::value) { + // TensorRT does not reject int8 input up front (as it does for uint8); instead it accepts the node + // and then fails at engine build time because int8 I/O without Q/DQ layers needs a calibrator or a + // set dynamic range. This op's 8-bit integer support targets the CPU EP, so exclude TensorRT here. + excluded_eps.insert(kTensorrtExecutionProvider); + } + test.Run(OpTester::ExpectResult::kExpectSuccess, "", excluded_eps); } TYPED_TEST(TensorOpTest, DepthToSpaceTest_5) { @@ -619,7 +657,14 @@ TYPED_TEST(TensorOpTest, DepthToSpaceTest_5) { } // type not supported by QNN EP: MLFloat16 and unsigned char - test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kQnnExecutionProvider}); + std::unordered_set excluded_eps = {kQnnExecutionProvider}; + if constexpr (std::is_same::value) { + // TensorRT does not reject int8 input up front (as it does for uint8); instead it accepts the node + // and then fails at engine build time because int8 I/O without Q/DQ layers needs a calibrator or a + // set dynamic range. This op's 8-bit integer support targets the CPU EP, so exclude TensorRT here. + excluded_eps.insert(kTensorrtExecutionProvider); + } + test.Run(OpTester::ExpectResult::kExpectSuccess, "", excluded_eps); } TEST(TensorOpTest, DepthToSpaceTest_CRD_Batched) {