Kezhan/change edge api to use index (#138)

* remove input edges while removing a node.

* put comments on how to remove a node without using resolve.

* fix test failure

* fix test failure.

* fix format

* try to guess and fix mac ci failure.

* change add remove edge api.

* update IR

* update comments

* fix comments.
This commit is contained in:
Ke Zhang 2018-12-12 14:30:55 -08:00 committed by GitHub
parent 2e79597531
commit 9baaf5e956
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 162 additions and 91 deletions

View file

@ -54,9 +54,10 @@ class Node {
Construct an EdgeEnd
@param node The source node if this is an input edge to the current node,
or the destination node if this is an output edge from the current node.
@param node_arg The NodeArg to use for the edge.
@param src_arg_index The node arg index of source node of the edge.
@param dst_arg_index The node arg index of destination node of the edge.
*/
EdgeEnd(const Node& node, const NodeArg& node_arg) noexcept;
EdgeEnd(const Node& node, int src_arg_index, int dst_arg_index) noexcept;
/** Construct a control edge.
@param node The node the edge joins to the current node.
@ -66,13 +67,18 @@ class Node {
/** Gets the Node that this EdgeEnd refers to. */
const Node& GetNode() const noexcept;
/** Gets the NodeArg that this edge end refers to.
@returns NodeArg pointer or nullptr if this is a control edge. */
const NodeArg* GetNodeArg() const noexcept;
/** Gets the source arg index.
@returns the source arg index of <*this> edge.*/
int GetSrcArgIndex() const;
/** Gets the destination arg index.
@returns the destination arg index of <*this> edge.*/
int GetDstArgIndex() const;
private:
const Node* node_;
const NodeArg* node_arg_;
int src_arg_index_;
int dst_arg_index_;
};
/** Gets the Node's NodeIndex. */
@ -146,7 +152,7 @@ class Node {
/** Gets the implicit inputs to this Node.
If this Node contains a subgraph, these are the NodeArg's that are implicitly consumed by Nodes within that
subgraph. e.g. If and Loop operators.*/
const std::vector<const NodeArg*>& ImplicitInputDefs() const noexcept {
const std::vector<NodeArg*>& ImplicitInputDefs() const noexcept {
return definitions_.implicit_input_defs;
}
@ -160,11 +166,10 @@ class Node {
struct EdgeEndCompare {
bool operator()(const EdgeEnd& lhs, const EdgeEnd& rhs) const {
if (lhs.GetNode().Index() == rhs.GetNode().Index()) {
auto lhs_arg = lhs.GetNodeArg();
auto rhs_arg = rhs.GetNodeArg();
std::string lhs_arg_name = lhs_arg == nullptr ? "" : lhs_arg->Name();
std::string rhs_arg_name = rhs_arg == nullptr ? "" : rhs_arg->Name();
return lhs_arg_name.compare(rhs_arg_name) < 0;
if (lhs.GetSrcArgIndex() == rhs.GetSrcArgIndex()) {
return lhs.GetDstArgIndex() < rhs.GetDstArgIndex();
}
return lhs.GetSrcArgIndex() < rhs.GetSrcArgIndex();
}
return lhs.GetNode().Index() < rhs.GetNode().Index();
}
@ -305,7 +310,7 @@ class Node {
@remarks For example, a subgraph in an 'If' node gets all its input values via this mechanism rather than
there being explicit inputs to the 'If' node that are passed to the subgraph.
They are pseudo-inputs to this Node as it has an implicit dependency on them. */
std::vector<const NodeArg*> implicit_input_defs;
std::vector<NodeArg*> implicit_input_defs;
private:
ONNXRUNTIME_DISALLOW_COPY_ASSIGNMENT_AND_MOVE(Definitions);
@ -391,7 +396,7 @@ class Node {
// OperatorSchema that <*this> node refers to.
const ONNX_NAMESPACE::OpSchema* op_ = nullptr;
Node::Type node_type_ = Node::Type::Primitive;
// The function body is owned by graph_
const Function* func_body_ = nullptr;
@ -588,6 +593,12 @@ class Graph {
Node& AddNode(const Node& other);
/** Remove a Node from this Graph and free it.
The output edges of this specified node MUST have been removed before removing the node.
The input edges of this specified node is removed while removing the node. The process of
removing a node from a graph should be,
1. Remove out edges of this specified node.
2. Remove this specified node.
3. Add new input edges connected with all out nodes.
@returns true if the node_index was valid
@remarks Do not call AddNode and Remove Node concurrently as they are not thread-safe.
*/
@ -596,16 +607,18 @@ class Graph {
/** Add an edge between two Nodes.
@param src_node_index NodeIndex of source Node that is providing output to the destination Node.
@param dst_node_index NodeIndex of destination Node that is receiving input from the source Node.
@param node_arg NodeArg to use for the edge.
@param src_arg_index node arg index of source node.
@param dst_arg_index node arg index of destination node.
*/
void AddEdge(NodeIndex src_node_index, NodeIndex dst_node_index, const NodeArg& node_arg);
void AddEdge(NodeIndex src_node_index, NodeIndex dst_node_index, int src_arg_index, int dst_arg_index);
/** Remove an edge between two Nodes.
@param src_node_index NodeIndex of source Node to remove an output edge from.
@param dst_node_index NodeIndex of destination Node to remove an input edge from.
@param node_arg NodeArg that is used by the edge that is being removed.
@param src_arg_index node arg index of source node.
@param dst_arg_index node arg index of destination node.
*/
void RemoveEdge(NodeIndex src_node_index, NodeIndex dst_node_index, const NodeArg& node_arg);
void RemoveEdge(NodeIndex src_node_index, NodeIndex dst_node_index, int src_arg_index, int dst_arg_index);
/**
Add a control edge between two Nodes in this Graph.
@ -779,7 +792,7 @@ class Graph {
struct ResolveContext {
ResolveContext() = default;
std::unordered_map<std::string, Node*> output_args;
std::unordered_map<std::string, std::pair<Node*, int>> output_args;
std::unordered_set<std::string> inputs_and_initializers;
std::unordered_set<std::string> outer_scope_node_args;
std::unordered_map<std::string, NodeIndex> node_name_to_index;
@ -798,7 +811,7 @@ class Graph {
};
// search this and up through any parent_graph_ instance for a NodeArg
const NodeArg* GetNodeArgIncludingParentGraphs(const std::string& node_arg_name) const;
NodeArg* GetNodeArgIncludingParentGraphs(const std::string& node_arg_name);
// Initialize all the graph inputs, initializers and outputs
common::Status InitInputsInitializersOutputs();

View file

@ -17,7 +17,7 @@ class OpKernelContextInternal : public OpKernelContext {
explicit OpKernelContextInternal(ExecutionFrame& frame,
const OpKernel& kernel,
const logging::Logger& logger,
const std::vector<const NodeArg*>& implicit_inputs,
const std::vector<NodeArg*>& implicit_inputs,
const bool& terminate_flag)
: OpKernelContext(&frame, &kernel, logger),
implicit_inputs_{implicit_inputs},
@ -51,7 +51,7 @@ class OpKernelContextInternal : public OpKernelContext {
const bool& GetTerminateFlag() const noexcept { return terminate_flag_; }
private:
const std::vector<const NodeArg*>& implicit_inputs_;
const std::vector<NodeArg*>& implicit_inputs_;
const bool& terminate_flag_;
};

View file

@ -70,7 +70,7 @@ SessionStateInitializer::SessionStateInitializer(onnxruntime::Graph& graph,
common::Status SessionStateInitializer::CreatePlan(const onnxruntime::GraphTransformerManager& graph_transformation_manager,
const InsertCastTransformer& insert_cast_transformer,
const std::vector<const NodeArg*>& outer_scope_node_args,
const std::vector<NodeArg*>& outer_scope_node_args,
bool enable_sequential_execution) {
ONNXRUNTIME_RETURN_IF_ERROR(TransformGraph(graph_, graph_transformation_manager,
execution_providers_, kernel_registry_manager_,

View file

@ -31,7 +31,7 @@ class SessionStateInitializer {
// First perform any transformations and create the execution plan
common::Status CreatePlan(const onnxruntime::GraphTransformerManager& graph_transformation_manager,
const InsertCastTransformer& insert_cast_transformer,
const std::vector<const NodeArg*>& outer_scope_node_args,
const std::vector<NodeArg*>& outer_scope_node_args,
bool enable_sequential_execution);
// initialize tensors, and save. save kernels and input/output node mappings

View file

@ -170,14 +170,13 @@ Status ConvBNFusion::Apply(onnxruntime::Graph& graph, bool& modified) const {
}
}
}
removed_nodes.push_back(bn_node.Index());
}
for (auto i : removed_nodes) {
graph.RemoveNode(i);
}
if (!removed_nodes.empty()) {
modified = true;
ONNXRUNTIME_RETURN_IF_ERROR(graph.Resolve());

View file

@ -126,7 +126,7 @@ Status ConvMulFusion::Apply(onnxruntime::Graph& graph, bool& modified) const {
}
}
}
removed_nodes.push_back(mul_node.Index());
}

View file

@ -223,20 +223,26 @@ bool NodeArg::Exists() const noexcept {
return exists_;
}
Node::EdgeEnd::EdgeEnd(const Node& node, const NodeArg& node_arg) noexcept
: node_(&node), node_arg_(&node_arg) {
Node::EdgeEnd::EdgeEnd(const Node& node, int src_arg_index, int dst_arg_index) noexcept
: node_(&node),
src_arg_index_(src_arg_index),
dst_arg_index_(dst_arg_index) {
}
Node::EdgeEnd::EdgeEnd(const Node& node) noexcept
: node_(&node), node_arg_(nullptr) {
: EdgeEnd(node, INT_MAX, INT_MAX) {
}
const Node& Node::EdgeEnd::GetNode() const noexcept {
return *node_;
}
const NodeArg* Node::EdgeEnd::GetNodeArg() const noexcept {
return node_arg_;
int Node::EdgeEnd::GetSrcArgIndex() const {
return src_arg_index_;
}
int Node::EdgeEnd::GetDstArgIndex() const {
return dst_arg_index_;
}
Node::NodeConstIterator::NodeConstIterator(EdgeConstIterator p_iter) {
@ -692,9 +698,9 @@ Graph::Graph(Graph& parent_graph, ONNX_NAMESPACE::GraphProto& subgraph_proto)
}
Status Graph::VerifyNoDuplicateName() {
const std::unordered_set<std::string>& inputs_and_initializers = resolve_context_.inputs_and_initializers;
std::unordered_map<std::string, Node*>& output_args = resolve_context_.output_args;
std::unordered_map<std::string, NodeIndex>& node_name_to_index = resolve_context_.node_name_to_index;
auto& inputs_and_initializers = resolve_context_.inputs_and_initializers;
auto& output_args = resolve_context_.output_args;
auto& node_name_to_index = resolve_context_.node_name_to_index;
output_args.clear();
node_name_to_index.clear();
@ -715,7 +721,9 @@ Status Graph::VerifyNoDuplicateName() {
node_name_to_index[node_name] = node.Index();
// Verify node outputs' name should be unique.
int output_index = -1;
for (const auto* output_def : node.OutputDefs()) {
++output_index;
if (output_def->Exists()) {
auto& output_arg_name = output_def->Name();
if (inputs_and_initializers.count(output_arg_name)) {
@ -723,7 +731,7 @@ Status Graph::VerifyNoDuplicateName() {
"Error: Duplicate definition of name (" + output_arg_name + ").");
return status;
}
auto result = output_args.insert({output_arg_name, &node});
auto result = output_args.insert({output_arg_name, {&node, output_index}});
if (!result.second) {
// Two outputs with same name, so that insertion fails.
Status status(ONNXRUNTIME, FAIL,
@ -760,7 +768,7 @@ common::Status Graph::SetOuterScopeNodeArgs(const std::unordered_set<std::string
std::transform(resolve_context_.output_args.cbegin(), resolve_context_.output_args.cend(),
std::inserter(node_args_in_scope_for_subgraph, node_args_in_scope_for_subgraph.end()),
[](const std::pair<std::string, Node*>& entry) { return entry.first; });
[](const std::pair<std::string, std::pair<Node*, int>>& entry) { return entry.first; });
for (auto* node : resolve_context_.nodes_with_subgraphs) {
for (auto& subgraph : node->MutableSubgraphs()) {
@ -773,8 +781,8 @@ common::Status Graph::SetOuterScopeNodeArgs(const std::unordered_set<std::string
return Status::OK();
}
const NodeArg* Graph::GetNodeArgIncludingParentGraphs(const std::string& node_arg_name) const {
const NodeArg* node_arg = GetNodeArg(node_arg_name);
NodeArg* Graph::GetNodeArgIncludingParentGraphs(const std::string& node_arg_name) {
NodeArg* node_arg = GetNodeArg(node_arg_name);
if (!node_arg && parent_graph_) {
node_arg = parent_graph_->GetNodeArgIncludingParentGraphs(node_arg_name);
@ -783,7 +791,7 @@ const NodeArg* Graph::GetNodeArgIncludingParentGraphs(const std::string& node_ar
return node_arg;
}
void Graph::AddEdge(NodeIndex src_node_index, NodeIndex dst_node_index, const NodeArg& node_arg) {
void Graph::AddEdge(NodeIndex src_node_index, NodeIndex dst_node_index, int src_arg_slot, int dst_arg_slot) {
if (nodes_.size() <= src_node_index ||
nodes_.size() <= dst_node_index ||
nullptr == nodes_[src_node_index] ||
@ -791,34 +799,47 @@ void Graph::AddEdge(NodeIndex src_node_index, NodeIndex dst_node_index, const No
// Invalid node indexes specified.
ONNXRUNTIME_THROW("Invalid node indexes specified when adding edge.");
}
// Verify whether the node_arg is input of dst and output of src firstly.
bool valid = false;
for (auto arg : nodes_[src_node_index]->OutputDefs()) {
if (arg == &node_arg) {
valid = true;
break;
NodeArg *src_arg = nullptr, *dst_arg = nullptr;
if (nodes_[src_node_index]->MutableDefinitions().output_defs.size() > src_arg_slot) {
src_arg = nodes_[src_node_index]->MutableDefinitions().output_defs[src_arg_slot];
}
if (nullptr == src_arg) {
ONNXRUNTIME_THROW("Invalid source node arg slot specified when adding edge.");
}
auto& dst_node_defs = nodes_[dst_node_index]->MutableDefinitions();
NodeArg** dst_arg_pointer = nullptr;
if (dst_node_defs.input_defs.size() > dst_arg_slot) {
dst_arg_pointer = &dst_node_defs.input_defs[dst_arg_slot];
dst_arg = *dst_arg_pointer;
} else {
auto num_of_explicit_inputs = dst_node_defs.input_defs.size();
if (num_of_explicit_inputs + dst_node_defs.implicit_input_defs.size() > dst_arg_slot) {
dst_arg_pointer = &dst_node_defs.implicit_input_defs[dst_arg_slot - num_of_explicit_inputs];
dst_arg = *dst_arg_pointer;
}
}
ONNXRUNTIME_ENFORCE(valid);
valid = false;
for (auto arg : nodes_[dst_node_index]->InputDefs()) {
if (arg == &node_arg) {
valid = true;
break;
if (nullptr == dst_arg) {
ONNXRUNTIME_THROW("Invalid destination node arg slot specified when adding edge.");
}
if (src_arg != dst_arg) {
if (src_arg->Type() != dst_arg->Type()) {
// The output type of source node arg does not match the input type of destination node arg.
ONNXRUNTIME_THROW("Argument type mismatch when adding edge.");
} else {
src_arg->UpdateTypeAndShape(*dst_arg);
*dst_arg_pointer = src_arg;
}
}
for (auto arg : nodes_[dst_node_index]->ImplicitInputDefs()) {
if (arg == &node_arg) {
valid = true;
break;
}
}
ONNXRUNTIME_ENFORCE(valid);
nodes_[dst_node_index]->MutableRelationships().input_edges.insert(Node::EdgeEnd(*nodes_[src_node_index], node_arg));
nodes_[src_node_index]->MutableRelationships().output_edges.insert(Node::EdgeEnd(*nodes_[dst_node_index], node_arg));
nodes_[src_node_index]->MutableRelationships().output_edges.insert(Node::EdgeEnd(*nodes_[dst_node_index], src_arg_slot, dst_arg_slot));
nodes_[dst_node_index]->MutableRelationships().input_edges.insert(Node::EdgeEnd(*nodes_[src_node_index], src_arg_slot, dst_arg_slot));
}
void Graph::RemoveEdge(NodeIndex src_node_index, NodeIndex dst_node_index, const NodeArg& node_arg) {
void Graph::RemoveEdge(NodeIndex src_node_index, NodeIndex dst_node_index, int src_arg_slot, int dst_arg_slot) {
if (nodes_.size() <= src_node_index ||
nodes_.size() <= dst_node_index ||
nullptr == nodes_[src_node_index] ||
@ -826,31 +847,36 @@ void Graph::RemoveEdge(NodeIndex src_node_index, NodeIndex dst_node_index, const
// Invalid node indexes specified.
ONNXRUNTIME_THROW("Invalid node indexes specified when removing edge.");
}
// Verify whether the node_arg is input of dst and output of src firstly.
bool valid = false;
for (auto arg : nodes_[src_node_index]->OutputDefs()) {
if (arg == &node_arg) {
valid = true;
break;
const NodeArg *src_arg = nullptr, *dst_arg = nullptr;
if (nodes_[src_node_index]->GetDefinitions().output_defs.size() > src_arg_slot) {
src_arg = nodes_[src_node_index]->GetDefinitions().output_defs[src_arg_slot];
}
if (nullptr == src_arg) {
ONNXRUNTIME_THROW("Invalid source node arg slot specified when removing edge.");
}
auto& dst_node_defs = nodes_[dst_node_index]->GetDefinitions();
if (dst_node_defs.input_defs.size() > dst_arg_slot) {
dst_arg = dst_node_defs.input_defs[dst_arg_slot];
} else {
auto num_of_explicit_inputs = dst_node_defs.input_defs.size();
if (num_of_explicit_inputs + dst_node_defs.implicit_input_defs.size() > dst_arg_slot) {
dst_arg = dst_node_defs.implicit_input_defs[dst_arg_slot - num_of_explicit_inputs];
}
}
ONNXRUNTIME_ENFORCE(valid);
valid = false;
for (auto arg : nodes_[dst_node_index]->InputDefs()) {
if (arg == &node_arg) {
valid = true;
break;
}
if (nullptr == dst_arg) {
ONNXRUNTIME_THROW("Invalid destination node arg slot specified when removing edge.");
}
for (auto arg : nodes_[dst_node_index]->ImplicitInputDefs()) {
if (arg == &node_arg) {
valid = true;
break;
}
if (src_arg != dst_arg) {
// The edge ends specified by source and destination arg slot are not referring to same node arg.
// It means there was no edge between these two slots before.
ONNXRUNTIME_THROW("Argument type mismatch when removing edge.");
}
ONNXRUNTIME_ENFORCE(valid);
nodes_[dst_node_index]->MutableRelationships().input_edges.erase(Node::EdgeEnd(*nodes_[src_node_index], node_arg));
nodes_[src_node_index]->MutableRelationships().output_edges.erase(Node::EdgeEnd(*nodes_[dst_node_index], node_arg));
nodes_[dst_node_index]->MutableRelationships().input_edges.erase(Node::EdgeEnd(*nodes_[src_node_index], src_arg_slot, dst_arg_slot));
nodes_[src_node_index]->MutableRelationships().output_edges.erase(Node::EdgeEnd(*nodes_[dst_node_index], src_arg_slot, dst_arg_slot));
}
GSL_SUPPRESS(es .84) // ignoring return value from unordered_map::insert causes noisy complaint
@ -867,7 +893,7 @@ Status Graph::BuildConnections(std::vector<std::string>& outer_scope_node_args_c
for (auto& node_arg_name : node_args_consumed) {
bool node_arg_in_parent_graph = false;
const auto* node_arg = GetNodeArg(node_arg_name);
auto node_arg = GetNodeArg(node_arg_name);
if (node_arg == nullptr) {
// it's a node arg from outside this graph's scope, so add that to the list we return
@ -897,8 +923,13 @@ Status Graph::BuildConnections(std::vector<std::string>& outer_scope_node_args_c
// add it to the Node's list of implicit inputs
auto& implicit_inputs = node->MutableDefinitions().implicit_input_defs;
if (std::find(implicit_inputs.cbegin(), implicit_inputs.cend(), node_arg) == implicit_inputs.cend()) {
int input_slot_index = static_cast<int>(node->GetDefinitions().input_defs.size());
auto iter = std::find(implicit_inputs.cbegin(), implicit_inputs.cend(), node_arg);
if (implicit_inputs.cend() == iter) {
implicit_inputs.push_back(node_arg);
input_slot_index += static_cast<int>(implicit_inputs.size() - 1);
} else {
input_slot_index += static_cast<int>(iter - implicit_inputs.cbegin());
}
if (node_arg_in_parent_graph ||
@ -914,8 +945,8 @@ Status Graph::BuildConnections(std::vector<std::string>& outer_scope_node_args_c
ONNXRUNTIME_ENFORCE(entry != resolve_context_.output_args.end());
// Create relationship between this node (node), and the node providing the output (output_node).
Node& output_node = *entry->second;
AddEdge(output_node.Index(), node->Index(), *node_arg);
Node& output_node = *entry->second.first;
AddEdge(output_node.Index(), node->Index(), entry->second.second, input_slot_index);
inner_nodes.insert(&output_node);
}
@ -932,7 +963,9 @@ Status Graph::BuildConnections(std::vector<std::string>& outer_scope_node_args_c
if (input_args.size() > 0) {
// This node needs inputs.
int input_slot_index = -1;
for (const auto* input_arg : input_args) {
++input_slot_index;
if (!input_arg->Exists()) {
// This input could be optional and it does not exist in this case.
continue;
@ -953,8 +986,8 @@ Status Graph::BuildConnections(std::vector<std::string>& outer_scope_node_args_c
}
// Create relationship between this node (node), and the node providing the output (output_node).
Node& output_node = *output_arg_iter->second;
AddEdge(output_node.Index(), node.Index(), *input_arg);
Node& output_node = *output_arg_iter->second.first;
AddEdge(output_node.Index(), node.Index(), output_arg_iter->second.second, input_slot_index);
inner_nodes.insert(&output_node);
}
@ -2023,6 +2056,19 @@ Node& Graph::AddNode(const std::string& name,
}
bool Graph::RemoveNode(NodeIndex p_index) {
auto node = GetNode(p_index);
if (nullptr == node /*|| 0 != node->GetRelationships().output_edges.size()*/) {
// Node should be removed after all out edges are removed.
// TODO: add the check commented out back.
return false;
}
// Remove all input edges.
auto input_edges = node->GetRelationships().input_edges;
for (auto& input_edge : input_edges) {
RemoveEdge(input_edge.GetNode().Index(), p_index, input_edge.GetSrcArgIndex(), input_edge.GetDstArgIndex());
}
return ReleaseNode(p_index);
}
@ -2437,6 +2483,14 @@ Node& Graph::FuseSubGraph(std::unique_ptr<::onnxruntime::IndexedSubGraph> sub_gr
// Remove nodes fused above.
auto& sub_graph_ref = function_container_.back()->GetIndexedSubGraph();
for (auto node_index : sub_graph_ref.nodes) {
auto node = GetNode(node_index);
if (nullptr == node) {
continue;
}
auto output_edges = node->GetRelationships().output_edges;
for (auto output_edge : output_edges) {
RemoveEdge(node->Index(), output_edge.GetNode().Index(), output_edge.GetSrcArgIndex(), output_edge.GetDstArgIndex());
}
RemoveNode(node_index);
}
return fused_node;
@ -2446,6 +2500,10 @@ Status Graph::InlineFunction(Node& node) {
// Remove the function node, add the nodes in function's subgraph into the
// main graph.
const Graph& subgraph = node.GetFunctionBody()->Body();
auto output_edges = node.GetRelationships().output_edges;
for (auto output_edge : output_edges) {
RemoveEdge(node.Index(), output_edge.GetNode().Index(), output_edge.GetSrcArgIndex(), output_edge.GetDstArgIndex());
}
RemoveNode(node.Index());
for (const auto& subgraph_node : subgraph.Nodes()) {
AddNode(subgraph_node);

View file

@ -82,6 +82,7 @@ Status UnsqueezeElimination::Apply(onnxruntime::Graph& graph, bool& modified) co
}
}
}
removed_nodes.push_back(node.Index());
}

View file

@ -165,8 +165,8 @@ TEST(GraphTraversalTest, ReverseDFS) {
EXPECT_TRUE(status.IsOK()) << status.ErrorMessage();
// Remove/Add edge should not ask for resolving again.
graph.RemoveEdge(node_1.Index(), node_3.Index(), *graph.GetNodeArg("node_1_out_1"));
graph.AddEdge(node_1.Index(), node_3.Index(), *graph.GetNodeArg("node_1_out_1"));
graph.RemoveEdge(node_1.Index(), node_3.Index(), 0, 0);
graph.AddEdge(node_1.Index(), node_3.Index(), 0, 0);
std::vector<const Node*> from;
for (auto& node : graph.Nodes()) {