2 * Copyright (c) 2019 Samsung Electronics Co., Ltd. All Rights Reserved
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #include <compiler/HEScheduler.h>
18 #include <exec/ExecTime.h>
21 #include <ir/InternalType.h>
22 #include <ir/TypeInfo.h>
23 #include <ir/DataType.h>
25 #include <ir/operation/BinaryArithmetic.h>
26 #include <ir/operation/FullyConnected.h>
28 #include <gtest/gtest.h>
32 using namespace onert;
34 using namespace backend;
35 using namespace operation;
39 // Mock backends classes
42 struct MockConfigCPU : public IConfig
44 std::string id() override { return "cpu"; }
45 bool initialize() override { return true; };
46 bool supportPermutation() override { return false; }
47 Layout supportLayout(const Operation &, Layout) override { return Layout::UNKNOWN; }
48 bool supportDynamicTensor() override { return false; }
49 bool supportFP16() override { return false; }
52 struct MockBackendCPU : public Backend
54 std::shared_ptr<IConfig> config() const override { return std::make_shared<MockConfigCPU>(); }
55 std::unique_ptr<BackendContext>
56 newContext(const Graph &, const std::shared_ptr<custom::IKernelBuilder> &, bool) const override
58 return std::unique_ptr<BackendContext>(
59 new BackendContext{this, nullptr, nullptr, nullptr, nullptr});
63 struct MockConfigGPU : public IConfig
65 std::string id() override { return "gpu"; }
66 bool initialize() override { return true; };
67 bool supportPermutation() override { return false; }
68 ir::Layout supportLayout(const ir::Operation &, ir::Layout) override
70 return ir::Layout::UNKNOWN;
72 bool supportDynamicTensor() override { return false; }
73 bool supportFP16() override { return false; }
76 struct MockBackendGPU : public Backend
78 std::shared_ptr<IConfig> config() const override { return std::make_shared<MockConfigGPU>(); }
79 std::unique_ptr<BackendContext>
80 newContext(const Graph &, const std::shared_ptr<custom::IKernelBuilder> &, bool) const override
82 return std::unique_ptr<BackendContext>(
83 new BackendContext{this, nullptr, nullptr, nullptr, nullptr});
87 struct MockConfigNPU : public IConfig
89 std::string id() override { return "npu"; }
90 bool initialize() override { return true; };
91 bool supportPermutation() override { return false; }
92 ir::Layout supportLayout(const ir::Operation &, ir::Layout) override
94 return ir::Layout::UNKNOWN;
96 bool supportDynamicTensor() override { return false; }
97 bool supportFP16() override { return false; }
100 struct MockBackendNPU : public Backend
102 std::shared_ptr<IConfig> config() const override { return std::make_shared<MockConfigNPU>(); }
103 std::unique_ptr<BackendContext>
104 newContext(const Graph &, const std::shared_ptr<custom::IKernelBuilder> &, bool) const override
106 return std::unique_ptr<BackendContext>(
107 new BackendContext{this, nullptr, nullptr, nullptr, nullptr});
115 const int OPERAND_ELEMS = 268203;
116 const int OPERAND_SIZE = OPERAND_ELEMS * 4;
117 const int OPERATION_SIZE = OPERAND_SIZE * 3;
119 const std::string LINEAR("Linear");
120 const std::string DATAFLOW("Dataflow");
121 const std::string PARALLEL("Parallel");
127 // Set executor through environment variable
128 void setExecutor(const std::string &executor) { setenv("EXECUTOR", executor.c_str(), true); }
130 // Set profiling mode through environment variable
131 void setProfilingMode(const bool value) { setenv("PROFILING_MODE", value ? "1" : "0", true); }
133 // Calculate operation size by addition sizes of all input and output operands
134 uint32_t calcOpSize(const std::shared_ptr<Graph> &graph, const OperationIndex &op_idx)
137 const auto &op = graph->operations().at(op_idx);
138 for (const auto &ind : op.getInputs() + op.getOutputs())
139 size += graph->operands().at(ind).info().total_size();
143 // Set execution operation time. This method is needed since ExecutionTime has only
144 // 'updateOperationExecTime' method.
145 void setOperationExecTime(ExecTime &et, const Backend *backend, const std::string &operation,
146 bool quant, uint32_t op_size, int64_t time)
148 // You shouldn't set negative time with this method since nnfw JSON deserializer can't read it
150 int64_t prev_time = et.getOperationExecTime(backend, operation, quant, op_size);
151 int64_t time_to_set = prev_time == ExecTime::NOT_FOUND ? time : 2 * time - prev_time;
152 et.updateOperationExecTime(backend, operation, quant, op_size, time_to_set);
153 assert(et.getOperationExecTime(backend, operation, quant, op_size) == time);
156 // Set same execution time for all given backends/operations
157 void setOperationsExecutionTime(const std::vector<const Backend *> &backends,
158 const std::vector<std::string> &op_names,
159 const std::vector<uint32_t> &op_sizes, int64_t exec_time)
161 assert(op_names.size() == op_sizes.size());
162 ExecTime et(backends);
163 for (int i = 0; i < op_names.size(); ++i)
165 for (auto &backend : backends)
166 setOperationExecTime(et, backend, op_names[i], false, op_sizes[i], exec_time);
168 et.uploadOperationsExecTime();
171 // Set permute time from one backend to another. This method is needed since ExecutionTime has only
172 // 'updatePermuteTime' method.
173 void setPermutationTime(ExecTime &et, const Backend *from_backend, const Backend *to_backend,
174 bool quant, uint32_t op_size, int64_t time)
176 // You shouldn't set negative time with this method since nnfw JSON deserializer can't read it
178 int64_t prev_time = et.getPermuteTime(from_backend, to_backend, quant, op_size);
179 int64_t time_to_set = prev_time == ExecTime::NOT_FOUND ? time : 2 * time - prev_time;
180 et.updatePermuteTime(from_backend, to_backend, quant, op_size, time_to_set);
181 assert(et.getPermuteTime(from_backend, to_backend, quant, op_size) == time);
184 // Set same permutation time between all given backends
185 void setPermutationsExecutionTime(const std::vector<const Backend *> &backends,
186 const int operand_size, const int64_t exec_time)
188 ExecTime et(backends);
189 for (const auto &backend : backends)
191 for (auto &other_backend : backends)
193 if (backend == other_backend)
195 setPermutationTime(et, backend, other_backend, false, operand_size, exec_time);
198 et.uploadOperationsExecTime();
202 // Functions for creating graphs
205 using OIS = OperandIndexSequence;
207 template <typename NodeT, typename... Types>
208 OperationIndex create(std::shared_ptr<Graph> graph, Types &&... args)
210 auto op = std::make_unique<NodeT>(std::forward<Types>(args)...);
211 auto op_idx = graph->addOperation(std::move(op));
212 // For now in scheduler test all operations in tested graphs has same size (for simplicity)
213 assert(calcOpSize(graph, op_idx) == OPERATION_SIZE);
217 // Create straight graph: Add->Sub->Mul
218 std::shared_ptr<Graph> createStraightGraph()
220 auto graph = std::make_shared<Graph>();
221 const TypeInfo float_op(DataType::FLOAT32);
224 auto add_lhs_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
225 auto add_rhs_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
226 auto add_out_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
227 BinaryArithmetic::Param add_op_params{BinaryArithmetic::ArithmeticType::ADD, Activation::NONE};
228 create<BinaryArithmetic>(graph, OIS{add_lhs_idx, add_rhs_idx}, OIS{add_out_idx}, add_op_params);
231 auto sub_const_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
232 auto sub_out_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
233 BinaryArithmetic::Param sub_op_params{BinaryArithmetic::ArithmeticType::SUB, Activation::NONE};
234 create<BinaryArithmetic>(graph, OIS{add_out_idx, sub_const_idx}, OIS{sub_out_idx}, sub_op_params);
237 auto mul_const_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
238 auto mul_out_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
239 BinaryArithmetic::Param mul_op_params{BinaryArithmetic::ArithmeticType::MUL, Activation::NONE};
240 create<BinaryArithmetic>(graph, OIS{sub_out_idx, mul_const_idx}, OIS{mul_out_idx}, mul_op_params);
242 graph->finishBuilding();
246 /* Create branched graph:
255 std::shared_ptr<Graph> createBranchedGraph()
257 auto graph = std::make_shared<Graph>();
258 const TypeInfo float_op(DataType::FLOAT32);
261 auto add_lhs_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
262 auto add_rhs_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
263 auto add_out_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
264 BinaryArithmetic::Param add_op_params{BinaryArithmetic::ArithmeticType::ADD, Activation::NONE};
265 create<BinaryArithmetic>(graph, OIS{add_lhs_idx, add_rhs_idx}, OIS{add_out_idx}, add_op_params);
268 auto mul1_const_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
269 auto mul1_out_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
270 BinaryArithmetic::Param mul1_op_params{BinaryArithmetic::ArithmeticType::MUL, Activation::NONE};
271 create<BinaryArithmetic>(graph, OIS{add_out_idx, mul1_const_idx}, OIS{mul1_out_idx},
275 auto mul2_const_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
276 auto mul2_out_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
277 BinaryArithmetic::Param mul2_op_params{BinaryArithmetic::ArithmeticType::MUL, Activation::NONE};
278 create<BinaryArithmetic>(graph, OIS{mul1_out_idx, mul2_const_idx}, OIS{mul2_out_idx},
282 auto fc1_const_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
283 auto fc1_out_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
284 FullyConnected::Param fc1_op_params{Activation::NONE};
285 create<FullyConnected>(graph, OIS{add_out_idx, fc1_const_idx}, OIS{fc1_out_idx}, fc1_op_params);
288 auto fc2_const_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
289 auto fc2_out_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
290 FullyConnected::Param fc2_op_params{Activation::NONE};
291 create<FullyConnected>(graph, OIS{fc1_out_idx, fc2_const_idx}, OIS{fc2_out_idx}, fc2_op_params);
294 auto sub_out_idx = graph->addOperand(ir::Shape{OPERAND_ELEMS}, float_op);
295 BinaryArithmetic::Param sub_op_params{BinaryArithmetic::ArithmeticType::SUB, Activation::NONE};
296 create<BinaryArithmetic>(graph, OIS{mul2_out_idx, fc2_out_idx}, OIS{sub_out_idx}, sub_op_params);
298 graph->finishBuilding();
303 // Tests setup/teardown
306 // SetUp/TearDown methods runs before/after each test and performs actions common for each test
307 class SchedulerTest : public ::testing::Test
310 void SetUp() override
312 // Initialize mock backends
313 _cpu_backend = new MockBackendCPU();
314 _gpu_backend = new MockBackendGPU();
315 _npu_backend = new MockBackendNPU();
316 _mock_backends = {_cpu_backend, _gpu_backend, _npu_backend};
318 // Remove previous profile data if it exists
319 if (!remove("exec_time.json"))
321 // DO NOTHING (no profile data)
324 // Remember original value of 'EXECUTOR' environment variable
325 char *executor = std::getenv("EXECUTOR");
326 _original_executor = executor == nullptr ? "" : executor;
328 // Remember original value of 'PROFILING_MODE' environment variable
329 char *profiling_mode = std::getenv("PROFILING_MODE");
330 _original_profiling_mode = profiling_mode == nullptr ? "" : profiling_mode;
333 void TearDown() override
338 EXPECT_EQ(remove("exec_time.json"), 0);
339 setenv("EXECUTOR", _original_executor.c_str(), true);
340 setenv("PROFILING_MODE", _original_profiling_mode.c_str(), true);
343 backend::BackendContexts buildBackendContexts(const Graph &graph)
345 backend::BackendContexts contexts;
346 for (auto backend : _mock_backends)
348 contexts.emplace(backend, backend->newContext(graph, nullptr, false));
353 const MockBackendCPU *_cpu_backend{nullptr};
354 const MockBackendGPU *_gpu_backend{nullptr};
355 const MockBackendNPU *_npu_backend{nullptr};
356 std::vector<const Backend *> _mock_backends;
358 std::string _original_executor;
359 std::string _original_profiling_mode;
362 class SchedulerTestWithExecutorParam : public SchedulerTest,
363 public testing::WithParamInterface<std::string>
371 // Test scheduler behavior for straight graph with known execution time of all nodes and permutes.
372 TEST_P(SchedulerTestWithExecutorParam, straight_graph_known_exec_time)
374 setExecutor(GetParam());
378 auto graph(createStraightGraph());
379 subgs.push(ir::SubgraphIndex{0}, graph);
380 OperationIndex add_op_idx(0), sub_op_idx(1), mul_op_idx(2);
382 // Set default execution and transfer time
383 setPermutationsExecutionTime(_mock_backends, OPERAND_SIZE, 1);
384 setOperationsExecutionTime(_mock_backends, {"Add", "Sub", "Mul"},
385 {OPERATION_SIZE, OPERATION_SIZE, OPERATION_SIZE}, 1e4);
388 // Expected behaviour: scheduler assigns different backend to each node
390 // For each backend reduce execution time of one node
391 ExecTime et(_mock_backends);
392 setOperationExecTime(et, _cpu_backend, "Add", false, OPERATION_SIZE, 1);
393 setOperationExecTime(et, _gpu_backend, "Sub", false, OPERATION_SIZE, 1);
394 setOperationExecTime(et, _npu_backend, "Mul", false, OPERATION_SIZE, 1);
395 et.uploadOperationsExecTime();
398 auto backend_contexts = buildBackendContexts(*graph);
399 auto scheduler = compiler::HEScheduler(backend_contexts,
400 compiler::fetchCompilerOptionsFromGlobalConfig(subgs));
401 const auto br = scheduler.schedule(*graph);
402 ASSERT_EQ(br->getBackend(add_op_idx)->config()->id(), "cpu");
403 ASSERT_EQ(br->getBackend(sub_op_idx)->config()->id(), "gpu");
404 ASSERT_EQ(br->getBackend(mul_op_idx)->config()->id(), "npu");
408 // Expected behaviour: scheduler assigns single backend to all nodes because of big transfer time
410 // Increase transfer time
411 setPermutationsExecutionTime(_mock_backends, OPERAND_SIZE, 1e5);
414 auto backend_contexts = buildBackendContexts(*graph);
415 auto scheduler = compiler::HEScheduler(backend_contexts,
416 compiler::fetchCompilerOptionsFromGlobalConfig(subgs));
417 const auto br = scheduler.schedule(*graph);
418 ASSERT_EQ(br->getBackend(add_op_idx)->config()->id(), "cpu");
419 ASSERT_EQ(br->getBackend(sub_op_idx)->config()->id(), "cpu");
420 ASSERT_EQ(br->getBackend(mul_op_idx)->config()->id(), "cpu");
424 // Test scheduler behavior for branched graph with known execution time of all nodes and permutes
425 TEST_P(SchedulerTestWithExecutorParam, branched_graph_known_exec_time)
427 const int64_t NPU_ET = 5000;
428 setExecutor(GetParam());
432 auto graph(createBranchedGraph());
433 subgs.push(ir::SubgraphIndex{0}, graph);
434 OperationIndex add_op_idx(0), mul1_op_idx(1), mul2_op_idx(2), fc1_op_idx(3), fc2_op_idx(4),
437 // Set default execution and transfer time
438 setPermutationsExecutionTime(_mock_backends, OPERAND_SIZE, 1000);
439 setOperationsExecutionTime(_mock_backends, {"Add", "Sub", "Mul", "FullyConnected"},
440 {OPERATION_SIZE, OPERATION_SIZE, OPERATION_SIZE, OPERATION_SIZE}, 1e4);
443 // Expected behaviour: for dataflow and linear executors scheduler assigns fastest backend to all
444 // nodes, in case of parallel executor scheduler assigns different backends to branches.
446 // Reduce execution time
447 ExecTime et(_mock_backends);
448 setOperationExecTime(et, _npu_backend, "Add", false, OPERATION_SIZE, NPU_ET);
449 setOperationExecTime(et, _npu_backend, "Mul", false, OPERATION_SIZE, NPU_ET);
450 setOperationExecTime(et, _npu_backend, "Sub", false, OPERATION_SIZE, NPU_ET);
451 setOperationExecTime(et, _npu_backend, "FullyConnected", false, OPERATION_SIZE, NPU_ET);
452 setOperationExecTime(et, _gpu_backend, "Mul", false, OPERATION_SIZE, NPU_ET + 1000);
453 setOperationExecTime(et, _gpu_backend, "FullyConnected", false, OPERATION_SIZE, NPU_ET + 1000);
454 et.uploadOperationsExecTime();
457 auto backend_contexts = buildBackendContexts(*graph);
458 auto scheduler = compiler::HEScheduler(backend_contexts,
459 compiler::fetchCompilerOptionsFromGlobalConfig(subgs));
460 const auto br = scheduler.schedule(*graph);
462 std::string branch1_expected_backend("npu"), branch2_expected_backend("npu");
463 if (GetParam() == PARALLEL)
465 branch1_expected_backend =
466 br->getBackend(mul1_op_idx)->config()->id() == "npu" ? "npu" : "gpu";
467 branch2_expected_backend = branch1_expected_backend == "npu" ? "gpu" : "npu";
470 ASSERT_EQ(br->getBackend(add_op_idx)->config()->id(), "npu");
471 ASSERT_EQ(br->getBackend(mul1_op_idx)->config()->id(), branch1_expected_backend);
472 ASSERT_EQ(br->getBackend(mul2_op_idx)->config()->id(), branch1_expected_backend);
473 ASSERT_EQ(br->getBackend(fc1_op_idx)->config()->id(), branch2_expected_backend);
474 ASSERT_EQ(br->getBackend(fc2_op_idx)->config()->id(), branch2_expected_backend);
475 ASSERT_EQ(br->getBackend(sub_op_idx)->config()->id(), "npu");
479 // Expected behaviour: scheduler assigns single backend to all nodes
481 // Increase execution time for GPU backend
482 ExecTime et(_mock_backends);
483 /* for parallel executor: set a time, that is larger than sum_of_other_branches_nodes_cnt *
484 * npu_exec_time so that npu is prefered: the ith branch will wait for npu until it finishes the
485 * [0;i-1] branches nodes in DFS order. In each branch it goes deep intul doesn't encounter
486 * branching or scheduler assigns another backend to a node*/
487 setOperationExecTime(et, _gpu_backend, "Mul", false, OPERATION_SIZE, NPU_ET * 3 + 1);
488 setOperationExecTime(et, _gpu_backend, "FullyConnected", false, OPERATION_SIZE, NPU_ET * 3 + 1);
489 et.uploadOperationsExecTime();
492 auto backend_contexts = buildBackendContexts(*graph);
493 auto scheduler = compiler::HEScheduler(backend_contexts,
494 compiler::fetchCompilerOptionsFromGlobalConfig(subgs));
495 const auto br = scheduler.schedule(*graph);
496 ASSERT_EQ(br->getBackend(add_op_idx)->config()->id(), "npu");
497 ASSERT_EQ(br->getBackend(mul1_op_idx)->config()->id(), "npu");
498 ASSERT_EQ(br->getBackend(mul2_op_idx)->config()->id(), "npu");
499 ASSERT_EQ(br->getBackend(fc1_op_idx)->config()->id(), "npu");
500 ASSERT_EQ(br->getBackend(fc2_op_idx)->config()->id(), "npu");
501 ASSERT_EQ(br->getBackend(sub_op_idx)->config()->id(), "npu");
505 // SchedulerTestWithExecutorParam tests are parameterized with executor name and runs three times -
506 // one time for each executor
507 INSTANTIATE_TEST_CASE_P(AllExecutors, SchedulerTestWithExecutorParam,
508 testing::Values(LINEAR, DATAFLOW, PARALLEL));
510 // Test scheduler behavior for branched graph and enabled profiling mode
511 TEST_F(SchedulerTest, branched_graph_profiling_mode)
515 // Turn on profiling mode
516 setProfilingMode(true);
517 setExecutor(DATAFLOW);
521 auto graph(createBranchedGraph());
522 subgs.push(ir::SubgraphIndex{0}, graph);
523 OperationIndex add_op_idx(0), mul1_op_idx(1), mul2_op_idx(2), fc1_op_idx(3), fc2_op_idx(4),
527 // Expected behaviour: scheduler assigns backends to nodes with unknown execution time
529 // Set execution time for all backends/nodes except for cpu/Sub, npu/Mul, gpu/FC
530 ExecTime et(_mock_backends);
531 setOperationExecTime(et, _cpu_backend, "Add", false, OPERATION_SIZE, ET);
532 setOperationExecTime(et, _cpu_backend, "Mul", false, OPERATION_SIZE, ET + 1);
533 setOperationExecTime(et, _cpu_backend, "FullyConnected", false, OPERATION_SIZE, ET);
534 setOperationExecTime(et, _npu_backend, "Add", false, OPERATION_SIZE, ET);
535 setOperationExecTime(et, _npu_backend, "FullyConnected", false, OPERATION_SIZE, ET);
536 setOperationExecTime(et, _npu_backend, "Sub", false, OPERATION_SIZE, ET);
537 setOperationExecTime(et, _gpu_backend, "Add", false, OPERATION_SIZE, ET);
538 setOperationExecTime(et, _gpu_backend, "Mul", false, OPERATION_SIZE, ET + 1);
539 setOperationExecTime(et, _gpu_backend, "Sub", false, OPERATION_SIZE, ET);
540 et.uploadOperationsExecTime();
543 auto backend_contexts = buildBackendContexts(*graph);
544 auto scheduler = compiler::HEScheduler(backend_contexts,
545 compiler::fetchCompilerOptionsFromGlobalConfig(subgs));
546 const auto br = scheduler.schedule(*graph);
547 ASSERT_EQ(br->getBackend(mul1_op_idx)->config()->id(), "npu");
548 ASSERT_EQ(br->getBackend(mul2_op_idx)->config()->id(), "npu");
549 ASSERT_EQ(br->getBackend(fc1_op_idx)->config()->id(), "gpu");
550 ASSERT_EQ(br->getBackend(fc2_op_idx)->config()->id(), "gpu");
551 ASSERT_EQ(br->getBackend(sub_op_idx)->config()->id(), "cpu");
555 // Expected behaviour: scheduler shuffling backends, so different backends are assigned to
558 // Set execution time for rest backends/nodes (cpu/Sub, npu/Mul, gpu/FC)
559 ExecTime et(_mock_backends);
560 setOperationExecTime(et, _cpu_backend, "Sub", false, OPERATION_SIZE, ET);
561 setOperationExecTime(et, _npu_backend, "Mul", false, OPERATION_SIZE, ET + 1);
562 setOperationExecTime(et, _gpu_backend, "FullyConnected", false, OPERATION_SIZE, ET);
563 et.uploadOperationsExecTime();
566 auto backend_contexts = buildBackendContexts(*graph);
567 auto scheduler = compiler::HEScheduler(backend_contexts,
568 compiler::fetchCompilerOptionsFromGlobalConfig(subgs));
569 const auto br = scheduler.schedule(*graph);
570 ASSERT_NE(br->getBackend(add_op_idx)->config()->id(),
571 br->getBackend(mul1_op_idx)->config()->id());
572 ASSERT_NE(br->getBackend(add_op_idx)->config()->id(),
573 br->getBackend(fc1_op_idx)->config()->id());
574 ASSERT_NE(br->getBackend(mul1_op_idx)->config()->id(),
575 br->getBackend(mul2_op_idx)->config()->id());
576 ASSERT_NE(br->getBackend(fc1_op_idx)->config()->id(),
577 br->getBackend(fc2_op_idx)->config()->id());
578 ASSERT_NE(br->getBackend(mul2_op_idx)->config()->id(),
579 br->getBackend(sub_op_idx)->config()->id());
580 ASSERT_NE(br->getBackend(fc2_op_idx)->config()->id(),
581 br->getBackend(sub_op_idx)->config()->id());
585 // TODO: Add tests with unknown execution and permutation time
587 } // unnamed namespace