-
Notifications
You must be signed in to change notification settings - Fork 4.1k
allow tensor alias for Loop's carried params #497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,7 +97,8 @@ std::ostream& operator<<(std::ostream& out, std::pair<const SequentialExecutionP | |
|
|
||
| class PlannerImpl { | ||
| public: | ||
| PlannerImpl(const onnxruntime::GraphViewer& graph_viewer, | ||
| PlannerImpl(const Node* parent_node, | ||
| const onnxruntime::GraphViewer& graph_viewer, | ||
| const std::vector<const NodeArg*>& outer_scope_node_args, | ||
| const ExecutionProviders& providers, | ||
| const KernelRegistryManager& kernel_registry, | ||
|
|
@@ -106,6 +107,7 @@ class PlannerImpl { | |
| SequentialExecutionPlan& plan) | ||
| : context_{context}, | ||
| plan_{plan}, | ||
| parent_node_{parent_node}, | ||
| graph_viewer_{graph_viewer}, | ||
| outer_scope_node_args_{outer_scope_node_args}, | ||
| execution_providers_{providers}, | ||
|
|
@@ -119,6 +121,7 @@ class PlannerImpl { | |
| const ISequentialPlannerContext& context_; | ||
| SequentialExecutionPlan& plan_; | ||
|
|
||
| const Node* parent_node_; | ||
| const onnxruntime::GraphViewer& graph_viewer_; | ||
| const std::vector<const NodeArg*>& outer_scope_node_args_; | ||
| const ExecutionProviders& execution_providers_; | ||
|
|
@@ -193,8 +196,7 @@ class PlannerImpl { | |
| } | ||
|
|
||
| // Find if there exists some input tensor that we can use in-place for output_arg | ||
| bool FindReusableInput(const onnxruntime::Node& node, int output_arg_num, MLValueIndex* reusable_input) { | ||
| auto p_output_arg = node.OutputDefs()[output_arg_num]; | ||
| bool FindAliasInput(const onnxruntime::Node& node, int output_arg_num, MLValueIndex* alias_input) { | ||
| const KernelCreateInfo* ci; | ||
| Status st = kernel_registry_.SearchKernelRegistry(node, &ci); | ||
| if (!st.IsOK() || ci == nullptr || ci->kernel_def == nullptr) { | ||
|
|
@@ -209,14 +211,26 @@ class PlannerImpl { | |
| if ((0 <= pair.first) && (static_cast<size_t>(pair.first) < input_args.size())) { | ||
| auto p_input_arg = input_args[pair.first]; | ||
| if (p_input_arg->Exists()) { | ||
| *reusable_input = Index(p_input_arg->Name()); | ||
| *alias_input = Index(p_input_arg->Name()); | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| bool FindInplaceInput(const onnxruntime::Node& node, int output_arg_num, MLValueIndex* inplace_input) { | ||
| auto p_output_arg = node.OutputDefs()[output_arg_num]; | ||
| const KernelCreateInfo* ci; | ||
| Status st = kernel_registry_.SearchKernelRegistry(node, &ci); | ||
| if (!st.IsOK() || ci == nullptr || ci->kernel_def == nullptr) { | ||
| return false; | ||
| } | ||
|
|
||
| const std::vector<std::pair<int, int>>& inplace_map = ci->kernel_def->MayInplace(); | ||
| auto& input_args = node.InputDefs(); | ||
| for (auto pair : inplace_map) { | ||
| if (pair.second == output_arg_num) { | ||
| if ((0 <= pair.first) && (static_cast<size_t>(pair.first) < input_args.size())) { | ||
|
|
@@ -227,14 +241,45 @@ class PlannerImpl { | |
| if (1 == UseCount(original)) { | ||
| if (SameSize(*p_input_arg, *p_output_arg)) { | ||
| // we can reuse this input since it is its last use and permitted for in-place update | ||
| *reusable_input = input_arg_index; // or original; both should be okay | ||
| *inplace_input = input_arg_index; // or original; both should be okay | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| bool FindReusableGraphInput(const onnxruntime::Node& node, int output_arg_num, MLValueIndex* reusable_input) { | ||
| // skip if this is main graph | ||
| if (parent_node_) { | ||
| // only check a single layer. ie. an Identity/Dropout node connecting graph's input and output | ||
| auto& graph_inputs = graph_viewer_.GetInputs(); | ||
| auto& graph_outputs = graph_viewer_.GetOutputs(); | ||
|
|
||
| if (FindAliasInput(node, output_arg_num, reusable_input)) { | ||
| auto& arg = ml_value_info_.at(Buffer(*reusable_input)).p_def_site; | ||
| auto it = std::find(graph_inputs.cbegin(), graph_inputs.cend(), arg); | ||
| if (it != graph_inputs.end()) { | ||
| auto graph_input_index = std::distance(graph_inputs.cbegin(), it); | ||
| ORT_ENFORCE(graph_input_index >= 0 && static_cast<size_t>(graph_input_index) < graph_inputs.size()); | ||
| auto graph_output_index = std::distance(graph_outputs.cbegin(), | ||
| std::find(graph_outputs.cbegin(), graph_outputs.cend(), node.OutputDefs()[output_arg_num])); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This should probably be outside the iteration so OutputDefs() and operator[] aren't called every time. Will probably get optimized out, but no need to rely on that. |
||
| ORT_ENFORCE(graph_output_index >= 0 && static_cast<size_t>(graph_output_index) < graph_outputs.size()); | ||
|
|
||
| if (parent_node_->OpType() == "Loop") { | ||
| // the matching carried parameters | ||
| return graph_input_index >= 2; | ||
| } else { | ||
| // TODO: other operators like Scan | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better if this information is provided in a more generic manner so that we're not hardcoding special casing for a list of operators here. e.g. something like the kernel def could be expanded and we lookup that information based on OpType. Otherwise changes to operator specs (e.g. say Loop in opset 10 changes the order of things) are easily missed and the code becomes fragile.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am fully agree with the point. However I think we need a design agreement before I make a change since this change may apply to the core type |
||
| } | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
|
|
@@ -497,13 +542,15 @@ class PlannerImpl { | |
| auto current = Index(node_output->Name()); | ||
| AllocPlan(current).value_type = utils::GetMLDataType(*node_output); | ||
| MLValueIndex reused; | ||
| if (std::find(graph_outputs.begin(), graph_outputs.end(), node_output) != graph_outputs.end()) { | ||
| if (std::find(graph_outputs.begin(), graph_outputs.end(), node_output) != graph_outputs.end() && | ||
| !FindReusableGraphInput(*pnode, output_arg_num, &reused)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this fallthrough logic, because the lines below will invoke "FindReusableInput" again. (In this case, correctness depends on FindReusableGraph & FindReusableInput "finding" the same "reused".) I think it will be better to use a nested "if FindReusableGraphInput(…)" and do the right thing inside this if-statement than cascading to subsequent else-branches.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, How about this comment? Can we change this to: if (this is a graph output) { if (FindReusableGraphInput(…)) Reuse(reused, current) else ...kAllocateOutput" ? |
||
| // node_output is graph's output, so we can't reuse intermedia buffer | ||
| AllocPlan(current).alloc_kind = AllocKind::kAllocateOutput; | ||
| } else if (IsNonTensor(*node_output)) { | ||
| // we do not try sharing-optimization for non-tensors | ||
| AllocPlan(current).alloc_kind = AllocKind::kAllocate; | ||
| } else if (FindReusableInput(*pnode, output_arg_num, &reused)) { | ||
| } else if (FindAliasInput(*pnode, output_arg_num, &reused) || | ||
| FindInplaceInput(*pnode, output_arg_num, &reused)) { | ||
| // Reuse one of this node's input buffers as the output buffer (for in-place update) | ||
| Reuse(reused, current); | ||
| } else if (!context_.EnableParallelExecution() && FindReusableTensor(*node_output, &reused)) { | ||
|
|
@@ -610,7 +657,8 @@ Status PlannerImpl::CreatePlan() { | |
| return Status::OK(); | ||
| } | ||
|
|
||
| Status SequentialPlanner::CreatePlan(const onnxruntime::GraphViewer& graph_viewer, | ||
| Status SequentialPlanner::CreatePlan(const Node* parent_node, | ||
| const onnxruntime::GraphViewer& graph_viewer, | ||
| const std::vector<const NodeArg*>& outer_scope_node_args, | ||
| const ExecutionProviders& providers, | ||
| const KernelRegistryManager& kernel_registry, | ||
|
|
@@ -620,7 +668,7 @@ Status SequentialPlanner::CreatePlan(const onnxruntime::GraphViewer& graph_viewe | |
| // allocate/reset here so we know it's clean | ||
| plan = std::make_unique<SequentialExecutionPlan>(); | ||
|
|
||
| PlannerImpl planner(graph_viewer, outer_scope_node_args, | ||
| PlannerImpl planner(parent_node, graph_viewer, outer_scope_node_args, | ||
| providers, kernel_registry, mlvalue_name_idx_map, context, *plan); | ||
|
|
||
| return planner.CreatePlan(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify this comment? Not quite sure what you mean by 'only check a single layer'. FindReusableInput looks at the alias and 'may inplace' maps and I'm not quite translating that into limiting the check to a 'single layer'.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic only checks one node instead of triversal the whole graph.
Triversal the whole graph will make this logic very complicated; for most of the cases, it's one
Identitynode connected the graph's input and outputThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A would expect the comment to be it just checks this graph rather than 'layer'. A graph could contain multiple layers of say RNNs, which I thought was a completely different concept.
Also I don't quite buy that 'most of the cases' will be one Identity node. If there's some state being carried between iterations, I would expect it is changed across each iteration and therefore there would be other nodes involved. I can understand unit test or very simple example models using an Identity node, but I wouldn't expect that in a real model.
In reply to: 258747489 [](ancestors = 258747489)