1 /* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
2
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
6
7 http://www.apache.org/licenses/LICENSE-2.0
8
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14 ==============================================================================*/
15
16 #include "tensorflow/lite/graph_info.h"
17
18 #include <stddef.h>
19
20 #include <utility>
21 #include <vector>
22
23 #include <gtest/gtest.h>
24 #include "tensorflow/lite/c/common.h"
25 #include "tensorflow/lite/testing/util.h"
26
27 namespace tflite {
28 namespace {
29
30 // Makes a TfLiteIntArray* from std::vector, must free with TfLiteIntFree().
ConvertVector(const std::vector<int> & x)31 TfLiteIntArray* ConvertVector(const std::vector<int>& x) {
32 TfLiteIntArray* lite = TfLiteIntArrayCreate(x.size());
33 for (size_t i = 0; i < x.size(); i++) lite->data[i] = x[i];
34 return lite;
35 }
36
37 // A very simple test graph that supports setting in/out tensors on nodes.
38 class SimpleTestGraph : public GraphInfo {
39 public:
SimpleTestGraph(int node_index_offset=0)40 explicit SimpleTestGraph(int node_index_offset = 0)
41 : node_index_offset_(node_index_offset) {
42 // 'node_index_offset' number of nodes are not present in the execution
43 // plan. (and hence not considered for partitioning)
44 for (int i = 0; i < node_index_offset; ++i) AddNode({}, {});
45 }
46
~SimpleTestGraph()47 ~SimpleTestGraph() override {
48 for (auto& node : nodes_) {
49 TfLiteIntArrayFree(node.inputs);
50 TfLiteIntArrayFree(node.outputs);
51 }
52 }
53
num_total_nodes() const54 size_t num_total_nodes() const override { return nodes_.size(); }
num_execution_nodes() const55 size_t num_execution_nodes() const override {
56 return nodes_.size() - node_index_offset_;
57 }
node(size_t index) const58 const TfLiteNode& node(size_t index) const override {
59 return nodes_[index + node_index_offset_];
60 }
node_index(size_t index) const61 size_t node_index(size_t index) const override {
62 return index + node_index_offset_;
63 }
num_tensors() const64 size_t num_tensors() const override { return tensors_.size(); }
tensor(size_t index)65 TfLiteTensor* tensor(size_t index) override { return &tensors_[index]; }
inputs() const66 const std::vector<int>& inputs() const override { return inputs_; }
outputs() const67 const std::vector<int>& outputs() const override { return outputs_; }
variables() const68 const std::vector<int>& variables() const override { return variables_; }
69
AddNode(const std::vector<int> & inputs,const std::vector<int> & outputs,bool might_have_side_effect=false)70 void AddNode(const std::vector<int>& inputs, const std::vector<int>& outputs,
71 bool might_have_side_effect = false) {
72 nodes_.push_back(TfLiteNode());
73 TfLiteNode& node = nodes_.back();
74 node.inputs = ConvertVector(inputs);
75 node.outputs = ConvertVector(outputs);
76 node.might_have_side_effect = might_have_side_effect;
77 }
78
AddTensors(int count)79 void AddTensors(int count) { tensors_.resize(count + tensors_.size()); }
80
SetInputsAndOutputs(const std::vector<int> & inputs,const std::vector<int> & outputs)81 void SetInputsAndOutputs(const std::vector<int>& inputs,
82 const std::vector<int>& outputs) {
83 inputs_ = inputs;
84 outputs_ = outputs;
85 }
86
87 private:
88 size_t node_index_offset_;
89 std::vector<TfLiteNode> nodes_;
90 std::vector<TfLiteTensor> tensors_;
91 std::vector<int> inputs_;
92 std::vector<int> outputs_;
93 std::vector<int> variables_;
94 };
95
96 // Partition a graph to generate a list of subgraphs. This wraps the API call
97 // we are testing and handles memory management and conversion to
98 // TfLiteIntArray. Populates `subgraphs` with resulting generated subgraphs.
PartitionGraph(const SimpleTestGraph & graph,const std::vector<int> & nodes_to_partition,std::vector<NodeSubset> * subgraphs)99 void PartitionGraph(const SimpleTestGraph& graph,
100 const std::vector<int>& nodes_to_partition,
101 std::vector<NodeSubset>* subgraphs) {
102 TfLiteIntArray* nodes_to_partition_int_array =
103 ConvertVector(nodes_to_partition);
104 ASSERT_EQ(PartitionGraphIntoIndependentNodeSubsets(
105 &graph, nodes_to_partition_int_array, subgraphs),
106 kTfLiteOk);
107 TfLiteIntArrayFree(nodes_to_partition_int_array);
108 }
109
110 // Same as above, but with explicitly specified control dependencies.
PartitionGraph(const SimpleTestGraph & graph,const std::vector<int> & nodes_to_partition,std::vector<NodeSubset> * subgraphs,const std::vector<std::pair<int,int>> & control_deps)111 void PartitionGraph(const SimpleTestGraph& graph,
112 const std::vector<int>& nodes_to_partition,
113 std::vector<NodeSubset>* subgraphs,
114 const std::vector<std::pair<int, int>>& control_deps) {
115 TfLiteIntArray* nodes_to_partition_int_array =
116 ConvertVector(nodes_to_partition);
117 ASSERT_EQ(PartitionGraphIntoIndependentNodeSubsets(
118 &graph, nodes_to_partition_int_array, control_deps, subgraphs),
119 kTfLiteOk);
120 TfLiteIntArrayFree(nodes_to_partition_int_array);
121 }
122
123 // Check a generated list of subgraphs against the expected list of subgraphs.
CheckPartitionSubgraphs(const std::vector<NodeSubset> & generated_subgraphs,const std::vector<NodeSubset> & expected_subgraphs)124 void CheckPartitionSubgraphs(
125 const std::vector<NodeSubset>& generated_subgraphs,
126 const std::vector<NodeSubset>& expected_subgraphs) {
127 ASSERT_EQ(generated_subgraphs.size(), expected_subgraphs.size());
128 for (size_t subgraph_index = 0; subgraph_index < generated_subgraphs.size();
129 subgraph_index++) {
130 EXPECT_EQ(generated_subgraphs[subgraph_index].nodes,
131 expected_subgraphs[subgraph_index].nodes);
132 EXPECT_EQ(generated_subgraphs[subgraph_index].input_tensors,
133 expected_subgraphs[subgraph_index].input_tensors);
134 EXPECT_EQ(generated_subgraphs[subgraph_index].output_tensors,
135 expected_subgraphs[subgraph_index].output_tensors);
136 }
137 }
138
139 // Test an empty trivial graph with no partitions.
TEST(PartitionTest,Nodes0PartitionNodes0)140 TEST(PartitionTest, Nodes0PartitionNodes0) {
141 SimpleTestGraph graph;
142 std::vector<int> nodes_to_partition = {};
143 std::vector<NodeSubset> generated_subgraphs;
144 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
145 CheckPartitionSubgraphs(generated_subgraphs, {});
146 }
147
148 // Test a trivial graph with no node and only 1 tensor.
149 // The tensor is input & output of the graph at the same time.
150 // Note: This is a regression test to ensure the partitioning logic
151 // handles this case without crashing.
TEST(PartitionTest,Nodes0PartitionNodes0Tensors1)152 TEST(PartitionTest, Nodes0PartitionNodes0Tensors1) {
153 SimpleTestGraph graph;
154 graph.AddTensors(1);
155 graph.SetInputsAndOutputs({0}, {0});
156 std::vector<int> nodes_to_partition = {};
157 std::vector<NodeSubset> generated_subgraphs;
158 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
159 CheckPartitionSubgraphs(generated_subgraphs, {});
160 }
161
162 // Test a 1 node graph with no partitions.
163 // Input: tensor(0) -> node(0) -> tensor(1), nodes_to_partition=[]
164 // Output: [kTfNoPartition, tensor(0) -> node(0) -> tensor(1)]
TEST(PartitionTest,Nodes1PartitionNodes0)165 TEST(PartitionTest, Nodes1PartitionNodes0) {
166 SimpleTestGraph graph;
167 graph.AddTensors(2);
168 graph.AddNode({0}, {1});
169 graph.SetInputsAndOutputs({0}, {1});
170 std::vector<int> nodes_to_partition = {};
171 std::vector<NodeSubset> generated_subgraphs;
172 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
173
174 NodeSubset expected_subgraph;
175 expected_subgraph.type = NodeSubset::kTfNonPartition;
176 expected_subgraph.nodes = {0};
177 expected_subgraph.input_tensors = {0};
178 expected_subgraph.output_tensors = {1};
179 CheckPartitionSubgraphs(generated_subgraphs, {expected_subgraph});
180 }
181
TEST(PartitionTest,Nodes1PartitionNodes0_WithOffset)182 TEST(PartitionTest, Nodes1PartitionNodes0_WithOffset) {
183 constexpr int node_index_offset = 17;
184 SimpleTestGraph graph(node_index_offset);
185 graph.AddTensors(2);
186 graph.AddNode({0}, {1});
187 graph.SetInputsAndOutputs({0}, {1});
188 std::vector<int> nodes_to_partition = {};
189 std::vector<NodeSubset> generated_subgraphs;
190 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
191
192 NodeSubset expected_subgraph;
193 expected_subgraph.type = NodeSubset::kTfNonPartition;
194 expected_subgraph.nodes = {node_index_offset};
195 expected_subgraph.input_tensors = {0};
196 expected_subgraph.output_tensors = {1};
197 CheckPartitionSubgraphs(generated_subgraphs, {expected_subgraph});
198 }
199
200 // Test a 1 node graph with no inputs that is fully partitioned.
201 // Input: node(0) -> tensor(1), nodes_to_partition=[node0]
202 // Output: [kTfPartition, node(0) -> tensor(1)]
TEST(PartitionTest,Nodes1PartitionNodes0Inputs0)203 TEST(PartitionTest, Nodes1PartitionNodes0Inputs0) {
204 SimpleTestGraph graph;
205 graph.AddTensors(1);
206 graph.AddNode({}, {0});
207 graph.SetInputsAndOutputs({}, {0});
208 std::vector<NodeSubset> generated_subgraphs;
209 std::vector<int> nodes_to_partition = {0};
210 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
211
212 NodeSubset expected_subgraph;
213 expected_subgraph.type = NodeSubset::kTfPartition;
214 expected_subgraph.nodes = {0};
215 expected_subgraph.input_tensors = {};
216 expected_subgraph.output_tensors = {0};
217 CheckPartitionSubgraphs(generated_subgraphs, {expected_subgraph});
218 }
219
220 // Test a 1 node graph that is partitioned completely.
221 // Input: tensor(0) -> node(0) -> tensor(1), nodes_to_partition=[node0]
222 // Output: [kTfPartition, tensor(0) -> node(0) -> tensor(1)]
TEST(PartitionTest,Nodes1PartitionNodes1)223 TEST(PartitionTest, Nodes1PartitionNodes1) {
224 SimpleTestGraph graph;
225 graph.AddTensors(2);
226 graph.AddNode({0}, {1});
227 graph.SetInputsAndOutputs({0}, {1});
228 std::vector<int> nodes_to_partition = {0};
229 std::vector<NodeSubset> generated_subgraphs;
230 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
231
232 NodeSubset expected_subgraph;
233 expected_subgraph.type = NodeSubset::kTfPartition;
234 expected_subgraph.nodes = {0};
235 expected_subgraph.input_tensors = {0};
236 expected_subgraph.output_tensors = {1};
237 CheckPartitionSubgraphs(generated_subgraphs, {expected_subgraph});
238 }
239
240 // Test a 2 node graph where 1 node is partitioned and the other is not.
241 // Input: tensor(0) -> node(0) -> tensor(1) -> node(1) -> tensor(2),
242 // nodes_to_partition = [1]
243 // Output: [kTfNonPartition, tensor(0) -> node(0) -> tensor(1),
244 // kTfPartition, tensor(1) -> node(1), tensor(2)]
TEST(PartitionTest,Nodes2PartitionNodes1)245 TEST(PartitionTest, Nodes2PartitionNodes1) {
246 SimpleTestGraph graph;
247 graph.AddTensors(3);
248 graph.AddNode({0}, {1});
249 graph.AddNode({1}, {2});
250 graph.SetInputsAndOutputs({0}, {2});
251 std::vector<int> nodes_to_partition = {1};
252 std::vector<NodeSubset> generated_subgraphs;
253 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
254
255 NodeSubset expected_subgraph0;
256 expected_subgraph0.type = NodeSubset::kTfPartition;
257 expected_subgraph0.nodes = {0};
258 expected_subgraph0.input_tensors = {0};
259 expected_subgraph0.output_tensors = {1};
260 NodeSubset expected_subgraph1;
261 expected_subgraph1.type = NodeSubset::kTfPartition;
262 expected_subgraph1.nodes = {1};
263 expected_subgraph1.input_tensors = {1};
264 expected_subgraph1.output_tensors = {2};
265 CheckPartitionSubgraphs(generated_subgraphs,
266 {expected_subgraph0, expected_subgraph1});
267 }
268
269 // Same as above, but with node offset to ensure correct handling of original vs
270 // execution plan indices.
TEST(PartitionTest,Nodes2PartitionNodes1_WithOffset)271 TEST(PartitionTest, Nodes2PartitionNodes1_WithOffset) {
272 constexpr int node_index_offset = 17;
273 SimpleTestGraph graph(node_index_offset);
274 graph.AddTensors(3);
275 graph.AddNode({0}, {1});
276 graph.AddNode({1}, {2});
277 graph.SetInputsAndOutputs({0}, {2});
278 std::vector<int> nodes_to_partition = {node_index_offset + 1};
279 std::vector<NodeSubset> generated_subgraphs;
280 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
281
282 NodeSubset expected_subgraph0;
283 expected_subgraph0.type = NodeSubset::kTfPartition;
284 expected_subgraph0.nodes = {node_index_offset + 0};
285 expected_subgraph0.input_tensors = {0};
286 expected_subgraph0.output_tensors = {1};
287 NodeSubset expected_subgraph1;
288 expected_subgraph1.type = NodeSubset::kTfPartition;
289 expected_subgraph1.nodes = {node_index_offset + 1};
290 expected_subgraph1.input_tensors = {1};
291 expected_subgraph1.output_tensors = {2};
292 CheckPartitionSubgraphs(generated_subgraphs,
293 {expected_subgraph0, expected_subgraph1});
294 }
295
296 // Test a 2 node graph where both nodes are fully partitioned.
297 // Input: tensor(0) -> node(0) -> tensor(1) -> node(1) -> tensor(2),
298 // nodes_to_partition = [0, 1]
299 // Output: [kTfPartition, tensor(0) -> node(0) -> node(1) -> tensor(1)]
TEST(PartitionTest,Nodes2PartitionNodes2)300 TEST(PartitionTest, Nodes2PartitionNodes2) {
301 SimpleTestGraph graph;
302 graph.AddTensors(3);
303 graph.AddNode({0}, {1});
304 graph.AddNode({1}, {2});
305 graph.SetInputsAndOutputs({0}, {2});
306 std::vector<int> nodes_to_partition = {0, 1};
307 std::vector<NodeSubset> generated_subgraphs;
308 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
309
310 NodeSubset expected_subgraph0;
311 expected_subgraph0.type = NodeSubset::kTfPartition;
312 expected_subgraph0.nodes = {0, 1};
313 expected_subgraph0.input_tensors = {0};
314 expected_subgraph0.output_tensors = {2};
315 CheckPartitionSubgraphs(generated_subgraphs, {expected_subgraph0});
316 }
317
318 // Test a three node model where we want to partition node 0 and node
319 // 2, but node 0 and node 2 cannot be in the same subgraph since node 2
320 // depends on node 1 which depends on node 0. Thus, we need to produce three
321 // subgraphs.
322 //
323 // Input: tensor(0) -> node(0) -> tensor(1)
324 // tensor(1) -> node(1) -> tensor(2)
325 // [tensor(2), tensor(1)] -> node(2) -> tensor(3)
326 // nodes_to_partition = [0, 2]
327 // Output: [[kTfPartition, tensor(0) -> node(0) -> tensor(1),
328 // [kTfNonPartition, tensor(1) -> node(1) -> tensor(2)],
329 // [kTfPartition, [tensor(2), tensor(1)] -> node(2) -> node(3)]
TEST(PartitionTest,Nodes3PartitionNodes2)330 TEST(PartitionTest, Nodes3PartitionNodes2) {
331 SimpleTestGraph graph;
332 graph.AddTensors(4);
333 graph.AddNode({0}, {1});
334 graph.AddNode({1}, {2});
335 graph.AddNode({1, 2}, {3});
336 graph.SetInputsAndOutputs({0}, {3});
337 std::vector<int> nodes_to_partition = {0, 2};
338 std::vector<NodeSubset> generated_subgraphs;
339 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
340
341 NodeSubset expected_subgraph0;
342 expected_subgraph0.type = NodeSubset::kTfPartition;
343 expected_subgraph0.nodes = {0};
344 expected_subgraph0.input_tensors = {0};
345 expected_subgraph0.output_tensors = {1};
346 NodeSubset expected_subgraph1;
347 expected_subgraph1.type = NodeSubset::kTfNonPartition;
348 expected_subgraph1.nodes = {1};
349 expected_subgraph1.input_tensors = {1};
350 expected_subgraph1.output_tensors = {2};
351 NodeSubset expected_subgraph2;
352 expected_subgraph2.type = NodeSubset::kTfPartition;
353 expected_subgraph2.nodes = {2};
354 expected_subgraph2.input_tensors = {1, 2};
355 expected_subgraph2.output_tensors = {3};
356 CheckPartitionSubgraphs(
357 generated_subgraphs,
358 {expected_subgraph0, expected_subgraph1, expected_subgraph2});
359 }
360
361 // Test correct partition for graph with control dependency.
362 // Graph for test is like
363 // varhandleOp -> ReadVariableOp -> Add -> AssignVariableOp
364 // |_________________________^ ^^
365 // |------------------------->ReadVariableOp -> (Output)
366 // ^^ is control dependency, in this case we don't want to invoke the
367 // last ReadVariableOp before AssignVariableOp finishes executing.
368 // '>' and '^' represents data dependency.
TEST(PartitionTest,Nodes4PartitionNodes3_WithControlDependency)369 TEST(PartitionTest, Nodes4PartitionNodes3_WithControlDependency) {
370 SimpleTestGraph graph;
371 // Construct graph.
372 {
373 graph.AddTensors(5);
374 graph.AddNode({0}, {1}, true);
375 graph.AddNode({1}, {2}, true);
376 graph.AddNode({2}, {3}, false);
377 graph.AddNode({1, 3}, {}, true);
378 graph.AddNode({1}, {4}, true);
379 }
380 graph.SetInputsAndOutputs({0}, {4});
381 std::vector<int> nodes_to_partition = {0, 1, 3, 4};
382 std::vector<NodeSubset> generated_subgraphs;
383 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs);
384
385 NodeSubset expected_subgraph0;
386 expected_subgraph0.type = NodeSubset::kTfPartition;
387 expected_subgraph0.nodes = {0, 1};
388 expected_subgraph0.input_tensors = {0};
389 expected_subgraph0.output_tensors = {1, 2};
390 NodeSubset expected_subgraph1;
391 expected_subgraph1.type = NodeSubset::kTfNonPartition;
392 expected_subgraph1.nodes = {2};
393 expected_subgraph1.input_tensors = {2};
394 expected_subgraph1.output_tensors = {3};
395 NodeSubset expected_subgraph2;
396 expected_subgraph2.type = NodeSubset::kTfPartition;
397 expected_subgraph2.nodes = {3, 4};
398 expected_subgraph2.input_tensors = {1, 3};
399 expected_subgraph2.output_tensors = {4};
400 CheckPartitionSubgraphs(
401 generated_subgraphs,
402 {expected_subgraph0, expected_subgraph1, expected_subgraph2});
403 }
404
405 // This is the same as Nodes4PartitionNodes3_WithControlDependency,
406 // but the control dependency is given by an external edge set.
TEST(PartitionTest,Nodes4PartitionNodes3_WithExternalControlDependency)407 TEST(PartitionTest, Nodes4PartitionNodes3_WithExternalControlDependency) {
408 SimpleTestGraph graph;
409 // Construct graph.
410 {
411 graph.AddTensors(5);
412 graph.AddNode({0}, {1});
413 graph.AddNode({1}, {2});
414 graph.AddNode({2}, {3});
415 graph.AddNode({1, 3}, {});
416 graph.AddNode({1}, {4});
417 }
418 graph.SetInputsAndOutputs({0}, {4});
419 std::vector<int> nodes_to_partition = {0, 1, 3, 4};
420 std::vector<NodeSubset> generated_subgraphs;
421
422 // Nodes {0,1,3,4} are stateful.
423 PartitionGraph(graph, nodes_to_partition, &generated_subgraphs,
424 {{0, 1}, {1, 3}, {3, 4}});
425 NodeSubset expected_subgraph0;
426 expected_subgraph0.type = NodeSubset::kTfPartition;
427 expected_subgraph0.nodes = {0, 1};
428 expected_subgraph0.input_tensors = {0};
429 expected_subgraph0.output_tensors = {1, 2};
430 NodeSubset expected_subgraph1;
431 expected_subgraph1.type = NodeSubset::kTfNonPartition;
432 expected_subgraph1.nodes = {2};
433 expected_subgraph1.input_tensors = {2};
434 expected_subgraph1.output_tensors = {3};
435 NodeSubset expected_subgraph2;
436 expected_subgraph2.type = NodeSubset::kTfPartition;
437 expected_subgraph2.nodes = {3, 4};
438 expected_subgraph2.input_tensors = {1, 3};
439 expected_subgraph2.output_tensors = {4};
440 CheckPartitionSubgraphs(
441 generated_subgraphs,
442 {expected_subgraph0, expected_subgraph1, expected_subgraph2});
443 }
444
445 } // namespace
446 } // namespace tflite
447