/* * 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 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 OpCatOutTest : public OperatorTest { protected: Tensor& op_cat_out(TensorList tensors, int64_t dim, Tensor& out) { return torch::executor::aten::cat_outf(context_, tensors, dim, out); } template void test_dtype() { TensorFactory tf; // Will be concatenated along dim[1]. Use different input values so we can // see where each output value came from. Tensor x = tf.ones({2, 1}); Tensor y = tf.zeros({2, 1}); std::vector inputs = {x, y}; Tensor out = tf.ones({2, 2}); op_cat_out(ArrayRef(inputs.data(), inputs.size()), /*dim=*/1, out); // clang-format off Tensor expected = tf.make( {2, 2}, { 1, 0, 1, 0, }); // clang-format on EXPECT_TENSOR_EQ(out, expected); } }; TEST_F(OpCatOutTest, SmokeDim1) { TensorFactory tf; // Two tensors with the same number of dimensions and the same dim[0] // size, but different dim[1] sizes. These will be concatenated along dim[1]. // clang-format off Tensor x = tf.make( {2, 3}, { 1, 2, 3, 4, 5, 6, }); Tensor y = tf.make( {2, 1}, { 10, 20, }); // clang-format on std::vector inputs = {x, y}; // Output tensor with the shape of the two input tensors concatenated along // dim[1]. // - It should have the same number of dimensions as each input. // - For non-cat dimensions (dim[0]), it should have the same size as the // input tensors. // - For the cat dimension (dim[1]), its size should be the sum of the cat // dimensions of the inputs: in this case, 3 + 1. Tensor out = tf.zeros({2, 4}); // Concatenate along dim[1]. Tensor ret = op_cat_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/1, out); // Should always return the provided out Tensor. EXPECT_TENSOR_EQ(ret, out); // clang-format off Tensor expected = tf.make( {2, 4}, { 1, 2, 3, 10, 4, 5, 6, 20, }); // clang-format on EXPECT_TENSOR_EQ(out, expected); } TEST_F(OpCatOutTest, HalfSupport) { if (torch::executor::testing::SupportedFeatures::get()->is_aten) { GTEST_SKIP() << "Test Half support only for ExecuTorch mode"; } TensorFactory tf; Tensor x = tf.make({2, 3}, {1.5, -2.0, 3.25, 4.0, -5.5, 6.5}); Tensor y = tf.make({2, 1}, {10.0, 20.0}); std::vector inputs = {x, y}; Tensor out = tf.zeros({2, 4}); // Concatenate along dim[1]. Tensor ret = op_cat_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/1, out); Tensor expected = tf.make({2, 4}, {1.5, -2.0, 3.25, 10.0, 4.0, -5.5, 6.5, 20.0}); EXPECT_TENSOR_EQ(out, expected); } TEST_F(OpCatOutTest, NegativeDims) { TensorFactory tf; // Symmetrical input tensors can can be concatenated along any dimension. // clang-format off Tensor x = tf.make( {2, 2}, { 1, 2, 3, 4, }); Tensor y = tf.make( {2, 2}, { 10, 20, 30, 40, }); // clang-format on std::vector inputs = {x, y}; // Cat along dim[-1], which should be the same as dim[1]. Tensor out_neg1 = tf.zeros({2, 4}); op_cat_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/-1, out_neg1); Tensor out_1 = tf.zeros({2, 4}); op_cat_out(ArrayRef(inputs.data(), inputs.size()), /*dim=*/1, out_1); EXPECT_TENSOR_EQ(out_neg1, out_1); // Cat along dim[-2], which should be the same as dim[0]. Tensor out_neg2 = tf.zeros({4, 2}); op_cat_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/-2, out_neg2); Tensor out_0 = tf.zeros({4, 2}); op_cat_out(ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out_0); EXPECT_TENSOR_EQ(out_neg2, out_0); } /// A generic smoke test that works for any dtype that supports ones() and /// zeros(). TEST_F(OpCatOutTest, 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(OpCatOutTest, EmptyInputTensorShapeIgnored) { if (torch::executor::testing::SupportedFeatures::get()->is_aten) { GTEST_SKIP() << "ATen kernel doesn't ignore empty input tensor shape"; } TensorFactory tf; // An empty tensor with a shape totally different from the non-empty inputs. Tensor empty = tf.make({0, 10, 3}, {}); EXPECT_EQ(empty.numel(), 0); Tensor x = tf.ones({2, 2}); std::vector inputs = {x, empty, x}; // Output whose shape is appropriate for concatenating along dim[0]. Tensor out = tf.zeros({4, 2}); op_cat_out(ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out); // Success if it doesn't assert on the weird-shaped empty input. } TEST_F(OpCatOutTest, DimBounds) { TensorFactory tf; // Cat a single tensor, which can be done across any dimension and still // produces the same output shape. Tensor x = tf.ones({2, 2}); ArrayRef inputs(&x, 1); Tensor out = tf.zeros({2, 2}); // Some valid dim values. // Negative values work like python indices: -1 is the rightmost element, // -2 the second-from-rightmost, etc. const std::vector valid_dims = {0, 1, -1, -2}; for (int64_t dim : valid_dims) { op_cat_out(inputs, dim, out); // Success if it doesn't assert. } // Some invalid dim values. const std::vector invalid_dims = {2, -3}; for (int64_t dim : invalid_dims) { ET_EXPECT_KERNEL_FAILURE(context_, op_cat_out(inputs, dim, out)); } } TEST_F(OpCatOutTest, NoInputTensorsWithNonEmptyOutputDies) { TensorFactory tf; Tensor out = tf.ones({1}); // Providing an empty list of input tensors should // cause an assertion and kill the test process. ET_EXPECT_KERNEL_FAILURE( context_, op_cat_out(ArrayRef(), /*dim=*/0, out)); } TEST_F(OpCatOutTest, NoInputTensorsWithEmptyOutputDies) { TensorFactory tf; // Make an empty out tensor and demonstrate that it's empty. Tensor out = tf.make({0}, {}); EXPECT_EQ(out.numel(), 0); // Providing an empty list of input tensors should // cause an assertion and kill the test process. ET_EXPECT_KERNEL_FAILURE( context_, op_cat_out(ArrayRef(), /*dim=*/0, out)); } TEST_F(OpCatOutTest, MismatchedDtypesDies) { TensorFactory tf_int; TensorFactory tf_float; Tensor out = tf_int.zeros({4, 2}); // Same shape as the output, but a different dtype. std::vector inputs = {tf_float.ones({2, 2})}; ET_EXPECT_KERNEL_FAILURE( context_, op_cat_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out)); } TEST_F(OpCatOutTest, MismatchedDimensionsDies) { if (torch::executor::testing::SupportedFeatures::get()->is_aten) { GTEST_SKIP() << "ATen kernel can handle mismatched dimensions"; } TensorFactory tf; Tensor out = tf.zeros({2, 2}); // Same dtype and numel as the output, but a different number of dimensions. std::vector inputs = {tf.ones({1, 1, 1, 1})}; ET_EXPECT_KERNEL_FAILURE( context_, op_cat_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out)); } TEST_F(OpCatOutTest, MismatchedDimensionSizeDies) { if (torch::executor::testing::SupportedFeatures::get()->is_aten) { GTEST_SKIP() << "ATen kernel can handle mismatched dimension size"; } TensorFactory tf; Tensor out = tf.zeros({2, 2}); // Same dtype and number of dimensions as the output, but a different-sized 1 // dimension. std::vector inputs = {tf.ones({2, 3})}; ET_EXPECT_KERNEL_FAILURE( context_, op_cat_out( ArrayRef(inputs.data(), inputs.size()), /*dim=*/0, out)); } TEST_F(OpCatOutTest, WrongOutShapeDies) { if (torch::executor::testing::SupportedFeatures::get()->is_aten) { GTEST_SKIP() << "ATen kernel can handle wrong out shape"; } TensorFactory tf; // Should be {4, 3} to match the inputs when calling cat() with dim 0. Tensor out = tf.zeros({4, 5}); std::vector inputs = { tf.ones({2, 3}), tf.ones({2, 3}), }; ET_EXPECT_KERNEL_FAILURE( context_, op_cat_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.cat(x, 0) op = "op_cat_out" opt_extra_params = "0," dtype = "ScalarType::Int" check = "EXPECT_TENSOR_EQ" */ TEST_F(OpCatOutTest, DynamicShapeUpperBoundSameAsExpected) { /* %python out_args = "{8, 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({8, 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({8, 3}, torch::executor::TensorShapeDynamism::DYNAMIC_BOUND); op_cat_out(x, 0, out); EXPECT_TENSOR_EQ(out, expected); } TEST_F(OpCatOutTest, DynamicShapeUpperBoundLargerThanExpected) { /* %python out_args = "{10, 10}, 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({8, 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({10, 10}, torch::executor::TensorShapeDynamism::DYNAMIC_BOUND); op_cat_out(x, 0, out); EXPECT_TENSOR_EQ(out, expected); } TEST_F(OpCatOutTest, DynamicShapeUnbound) { if (!torch::executor::testing::SupportedFeatures::get()->output_resize) { GTEST_SKIP() << "Dynamic shape unbound not supported"; } /* %python out_args = "{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({8, 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}, torch::executor::TensorShapeDynamism::DYNAMIC_UNBOUND); op_cat_out(x, 0, out); EXPECT_TENSOR_EQ(out, expected); }