From c492527ddfb53a52352812f75553bfa916961b15 Mon Sep 17 00:00:00 2001 From: SeungHui Youn <61981457+zetwhite@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:19:12 +0900 Subject: [PATCH] [luci/pass] Introduce RemoveUnnecessaryTransposeNetPass (#11976) This PR introduces RemoveUnnecessaryTransposeNetPass and related unit tests. ONE-DCO-1.0-Signed-off-by: SeungHui Youn --- .../luci/pass/include/luci/CircleOptimizer.h | 1 + .../Pass/RemoveUnnecessaryTransposeNetPass.h | 34 ++ compiler/luci/pass/src/CircleOptimizer.cpp | 5 + .../src/RemoveUnnecessaryTransposeNetPass.cpp | 450 ++++++++++++++++++ ...RemoveUnnecessaryTransposeNetPass.test.cpp | 265 +++++++++++ 5 files changed, 755 insertions(+) create mode 100644 compiler/luci/pass/include/luci/Pass/RemoveUnnecessaryTransposeNetPass.h create mode 100644 compiler/luci/pass/src/RemoveUnnecessaryTransposeNetPass.cpp create mode 100644 compiler/luci/pass/src/RemoveUnnecessaryTransposeNetPass.test.cpp diff --git a/compiler/luci/pass/include/luci/CircleOptimizer.h b/compiler/luci/pass/include/luci/CircleOptimizer.h index ebd7a3f1099..fc5f0455e9e 100644 --- a/compiler/luci/pass/include/luci/CircleOptimizer.h +++ b/compiler/luci/pass/include/luci/CircleOptimizer.h @@ -84,6 +84,7 @@ class CircleOptimizer final RemoveUnnecessaryStridedSlice, RemoveUnnecessarySplit, RemoveUnnecessaryReshape, + RemoveUnnecessaryTranspose, TransformMinMaxToRelu6Pass, TransformMinReluToRelu6Pass, DecomposeHardSwishPass, diff --git a/compiler/luci/pass/include/luci/Pass/RemoveUnnecessaryTransposeNetPass.h b/compiler/luci/pass/include/luci/Pass/RemoveUnnecessaryTransposeNetPass.h new file mode 100644 index 00000000000..c7f74650008 --- /dev/null +++ b/compiler/luci/pass/include/luci/Pass/RemoveUnnecessaryTransposeNetPass.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LUCI_REMOVE_UNNECESSARY_TRANSPOSE_NET_PASS_H__ +#define __LUCI_REMOVE_UNNECESSARY_TRANSPOSE_NET_PASS_H__ + +#include + +namespace luci +{ + +struct RemoveUnnecessaryTransposeNetPass final : public logo::Pass +{ + const char *name(void) const final { return "luci::RemoveUnnecessaryTransposeNetPass"; } + + bool run(loco::Graph *g) final; +}; + +} // namespace luci + +#endif // __LUCI_REMOVE_UNNECESSARY_TRANSPOSE_NET_PASS_H__ diff --git a/compiler/luci/pass/src/CircleOptimizer.cpp b/compiler/luci/pass/src/CircleOptimizer.cpp index 63573818910..b3102805b14 100644 --- a/compiler/luci/pass/src/CircleOptimizer.cpp +++ b/compiler/luci/pass/src/CircleOptimizer.cpp @@ -57,6 +57,7 @@ #include "luci/Pass/RemoveUnnecessarySlicePass.h" #include "luci/Pass/RemoveUnnecessaryStridedSlicePass.h" #include "luci/Pass/RemoveUnnecessarySplitPass.h" +#include "luci/Pass/RemoveUnnecessaryTransposeNetPass.h" #include "luci/Pass/ReplaceNonConstFCWithBatchMatMulPass.h" #include "luci/Pass/ReplaceMulAddWithDepthwiseConvPass.h" #include "luci/Pass/ReplaceSubWithAddPass.h" @@ -401,6 +402,10 @@ void CircleOptimizer::optimize(loco::Graph *g) const { phase.emplace_back(std::make_unique()); } + if (_options->query(Options::Algorithm::RemoveUnnecessaryTranspose)) + { + phase.emplace_back(std::make_unique()); + } if (_options->query(Options::Algorithm::RemoveRedundantReshape)) { phase.emplace_back(std::make_unique()); diff --git a/compiler/luci/pass/src/RemoveUnnecessaryTransposeNetPass.cpp b/compiler/luci/pass/src/RemoveUnnecessaryTransposeNetPass.cpp new file mode 100644 index 00000000000..c2807639938 --- /dev/null +++ b/compiler/luci/pass/src/RemoveUnnecessaryTransposeNetPass.cpp @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/RemoveUnnecessaryTransposeNetPass.h" + +#include +#include + +#include + +namespace +{ + +class TaggedShapeAnalyzer final +{ +public: + /** + * @brief check 'Transpose-Reshape-Transpose' can be replaced by one 'Reshape'. + * + * @example + * Let's explain how analyzer check Transpose-Reshape-Transpose pattern with an exact example. + * + * Let's assume under pattern is given : + * + * Input(1, 7, 7, 448) + * | + * Transpose(perm=(0, 3, 1, 2)) + * | + * Resahape(shape=(1, 448, 49)) + * | + * Transpose(perm=(0, 2, 1)) + * | + * Output(1, 49, 448) + * + * It simulates how each dimension of the tensor's shape are transformed/moved + * using a member variable named '_shape'. + * 'tags' in _shape record the initial order of each dimension. + * + * TIMELINE | _shape states : + * | + * init_shape_with_tag | - value : (1) (7) (7) (448) + * | - tags : (-) (0) (1) (2) + * | + * analyze_transpose | - value : (1) (448) (7) (7) + * | - tags : (-) (2) (0) (1) + * | + * analyze_reshape | - value : (1) (448) (49) + * | - tags : (-) (2) (0, 1) + * | + * anaylze_transpose | - value : (1) (49) (448) + * | - tags : (-) (0, 1) (2) + * + * After all simulation done, if tags are in same order as initial _shape, + * Transpose has no effect in final shape, which they can be removed as + * unnecessary Ops. + */ + template + bool can_remove_transposes(const luci::CircleTranspose *f_tr, const luci::CircleReshape *m_rs, + const luci::CircleTranspose *b_tr); + +private: + void init_shape_with_tag(const luci::CircleNode *); + + template void analyze_transpose(const luci::CircleTranspose *); + + template bool analyze_reshape(const luci::CircleReshape *); + + bool verify_tag() const; + + struct Dim final + { + uint32_t value; + std::vector tags; + }; + + const uint8_t START_TAG = 0; + + using Shape = std::vector; + Shape _shape; +}; + +/** + * @brief initalize _shape with input tensor named in_tensor + * + * @note 'tags' are attached to non-1 valued dimension. + */ +void TaggedShapeAnalyzer::init_shape_with_tag(const luci::CircleNode *in_tensor) +{ + _shape.clear(); + uint8_t tag = START_TAG; + + for (uint32_t i = 0; i < in_tensor->rank(); i++) + { + TaggedShapeAnalyzer::Dim dim; + { + dim.value = in_tensor->dim(i).value(); + if (dim.value != 1) + dim.tags.push_back(tag++); + } + _shape.push_back(dim); + } +} + +/** + * @brief update _shape based on 'Transpose' permutation value + * + * @example Let's assume Transpose(perm=0, 3, 1, 2) is given to [before] _shape. + * + * This function reordered the Dims' order based on permutaiton value. + * + * [before] _shape : + * - value : (1) (7) (7) (448) + * - tags : (-) (0) (1) (2) + * + * [after] _shape : + * - value : (1) (448) (7) (7) + * - tags : (-) (2) (0) (1) + */ +template +void TaggedShapeAnalyzer::analyze_transpose(const luci::CircleTranspose *transpose_node) +{ + const luci::CircleConst *perm_node = loco::must_cast(transpose_node->perm()); + assert(perm_node->dtype() == PermType); + + TaggedShapeAnalyzer::Shape new_shape; + const auto size = perm_node->size(); + for (uint32_t i = 0; i < size; i++) + { + auto perm_idx = perm_node->at(i); + new_shape.push_back(_shape.at(perm_idx)); + } + _shape = new_shape; +} + +/** + * @brief update _shape based on 'Reshape' shape value + * + * @return False, if it determined that removing transposes is impossible + * + * @example Let's assume Reshape(shape=1, 448, 49) is given to [before] _shape. + * + * [before] _shape : + * - value : (1) (448) (7) (7) + * - tags : (-) (2) (0) (1) + * + * [after] _shape : + * - value : (1) (448) (49) + * - tags : (-) (2) (0, 1) + */ +template +bool TaggedShapeAnalyzer::analyze_reshape(const luci::CircleReshape *reshape_node) +{ + const luci::CircleConst *shape_node = loco::must_cast(reshape_node->shape()); + assert(shape_node->dtype() == ReshapeType); + + // At least one element must be in reshape's output-tensor. + if (shape_node->size() <= 0) + return false; + + // Create new_shape based on reshape_node/shape + Shape new_shape; + for (uint32_t i = 0; i < shape_node->size(); i++) + { + TaggedShapeAnalyzer::Dim dim; + dim.value = shape_node->at(i); + + new_shape.push_back(dim); + } + + // indexing for _shape [old_shape_start_idx, old_shape_end_idx) + uint32_t old_shape_start_idx = 0; + uint32_t old_shape_end_idx = 1; + uint32_t old_shape_product = _shape[old_shape_start_idx].value; + + auto expand_range = [&]() -> bool { + if (old_shape_end_idx >= _shape.size()) + return false; + + old_shape_product *= _shape[old_shape_end_idx].value; + old_shape_end_idx++; + return true; + }; + + auto move_to_next_range = [&]() -> bool { + if (old_shape_end_idx >= _shape.size()) + return false; + + old_shape_start_idx = old_shape_end_idx; + old_shape_end_idx++; + old_shape_product = _shape[old_shape_start_idx].value; + return true; + }; + + // Add tags from '_shape' to the 'new_shape' + uint32_t new_shape_idx = 0; + while (new_shape_idx < new_shape.size()) + { + Dim &target_dim = new_shape[new_shape_idx]; + + // Ignore dim == 1 + if (target_dim.value == 1) + { + new_shape_idx++; + continue; + } + + while (old_shape_product < target_dim.value) + { + if (expand_range() == false) + break; + } + + if (old_shape_product != target_dim.value) + return false; + + assert(old_shape_product == target_dim.value); + for (uint32_t idx = old_shape_start_idx; idx < old_shape_end_idx; idx++) + { + const auto &old_tags = _shape[idx].tags; + target_dim.tags.insert(target_dim.tags.end(), old_tags.begin(), old_tags.end()); + } + + new_shape_idx++; + move_to_next_range(); + } + _shape = new_shape; + return true; +} + +bool TaggedShapeAnalyzer::verify_tag() const +{ + // check whether tags in _shape are incremental + uint8_t tag = START_TAG; + for (const auto &dim : _shape) + { + for (const auto &t : dim.tags) + { + if (t == tag) + tag++; + else + return false; + } + } + return true; +} + +// For implementation details, please refer the comment with declaration. +template +bool TaggedShapeAnalyzer::can_remove_transposes(const luci::CircleTranspose *f_tr, + const luci::CircleReshape *m_rs, + const luci::CircleTranspose *b_tr) +{ + assert(loco::must_cast(f_tr->perm())->dtype() == DType); + assert(loco::must_cast(m_rs->shape())->dtype() == DType); + assert(loco::must_cast(b_tr->perm())->dtype() == DType); + + const luci::CircleNode *in_tensor = loco::must_cast(f_tr->a()); + + init_shape_with_tag(in_tensor); + + analyze_transpose(f_tr); + + if (not analyze_reshape(m_rs)) + return false; + + analyze_transpose(b_tr); + + if (not verify_tag()) + return false; + + return true; +} + +/** + * @brief create CircleReshape node that reshapes 'front_transpose input tensor shape' into + * 'back_transposes output tensor shape' + */ +template +luci::CircleReshape *create_reshape_node(loco::Graph *graph, + const luci::CircleTranspose *front_transpose, + const luci::CircleReshape *mid_reshape, + const luci::CircleTranspose *back_transpose) +{ + std::string composed_name = + front_transpose->name() + ";" + mid_reshape->name() + ";" + back_transpose->name(); + + std::vector> src_origin{luci::get_origin(front_transpose), + luci::get_origin(mid_reshape), + luci::get_origin(back_transpose)}; + auto const composed_origin = luci::composite_origin(src_origin); + + auto shape_node = graph->nodes()->create(); + { + shape_node->dtype(ShapeType); + shape_node->rank(1); + shape_node->dim(0).set(back_transpose->rank()); + + shape_node->size(back_transpose->rank()); + for (uint32_t i = 0; i < back_transpose->rank(); i++) + { + shape_node->at(i) = back_transpose->dim(i).value(); + } + shape_node->shape_status(luci::ShapeStatus::VALID); + shape_node->name(composed_name + "/shape"); + luci::add_origin(shape_node, composed_origin); + } + + auto reshape_node = graph->nodes()->create(); + { + reshape_node->name(composed_name); + reshape_node->tensor(front_transpose->a()); + reshape_node->shape(shape_node); + luci::add_origin(reshape_node, composed_origin); + } + return reshape_node; +} + +bool remove_unnecessary_transpose(luci::CircleTranspose *node) +{ + // find 'front_transpose - mid_reshape - back_transpose' pattern + const auto back_transpose = node; + const auto mid_reshape = dynamic_cast(back_transpose->a()); + { + if (mid_reshape == nullptr) + return false; + } + const auto front_transpose = dynamic_cast(mid_reshape->tensor()); + { + if (not front_transpose) + return false; + } + + // check perm and shape are CircleConst node and its' datatype is S32 + const auto back_perm = dynamic_cast(back_transpose->perm()); + { + if (back_perm == nullptr) + return false; + + if (back_perm->dtype() != loco::DataType::S32) + return false; + } + const auto shape = dynamic_cast(mid_reshape->shape()); + { + if (shape == nullptr) + return false; + + if (shape->dtype() != loco::DataType::S32) + return false; + } + const auto front_perm = dynamic_cast(front_transpose->perm()); + { + if (front_perm == nullptr) + return false; + + if (front_perm->dtype() != loco::DataType::S32) + return false; + } + + // for now, handle only rank reduction equal (not expansion) cases + const auto output_rank = back_transpose->rank(); + const auto input_rank = front_transpose->rank(); + if (input_rank < output_rank) + return false; + + // analyze pattern to check this pass is applicable + TaggedShapeAnalyzer analyzer; + if (not analyzer.can_remove_transposes(front_transpose, mid_reshape, + back_transpose)) + return false; + + // repalce with new_node + luci::CircleReshape *new_node = create_reshape_node( + node->graph(), front_transpose, mid_reshape, back_transpose); + + replace(node).with(new_node); + + return true; +} + +} // namespace + +namespace luci +{ + +/** + * BEFORE + * + * Current pass only targets below cases: + * - in.rank() >= out.rank() + * - 'Reshape' used to reduce N dimension into one (e.g. A x B x C => A x BC) + * + * + * [CircleNode] [CircleConst] + * (in) (perm) + * \ / + * [CircleTranspose] [CircleConst] + * \ (shape) + * \ / + * [CircleReshape] [CircleConst] + * \ (perm) + * \ / + * [CircleTranspose] + * \ + * \ + * [CircleNode] + * (out) + * + * AFTER + * + * [CircleNode] [CircleConst] + * (in) (shape) + * \ / + * [CircleReshape] + * (new) + * \ + * [CircleNode] + * (out) + * + */ + +bool RemoveUnnecessaryTransposeNetPass::run(loco::Graph *g) +{ + bool changed = false; + + for (auto node : loco::active_nodes(loco::output_nodes(g))) + { + if (auto transpose_node = dynamic_cast(node)) + { + if (remove_unnecessary_transpose(transpose_node)) + changed = true; + } + } + + return changed; +} + +} // namespace luci diff --git a/compiler/luci/pass/src/RemoveUnnecessaryTransposeNetPass.test.cpp b/compiler/luci/pass/src/RemoveUnnecessaryTransposeNetPass.test.cpp new file mode 100644 index 00000000000..dd91bb062b3 --- /dev/null +++ b/compiler/luci/pass/src/RemoveUnnecessaryTransposeNetPass.test.cpp @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/RemoveUnnecessaryTransposeNetPass.h" + +#include + +#include + +#include + +namespace +{ + +using namespace luci::test; + +class TransposeReshapeTransposeGraph : public TestIOGraph +{ + +public: + // create input-transpose-reshape-transpose-output graph + void init_whole_graph(ShapeU32 in_shape, ShapeU32 front_perm, ShapeU32 mid_shape, + ShapeU32 back_perm, ShapeU32 out_shape) + { + TestIOGraph::init(in_shape, out_shape); + + _front_perm = g()->nodes()->create(); + { + _front_perm->name("front_transpose/perm"); + init_circle_const(_front_perm, front_perm); + } + + _front_transpose = g()->nodes()->create(); + { + _front_transpose->a(input()); + _front_transpose->name("front_transpose"); + _front_transpose->perm(_front_perm); + } + + _mid_shape = g()->nodes()->create(); + { + _mid_shape->name("mid_reshpae/shape"); + init_circle_const(_mid_shape, mid_shape); + } + + _mid_reshape = g()->nodes()->create(); + { + _mid_reshape->name("mid_reshape"); + _mid_reshape->tensor(_front_transpose); + _mid_reshape->shape(_mid_shape); + } + + _back_perm = g()->nodes()->create(); + { + _back_perm->name("back_transpose/perm"); + init_circle_const(_back_perm, back_perm); + } + + _back_transpose = g()->nodes()->create(); + { + _back_transpose->name("back_transpose"); + _back_transpose->a(_mid_reshape); + _back_transpose->perm(_back_perm); + } + + output()->from(_back_transpose); + } + + // create input-transpose-transpose-output graph + void init_transpose_only(ShapeU32 in_shape, ShapeU32 front_perm, ShapeU32 back_perm, + ShapeU32 out_shape) + { + TestIOGraph::init(in_shape, out_shape); + + _front_perm = g()->nodes()->create(); + { + _front_perm->name("front_transpose/perm"); + init_circle_const(_front_perm, front_perm); + } + + _front_transpose = g()->nodes()->create(); + { + _front_transpose->a(input()); + _front_transpose->name("front_transpose"); + _front_transpose->perm(_front_perm); + } + + _back_perm = g()->nodes()->create(); + { + _back_perm->name("back_transpose/perm"); + init_circle_const(_back_perm, back_perm); + } + + _back_transpose = g()->nodes()->create(); + { + _back_transpose->name("back_transpose"); + _back_transpose->a(_front_transpose); + _back_transpose->perm(_back_perm); + } + + output()->from(_back_transpose); + } + +private: + void init_circle_const(luci::CircleConst *const_node, ShapeU32 shape) + { + const_node->dtype(loco::DataType::S32); + const_node->size(shape.size()); + uint32_t i = 0; + for (auto v : shape) + { + const_node->at(i++) = v; + } + } + + luci::CircleTranspose *_front_transpose = nullptr; + luci::CircleConst *_front_perm = nullptr; + + luci::CircleReshape *_mid_reshape = nullptr; + luci::CircleConst *_mid_shape = nullptr; + + luci::CircleTranspose *_back_transpose = nullptr; + luci::CircleConst *_back_perm = nullptr; +}; + +} // namespace + +TEST(RemoveUnnecessaryTransposeNetPass, rank_reduction_pattern1) +{ + TransposeReshapeTransposeGraph g; + luci::RemoveUnnecessaryTransposeNetPass pass; + + /** + * (1, 14, 14, 192) + * | + * (1, 192, 14, 14) + * | + * (1, 192, 196) + * | + * (1, 196, 192) + */ + g.init_whole_graph(/*in*/ {1, 14, 14, 192}, /*perm*/ {0, 3, 1, 2}, /*reshape*/ {1, 192, 196}, + /*perm*/ {0, 2, 1}, /*out*/ {1, 196, 192}); + + EXPECT_TRUE(pass.run(g.g())); +} + +TEST(RemoveUnnecessaryTransposeNetPass, rank_reduction_pattern2) +{ + TransposeReshapeTransposeGraph g; + luci::RemoveUnnecessaryTransposeNetPass pass; + + /** + * (1, 100, 10, 12) + * | + * (1, 10, 12, 100) + * | + * (120, 100) + * | + * (100, 120) + */ + g.init_whole_graph(/*in*/ {1, 100, 10, 12}, /*perm*/ {0, 2, 3, 1}, /*reshape*/ {120, 100}, + /*perm*/ {1, 0}, + /*out*/ {100, 120}); + + EXPECT_TRUE(pass.run(g.g())); +} + +TEST(RemoveUnnecessaryTransposeNetPass, identity_pattern) +{ + TransposeReshapeTransposeGraph g; + luci::RemoveUnnecessaryTransposeNetPass pass; + + /** + * (1, 2, 3) + * | + * (1, 2, 3) + * | + * (1, 2, 3) + * | + * (1, 2, 3) + */ + g.init_whole_graph(/*in*/ {1, 2, 3}, /*perm*/ {0, 1, 2}, /*reshape*/ {1, 2, 3}, + /*perm*/ {0, 1, 2}, + /*out*/ {1, 2, 3}); + + EXPECT_TRUE(pass.run(g.g())); +} + +TEST(RemoveUnnecessaryTransposeNetPass, basic_pattern1_NEG) +{ + TransposeReshapeTransposeGraph g; + luci::RemoveUnnecessaryTransposeNetPass pass; + + /** + * (1, 2, 4, 6) + * | + * (1, 2, 6, 4) + * | + * (1, 12, 4) + * | + * (1, 4, 12) + */ + g.init_whole_graph(/*in*/ {1, 2, 4, 6}, /*perm*/ {0, 1, 3, 2}, /*reshape*/ {1, 12, 4}, + /*perm*/ {0, 2, 1}, + /*out*/ {1, 4, 12}); + + EXPECT_FALSE(pass.run(g.g())); +} + +TEST(RemoveUnnecessaryTransposeNetPass, basic_pattern2_NEG) +{ + TransposeReshapeTransposeGraph g; + luci::RemoveUnnecessaryTransposeNetPass pass; + + /** + * (15, 10, 10) + * | + * (10, 10, 15) + * | + * (1, 1, 1500) + * | + * (1500, 1, 1) + */ + g.init_whole_graph(/*in*/ {15, 10, 10}, /*perm*/ {1, 2, 0}, /*reshape*/ {1, 1, 1500}, + /*perm*/ {2, 0, 1}, + /*out*/ {1500, 1, 1}); + + EXPECT_FALSE(pass.run(g.g())); +} + +TEST(RemoveUnnecessaryTransposeNetPass, basic_pattern3_NEG) +{ + TransposeReshapeTransposeGraph g; + luci::RemoveUnnecessaryTransposeNetPass pass; + + /** + * (1, 2, 3, 4) + * | + * perm (0, 3, 1, 2) + * | + * (1, 4, 2, 3) + * | + * perm (0, 2, 3, 1) + * | + * (1, 2, 3, 4) + */ + g.init_transpose_only(/*in*/ {1, 2, 3, 4}, /*perm*/ {0, 3, 1, 2}, /*perm*/ {0, 2, 3, 1}, + /*out*/ {1, 2, 3, 4}); + + EXPECT_FALSE(pass.run(g.g())); +}