SequenceFusion transformation (#12845)

* SequenceFusion transformation and tests

* Enable SequenceFusion transformation in MOC

* add missed includes

* fix type

* fix build, apply review comments

* fix build

* fix build

* fix build again

* use ov namespace in has_result_consumers function

* fix win build

* try to fix win build

* investigate issue on win platform

* investigate issue on win platform

* investigate issue on win

* issue on win platform

* remove the transformation from MOC

* fix LSTMCell fusion, simplify transformation implementation, fix copying tensor and friendly names

* clean up

* add support for LSTMCell v0, resolve review comments, enable additional tests
This commit is contained in:
Ivan Tikhonov authored and GitHub committed 2022-10-03 10:36:54 +04:00
1 parent 0c54905587
commit 4f002c46b9
5 files changed
+621

No files matched your search

@@ -0,0 +1,34 @@
// Copyright (C) 2018-2022 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#pragma once
#include <memory>
#include <openvino/pass/graph_rewrite.hpp>
#include <transformations_visibility.hpp>
#include <vector>
namespace ov {
namespace pass {
class TRANSFORMATIONS_API SequenceFusion;
} // namespace pass
} // namespace ov
/**
* @ingroup ie_transformation_common_api
* @brief SequenceFusion transformation replaces a chain of Cells
* operations with single Sequence op.
*
* Supported cells: GRUCell, LSTMCell, RNNCell, AUGRUCell
* Prerequisites: the source of W,R,B inputs must be the same or
* it can be different Constants with the same type, shape and value.
*/
class ov::pass::SequenceFusion : public ov::pass::MatcherPass {
public:
OPENVINO_RTTI("SequenceFusion", "0");
SequenceFusion();
};
@@ -0,0 +1,355 @@
// Copyright (C) 2018-2022 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include "transformations/common_optimizations/sequence_fusion.hpp"
#include <memory>
#include "itt.hpp"
#include "ngraph_ops/augru_cell.hpp"
#include "ngraph_ops/augru_sequence.hpp"
#include "openvino/core/rt_info.hpp"
#include "openvino/opsets/opset3.hpp"
#include "openvino/opsets/opset9.hpp"
#include "openvino/pass/pattern/op/wrap_type.hpp"
#include "transformations/utils/utils.hpp"
using namespace std;
using namespace ov::element;
using namespace ov::opset9;
using namespace ov::op::util;
namespace {
bool is_equal_consts(const shared_ptr<ov::Node>& l, const shared_ptr<ov::Node>& r) {
auto l_const = dynamic_pointer_cast<Constant>(l);
auto r_const = dynamic_pointer_cast<Constant>(r);
if (l_const && r_const) {
auto l_ptr = l_const->get_data_ptr();
auto r_ptr = r_const->get_data_ptr();
size_t bytes = shape_size(l_const->get_shape()) * l_const->get_element_type().size();
return l_const->get_element_type() == r_const->get_element_type() &&
l_const->get_shape() == r_const->get_shape() && (l_ptr == r_ptr || memcmp(l_ptr, r_ptr, bytes) == 0);
}
return false;
}
bool check_WRB(const shared_ptr<RNNCellBase>& cell_1, const shared_ptr<RNNCellBase>& cell_2) {
int64_t idx_W = 2, idx_R = 3, idx_B = 4;
auto increase_indexes = [&]() {
++idx_B;
++idx_R;
++idx_W;
};
auto lstm_cell_v4_1 = dynamic_pointer_cast<LSTMCell>(cell_1);
auto lstm_cell_v4_2 = dynamic_pointer_cast<LSTMCell>(cell_2);
// 2nd input is Cell State
if (lstm_cell_v4_1 && lstm_cell_v4_2) {
increase_indexes();
}
auto lstm_cell_v0_1 = dynamic_pointer_cast<ov::opset3::LSTMCell>(cell_1);
auto lstm_cell_v0_2 = dynamic_pointer_cast<ov::opset3::LSTMCell>(cell_2);
if (lstm_cell_v0_1 && lstm_cell_v0_2) {
if (lstm_cell_v0_1->get_weights_format() != lstm_cell_v0_2->get_weights_format() ||
lstm_cell_v0_1->get_input_forget() != lstm_cell_v0_2->get_input_forget()) {
return false;
}
increase_indexes();
}
auto lW = cell_1->input_value(idx_W).get_node_shared_ptr();
auto lR = cell_1->input_value(idx_R).get_node_shared_ptr();
auto lB = cell_1->input_value(idx_B).get_node_shared_ptr();
auto rW = cell_2->input_value(idx_W).get_node_shared_ptr();
auto rR = cell_2->input_value(idx_R).get_node_shared_ptr();
auto rB = cell_2->input_value(idx_B).get_node_shared_ptr();
bool is_equal = (lW.get() == rW.get() || is_equal_consts(lW, rW));
is_equal = is_equal && (lR.get() == rR.get() || is_equal_consts(lR, rR));
is_equal = is_equal && (lB.get() == rB.get() || is_equal_consts(lB, rB));
return is_equal;
}
bool is_equal_cells(const shared_ptr<RNNCellBase>& cell_1, const shared_ptr<RNNCellBase>& cell_2) {
bool is_equal = true;
auto gru_cell_1 = dynamic_pointer_cast<GRUCell>(cell_1);
auto gru_cell_2 = dynamic_pointer_cast<GRUCell>(cell_2);
if (gru_cell_1 && gru_cell_2) {
is_equal = gru_cell_1->get_linear_before_reset() == gru_cell_2->get_linear_before_reset();
}
is_equal = is_equal && cell_1->get_type_name() == cell_2->get_type_name() &&
cell_1->get_hidden_size() == cell_2->get_hidden_size() &&
cell_1->get_activations() == cell_2->get_activations() &&
cell_1->get_activations_alpha() == cell_2->get_activations_alpha() &&
cell_1->get_activations_beta() == cell_2->get_activations_beta() &&
cell_1->get_clip() == cell_2->get_clip() && check_WRB(cell_1, cell_2);
return is_equal;
}
bool check_lstm_cell(const shared_ptr<RNNCellBase>& prev_cell, const shared_ptr<RNNCellBase>& current_cell) {
// check intermediate C outputs in case of LSTMCell
// LSTMCell - C -> LSTMCell
if ((dynamic_pointer_cast<LSTMCell>(prev_cell) || dynamic_pointer_cast<ov::opset3::LSTMCell>(prev_cell))) {
const auto& target_inputs = prev_cell->get_output_target_inputs(1);
bool valid = target_inputs.empty() ||
(target_inputs.size() == 1 &&
dynamic_cast<RNNCellBase*>(target_inputs.begin()->get_node()) == current_cell.get() &&
target_inputs.begin()->get_index() == 2);
// if intermediate C output is connected to other node, except LSTMCell,
// we can't replace cells with sequence. Sequence doesn't provide access to these outputs.
return valid;
}
return true;
}
shared_ptr<RNNCellBase> find_cell_chain(ov::pass::NodeRegistry& cp_from,
ov::pass::NodeRegistry& cp_to,
const shared_ptr<RNNCellBase>& current_cell,
ov::OutputVector& x_to_concat,
ov::OutputVector& attention_to_concat,
map<int, ov::Output<ov::Node>>& h_outputs_to_redirect,
int& cells_cnt,
const shared_ptr<ov::Node>& axis_1) {
cells_cnt = 1;
shared_ptr<RNNCellBase> current = current_cell;
while (true) {
cp_from.add(current);
// check the source node of HiddenState input
auto prev = current->input_value(1).get_node_shared_ptr();
auto prev_cell = dynamic_pointer_cast<RNNCellBase>(prev);
auto in_X = current->input(0);
x_to_concat.push_back(cp_to.make<Unsqueeze>(in_X.get_source_output(), axis_1));
h_outputs_to_redirect[cells_cnt] = current->output(0);
if (auto augru = dynamic_pointer_cast<ov::op::internal::AUGRUCell>(current)) {
attention_to_concat.push_back(cp_to.make<Unsqueeze>(augru->input_value(5), axis_1));
}
if (prev_cell && is_equal_cells(prev_cell, current) && check_lstm_cell(prev_cell, current)) {
current = prev_cell;
cells_cnt++;
} else {
break;
}
}
reverse(x_to_concat.begin(), x_to_concat.end());
reverse(attention_to_concat.begin(), attention_to_concat.end());
// the first cell in the chain
return current;
}
bool create_sequence(ov::pass::NodeRegistry& cp_to,
const shared_ptr<RNNCellBase>& first_cell,
const shared_ptr<RNNCellBase>& last_cell,
const ov::OutputVector& x_to_concat,
const ov::OutputVector& attention_to_concat,
const map<int, ov::Output<ov::Node>>& h_outputs_to_redirect,
int cells_cnt,
const shared_ptr<ov::Node>& axis_0,
const shared_ptr<ov::Node>& axis_1) {
int64_t idx_W = 2, idx_R = 3, idx_B = 4;
// 2nd input is Cell State
bool is_lstm = false;
if (dynamic_pointer_cast<LSTMCell>(last_cell) || dynamic_pointer_cast<ov::opset3::LSTMCell>(last_cell)) {
is_lstm = true;
idx_B++;
idx_R++;
idx_W++;
}
const auto X_in = cp_to.make<Concat>(x_to_concat, 1);
const auto Ht_in = cp_to.make<Unsqueeze>(first_cell->input_value(1), axis_1);
const auto W_in = cp_to.make<Unsqueeze>(first_cell->input_value(idx_W), axis_0);
const auto R_in = cp_to.make<Unsqueeze>(first_cell->input_value(idx_R), axis_0);
const auto B_in = cp_to.make<Unsqueeze>(first_cell->input_value(idx_B), axis_0);
const auto& shape_node = cp_to.add(ngraph::op::util::make_try_fold<ShapeOf>(first_cell->input_value(0)));
const auto& zero = cp_to.make<Constant>(i64, ov::Shape{1}, 0);
const auto& batch_dimension = cp_to.add(ngraph::op::util::make_try_fold<Gather>(shape_node, zero, axis_0));
auto seq_lengths_scalar = cp_to.make<Constant>(i64, ov::Shape{}, cells_cnt);
auto sequence_lengths_in =
cp_to.add(ngraph::op::util::make_try_fold<Broadcast>(seq_lengths_scalar, batch_dimension));
shared_ptr<ov::Node> sequence;
ov::OutputVector outputs(1);
if (dynamic_pointer_cast<LSTMCell>(first_cell)) {
const auto Ct_in = cp_to.make<Unsqueeze>(first_cell->input_value(2), axis_1);
sequence = cp_to.make<LSTMSequence>(X_in,
Ht_in,
Ct_in,
sequence_lengths_in,
W_in,
R_in,
B_in,
first_cell->get_hidden_size(),
ov::op::RecurrentSequenceDirection::FORWARD,
first_cell->get_activations_alpha(),
first_cell->get_activations_beta(),
first_cell->get_activations(),
first_cell->get_clip());
outputs.resize(2);
outputs[1] = cp_to.make<Squeeze>(sequence->output(2), axis_1);
} else if (auto lstm_cell_v0 = dynamic_pointer_cast<ov::opset3::LSTMCell>(first_cell)) {
// input_forget modification is not supported
if (lstm_cell_v0->get_input_forget()) {
return false;
}
auto weights_format = lstm_cell_v0->get_weights_format();
ov::Output<ov::Node> W = W_in, R = R_in, B = B_in;
if (weights_format != ov::op::LSTMWeightsFormat::FICO) {
W = ov::op::util::convert_lstm_node_format(W_in, convert_lstm_weights_enums(weights_format));
R = ov::op::util::convert_lstm_node_format(R_in, convert_lstm_weights_enums(weights_format));
B = ov::op::util::convert_lstm_node_format(B_in, convert_lstm_weights_enums(weights_format));
}
const auto Ct_in = cp_to.make<Unsqueeze>(first_cell->input_value(2), axis_1);
sequence = cp_to.make<LSTMSequence>(X_in,
Ht_in,
Ct_in,
sequence_lengths_in,
W,
R,
B,
first_cell->get_hidden_size(),
ov::op::RecurrentSequenceDirection::FORWARD,
first_cell->get_activations_alpha(),
first_cell->get_activations_beta(),
first_cell->get_activations(),
first_cell->get_clip());
outputs.resize(2);
outputs[1] = cp_to.make<Squeeze>(sequence->output(2), axis_1);
} else if (auto gru_cell = dynamic_pointer_cast<GRUCell>(first_cell)) {
sequence = cp_to.make<GRUSequence>(X_in,
Ht_in,
sequence_lengths_in,
W_in,
R_in,
B_in,
first_cell->get_hidden_size(),
ov::op::RecurrentSequenceDirection::FORWARD,
first_cell->get_activations(),
first_cell->get_activations_alpha(),
first_cell->get_activations_beta(),
first_cell->get_clip(),
gru_cell->get_linear_before_reset());
} else if (dynamic_pointer_cast<RNNCell>(first_cell)) {
sequence = cp_to.make<RNNSequence>(X_in,
Ht_in,
sequence_lengths_in,
W_in,
R_in,
B_in,
first_cell->get_hidden_size(),
ov::op::RecurrentSequenceDirection::FORWARD,
first_cell->get_activations(),
first_cell->get_activations_alpha(),
first_cell->get_activations_beta(),
first_cell->get_clip());
} else if (dynamic_pointer_cast<ov::op::internal::AUGRUCell>(first_cell)) {
const auto A_in = cp_to.make<Concat>(attention_to_concat, 1);
sequence = cp_to.make<ov::op::internal::AUGRUSequence>(X_in,
Ht_in,
sequence_lengths_in,
W_in,
R_in,
B_in,
A_in,
first_cell->get_hidden_size());
} else {
// cell is not supported;
return false;
}
if (!h_outputs_to_redirect.empty()) {
auto squeeze_Y = cp_to.make<Squeeze>(sequence->output(0), axis_1);
auto split = cp_to.make<Split>(squeeze_Y, axis_1, cells_cnt);
for (auto it : h_outputs_to_redirect) {
auto Hi = split->output(cells_cnt - it.first);
auto friendly_name = it.second.get_node_shared_ptr()->get_friendly_name();
if (it.first == 1) {
Hi = sequence->output(1);
}
auto squeeze = cp_to.make<Squeeze>(Hi, axis_1);
it.second.replace(squeeze);
if (is_lstm) {
friendly_name += ":1";
}
squeeze->set_friendly_name(friendly_name);
}
}
if (is_lstm) {
auto squeeze = cp_to.make<Squeeze>(sequence->output(2), axis_1);
last_cell->output(1).replace(squeeze);
squeeze->set_friendly_name(last_cell->get_friendly_name() + ":2");
}
return true;
}
} // namespace
ov::pass::SequenceFusion::SequenceFusion() {
MATCHER_SCOPE(SequenceFusion);
auto cell = pattern::wrap_type<RNNCellBase>();
matcher_pass_callback callback = [=](pattern::Matcher& m) {
NodeRegistry copy_from;
NodeRegistry copy_to;
auto cell = m.get_match_root();
shared_ptr<RNNCellBase> current_cell = dynamic_pointer_cast<RNNCellBase>(cell);
if (!current_cell) {
return false;
}
// check that this is the last Cell in the chain, e.g.
// GRUCell -> GRUCell (the last cell) -> OtherNode
// GRUCell (hidden_size = 128) -> GRUCell (hs = 128, the last) -> GRUCell (hs = 64)
for (const auto& target : cell->get_output_target_inputs(0)) {
auto cell_1 = dynamic_pointer_cast<RNNCellBase>(target.get_node()->shared_from_this());
if (cell_1 && is_equal_cells(cell_1, current_cell)) {
return false;
}
}
int cells_cnt;
ov::OutputVector x_to_concat;
ov::OutputVector attention_to_concat;
map<int, ov::Output<ov::Node>> h_outputs_to_redirect;
auto axis_0 = copy_to.make<Constant>(i64, Shape{}, 0);
auto axis_1 = copy_to.make<Constant>(i64, Shape{}, 1);
// detect chain (Cell->Cell->Cell->..)
auto first_cell = find_cell_chain(copy_from,
copy_to,
current_cell,
x_to_concat,
attention_to_concat,
h_outputs_to_redirect,
cells_cnt,
axis_1);
if (!first_cell) {
return false;
}
// no reasons to create sequence if the single cell detected.
// TODO: investigate optimal cnt of cells
constexpr int optimal_cnt_of_cells = 2;
if (cells_cnt < optimal_cnt_of_cells) {
return false;
}
auto res = create_sequence(copy_to,
first_cell,
current_cell,
x_to_concat,
attention_to_concat,
h_outputs_to_redirect,
cells_cnt,
axis_0,
axis_1);
if (!res) {
return false;
}
copy_runtime_info(copy_from.get(), copy_to.get());
return true;
};
auto m = make_shared<pattern::Matcher>(cell, matcher_name);
this->register_matcher(m, callback);
}