/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #include // Declares the operator #include #include #include #include #include #include #include #include using namespace ::testing; using exec_aten::ArrayRef; using exec_aten::ScalarType; using exec_aten::Tensor; using exec_aten::TensorList; using torch::executor::testing::TensorFactory; class OpStackOutTest : public OperatorTest { protected: Tensor& op_stack_out(TensorList tensors, int64_t dim, Tensor& out) { return torch::executor::aten::stack_outf(context_, tensors, dim, out); } template void test_dtype() { TensorFactory tf; // Will be stackd along out.dim(1). Use different input values so we can see // where each output value came from. Tensor x = tf.ones({3, 4}); Tensor y = tf.zeros({3, 4}); std::vector inputs = {x, y}; Tensor out = tf.ones({3, 2, 4}); op_stack_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/1, out); // The two tensors x and y are stacked along the 1st dimension with the // order [x, y], so the x and y should be equal to expected[:, 0, :] and // expected[:, 1, :] e.g. expected[i, 0, j] = x[i, j] and expected[i, 1, j] // = y[i, j] for any i in [-x.size(0), x.size(0)-1] and j in [-x.size(1), // x.size(1)-1] // clang-format off Tensor expected = tf.make( {3, 2, 4}, { // all ones below are from x, // and all zeros are from y. // [0, :, :] 1, 1, 1, 1, // [0, 0, :] 0, 0, 0, 0, // [0, 1, :] // [1, :, :] 1, 1, 1, 1, // [1, 0, :] 0, 0, 0, 0, // [1, 1, :] // [2, :, :] 1, 1, 1, 1, // [2, 0, :] 0, 0, 0, 0, // [2, 1, :] }); // clang-format on EXPECT_TENSOR_EQ(out, expected); } // Running stacking experiments along given dim. void run_stack_tests( const std::vector& inputs, int64_t dim, const Tensor& expected) { ArrayRef inputs_array(inputs.data(), inputs.size()); TensorFactory tf; const std::vector out_size( expected.sizes().begin(), expected.sizes().end()); Tensor out = tf.zeros(out_size); // Should always return the provided out Tensor. Tensor ret = op_stack_out(inputs_array, dim, out); EXPECT_TENSOR_EQ(out, ret); EXPECT_TENSOR_EQ(out, expected); ret = op_stack_out(inputs_array, /*dim=*/dim - out.dim(), out); EXPECT_TENSOR_EQ(out, ret); EXPECT_TENSOR_EQ(out, expected); } }; TEST_F(OpStackOutTest, InsertFront) { TensorFactory tf; // clang-format off Tensor x = tf.make( {3, 4}, { 1., 2., 3., 4., // [0, :] 5., 6., 7., 8., // [1, :] 9., 10., 11., 12., // [2, :] }); Tensor y = tf.make( {3, 4}, { -1., -2., -3., -4., // [0, :] -5., -6., -7., -8., // [1, :] -9., -10., -11., -12., // [2, :] }); // clang-format on const std::vector inputs = {x, y}; // Try to stack the two tensors in the front (dim = 0) // The size of expected output tensor should follow the below rules: // - For any input tensor, its size(i) == output.size(i) if i < dim, and its // size(i) == output.size(i+1) if i >= dim // - For the stack dimension (output[dim]), its size should be the number of // input tensors std::vector expected_size = {2, 3, 4}; // The two tensors x and y are stacked along the 0th dimension with the order // [x, y], so the x and y should be equal to expected[0, :, :] and expected[1, // :, :] e.g. expected[0, i, j] = x[i, j] and expected[1, i, j] = y[i, j] for // any i in [-x.size(0), x.size(0)-1] and j in [-x.size(1), x.size(1)-1] // clang-format off Tensor expected = tf.make( expected_size, { // [0, :, :] equals to x 1., 2., 3., 4., // [0, 0, :] 5., 6., 7., 8., // [0, 1, :] 9., 10., 11., 12., // [0, 2, :] // [1, :, :] equals to y -1., -2., -3., -4., // [1, 0, :] -5., -6., -7., -8., // [1, 1, :] -9., -10., -11., -12., // [1, 2, :] }); // clang-format on run_stack_tests(inputs, /*dim=*/0, expected); } TEST_F(OpStackOutTest, InsertMiddle) { TensorFactory tf; // Two tensors with same size. Stack them on multiple dimensions // clang-format off Tensor x = tf.make( {3, 4}, { 1., 2., 3., 4., // [0, :] 5., 6., 7., 8., // [1, :] 9., 10., 11., 12., // [2, :] }); Tensor y = tf.make( {3, 4}, { -1., -2., -3., -4., // [0, :] -5., -6., -7., -8., // [1, :] -9., -10., -11., -12., // [2, :] }); // clang-format on const std::vector inputs = {x, y}; // Try to stack the two tensors in the middle (dim = 1) // The size of expected output tensor should follow the below rules: // - For any input tensor, its size(i) == output.size(i) if i < dim, and its // size(i) == output.size(i+1) if i >= dim // - For the stack dimension (output[dim]), its size should be the number of // input tensors std::vector expected_size = {3, 2, 4}; // The two tensors x and y are stacked along the 1st dimension with the order // [x, y], so the x and y should be equal to expected[:, 0, :] and expected[:, // 1, :] e.g. expected[i, 0, j] = x[i, j] and expected[i, 1, j] = y[i, j] for // any i in [-x.size(0), x.size(0)-1] and j in [-x.size(1), x.size(1)-1] // clang-format off Tensor expected = tf.make( expected_size, { // [0, :, :] 1., 2., 3., 4., // [0, 0, :] = x[0, :] -1., -2., -3., -4., // [0, 1, :] = y[0, :] // [1, :, :] 5., 6., 7., 8., // [1, 0, :] = x[1, :] -5., -6., -7., -8., // [1, 1, :] = y[1, :] // [2, :, :] 9., 10., 11., 12., // [2, 0, :] = x[2, :] -9., -10., -11., -12., // [2, 1, :] = y[2, :] }); // clang-format on run_stack_tests(inputs, /*dim=*/1, expected); } TEST_F(OpStackOutTest, InsertEnd) { TensorFactory tf; // Two tensors with same size. Stack them on multiple dimensions // clang-format off Tensor x = tf.make( {3, 4}, { 1., 2., 3., 4., // [0, :] 5., 6., 7., 8., // [1, :] 9., 10., 11., 12., // [2, :] }); Tensor y = tf.make( {3, 4}, { -1., -2., -3., -4., // [0, :] -5., -6., -7., -8., // [1, :] -9., -10., -11., -12., // [2, :] }); // clang-format on const std::vector inputs = {x, y}; // Try to stack the two tensors at the end (dim = 2) // The size of expected output tensor should follow the below rules: // - For any input tensor, its size(i) == output.size(i) if i < dim, and its // size(i) == output.size(i+1) if i >= dim // - For the stack dimension (output[dim]), its size should be the number of // input tensors std::vector expected_size = {3, 4, 2}; // The two tensors x and y are stacked along the 2nd dimension with the order // [x, y], so the x and y should be equal to expected[:, :, 0] and expected[:, // :, 1] e.g. expected[i, j, 0] = x[i, j] and expected[i, j, 1] = y[i, j] for // any i in [-x.size(0), x.size(0)-1] and j in [-x.size(1), x.size(1)-1] // clang-format off Tensor expected = tf.make( expected_size, { // All values in the first column are from x, // and the second column are from y // [0, :, :] 1., -1., // [0, 0, :] 2., -2., // [0, 1, :] 3., -3., // [0, 2, :] 4., -4., // [0, 3, :] // [1, :, :] 5., -5., // [1, 0, :] 6., -6., // [1, 1, :] 7., -7., // [1, 2, :] 8., -8., // [1, 3, :] // [2, :, :] 9., -9., // [2, 0, :] 10., -10., // [2, 1, :] 11., -11., // [2, 2, :] 12., -12., // [2, 3, :] }); // clang-format on run_stack_tests(inputs, /*dim=*/2, expected); } /// A generic smoke test that works for any dtype that supports ones() and /// zeros(). TEST_F(OpStackOutTest, AllDtypesSupported) { #define TEST_ENTRY(ctype, dtype) test_dtype(); ET_FORALL_REAL_TYPES_AND(Bool, TEST_ENTRY); #undef TEST_ENTRY // TODO: Also add tests for half, complex, quantized, and other types. Easiest // way to do that would be to make TensorFactory support zeros() and ones() // for those types. } TEST_F(OpStackOutTest, NoInputTensorsWithEmptyOutTensorFails) { TensorFactory tf; // Make an empty out tensor and demonstrate that it's empty. Tensor out = tf.make({0}, {}); EXPECT_EQ(out.numel(), 0); // Pass an empty list of input tensors. ET_EXPECT_KERNEL_FAILURE( context_, op_stack_out(ArrayRef(), /*dim=*/0, out)); } TEST_F(OpStackOutTest, AllEmptyInputTensors) { TensorFactory tf; // Using empty tensors as input. Tensor empty = tf.make({0, 10, 3}, {}); EXPECT_EQ(empty.numel(), 0); std::vector inputs = {empty, empty, empty}; Tensor x = tf.ones({2, 2}); // Output whose shape is appropriate for stacking along out.dim(0). Tensor out = tf.make({3, 0, 10, 3}, {}); EXPECT_EQ(out.numel(), 0); Tensor ret = op_stack_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out); EXPECT_EQ(ret.numel(), 0); // Success if it doesn't assert on the weird-shaped empty input and the // empty_out is still a empty array } TEST_F(OpStackOutTest, DimOutOfBoundDies) { TensorFactory tf; // Stack a single tensor with size [1, 1]. The size of output would always be // [1, 1, 1] no matter stack on which dimension. Tensor x = tf.ones({1, 1}); ArrayRef inputs(&x, 1); Tensor out = tf.zeros({1, 1, 1}); // Some invalid dim values. const std::vector invalid_dims = {3, 4, 5, -4, -5, -6}; for (int64_t dim : invalid_dims) { ET_EXPECT_KERNEL_FAILURE(context_, op_stack_out(inputs, dim, out)); } } TEST_F(OpStackOutTest, MismatchedDtypesDies) { TensorFactory tf_int; TensorFactory tf_float; Tensor out = tf_int.zeros({1, 2, 2}); // Size is compatible to the output, but a mismatched dtype. std::vector inputs = {tf_float.ones({2, 2})}; ET_EXPECT_KERNEL_FAILURE( context_, op_stack_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out)); } TEST_F(OpStackOutTest, OutMatchNumelWithExtraDimAtEndDies) { if (torch::executor::testing::SupportedFeatures::get()->is_aten) { GTEST_SKIP() << "ATen kernel can handle out with mismatched dimensions"; } TensorFactory tf; Tensor out = tf.zeros({1, 2, 2, 1}); // Same dtype and numel as the output, but a mixmatched size (output.dim() // should always one greater than input.dim()) std::vector inputs = {tf.ones({2, 2})}; ET_EXPECT_KERNEL_FAILURE( context_, op_stack_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out)); } TEST_F(OpStackOutTest, OutMatchNumelLackDimAtFrontDies) { if (torch::executor::testing::SupportedFeatures::get()->is_aten) { GTEST_SKIP() << "ATen kernel can handle out with mismatched dimensions"; } TensorFactory tf; Tensor out = tf.zeros({2, 2}); // Same dtype and numel as the output, but a mixmatched size (output.dim() // should always one greater than input.dim()) std::vector inputs = {tf.ones({2, 2})}; ET_EXPECT_KERNEL_FAILURE( context_, op_stack_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out)); } TEST_F(OpStackOutTest, OutRegularMismatchDimDies) { if (torch::executor::testing::SupportedFeatures::get()->is_aten) { GTEST_SKIP() << "ATen kernel can handle out with mismatched dimensions"; } TensorFactory tf; // Should be {2, 2, 3} to match the inputs when calling stack() with dim 0. Tensor out = tf.zeros({2, 4, 5}); std::vector inputs = { tf.ones({2, 3}), tf.ones({2, 3}), }; ET_EXPECT_KERNEL_FAILURE( context_, op_stack_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out)); } /* %python import torch torch.manual_seed(0) x = [torch.randint(10, (2, 3)), torch.randint(10, (2, 3)), torch.randint(10, (2, 3)), torch.randint(10, (2, 3))] res = torch.stack(x, 0) op = "op_stack_out" opt_extra_params = "0," dtype = "ScalarType::Int" check = "EXPECT_TENSOR_EQ" */ TEST_F(OpStackOutTest, DynamicShapeUpperBoundSameAsExpected) { /* %python out_args = "{4, 2, 3}, torch::executor::TensorShapeDynamism::DYNAMIC_BOUND" %rewrite(unary_op_tensor_list_in) */ TensorFactory tf; std::vector xv = { tf.make({2, 3}, {4, 9, 3, 0, 3, 9}), tf.make({2, 3}, {7, 3, 7, 3, 1, 6}), tf.make({2, 3}, {6, 9, 8, 6, 6, 8}), tf.make({2, 3}, {4, 3, 6, 9, 1, 4})}; TensorList x(xv.data(), xv.size()); Tensor expected = tf.make({4, 2, 3}, {4, 9, 3, 0, 3, 9, 7, 3, 7, 3, 1, 6, 6, 9, 8, 6, 6, 8, 4, 3, 6, 9, 1, 4}); Tensor out = tf.zeros({4, 2, 3}, torch::executor::TensorShapeDynamism::DYNAMIC_BOUND); op_stack_out(x, 0, out); EXPECT_TENSOR_EQ(out, expected); } TEST_F(OpStackOutTest, DynamicShapeUpperBoundLargerThanExpected) { if (!torch::executor::testing::SupportedFeatures::get()->output_resize) { GTEST_SKIP() << "Dynamic shape not supported"; } /* %python out_args = "{5, 5, 5}, torch::executor::TensorShapeDynamism::DYNAMIC_BOUND" %rewrite(unary_op_tensor_list_in) */ TensorFactory tf; std::vector xv = { tf.make({2, 3}, {4, 9, 3, 0, 3, 9}), tf.make({2, 3}, {7, 3, 7, 3, 1, 6}), tf.make({2, 3}, {6, 9, 8, 6, 6, 8}), tf.make({2, 3}, {4, 3, 6, 9, 1, 4})}; TensorList x(xv.data(), xv.size()); Tensor expected = tf.make({4, 2, 3}, {4, 9, 3, 0, 3, 9, 7, 3, 7, 3, 1, 6, 6, 9, 8, 6, 6, 8, 4, 3, 6, 9, 1, 4}); Tensor out = tf.zeros({5, 5, 5}, torch::executor::TensorShapeDynamism::DYNAMIC_BOUND); op_stack_out(x, 0, out); EXPECT_TENSOR_EQ(out, expected); } TEST_F(OpStackOutTest, DynamicShapeUnbound) { if (!torch::executor::testing::SupportedFeatures::get()->output_resize) { GTEST_SKIP() << "Dynamic shape not supported"; } /* %python out_args = "{1, 1, 1}, torch::executor::TensorShapeDynamism::DYNAMIC_UNBOUND" %rewrite(unary_op_tensor_list_in) */ TensorFactory tf; std::vector xv = { tf.make({2, 3}, {4, 9, 3, 0, 3, 9}), tf.make({2, 3}, {7, 3, 7, 3, 1, 6}), tf.make({2, 3}, {6, 9, 8, 6, 6, 8}), tf.make({2, 3}, {4, 3, 6, 9, 1, 4})}; TensorList x(xv.data(), xv.size()); Tensor expected = tf.make({4, 2, 3}, {4, 9, 3, 0, 3, 9, 7, 3, 7, 3, 1, 6, 6, 9, 8, 6, 6, 8, 4, 3, 6, 9, 1, 4}); Tensor out = tf.zeros( {1, 1, 1}, torch::executor::TensorShapeDynamism::DYNAMIC_UNBOUND); op_stack_out(x, 0, out); EXPECT_TENSOR_EQ(out, expected); }