// // torchOptimize.cpp // MNNConverter // // Created by MNN on 2021/05/12. // Copyright © 2018, Alibaba Group Holding Limited // #include "torchOptimize.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "torchOpConverter.hpp" namespace torch { namespace jit { void removeUselessOps(Block* block) { for (auto it = block->nodes().begin(), end = block->nodes().end(); it != end; ++it) { for (auto b : it->blocks()) { removeUselessOps(b); } std::set uselessKind = { // prime prim::Print, prim::RaiseException, prim::TimePoint, prim::annotate, // aten aten::warn, }; // useless op if (uselessKind.count(it->kind())) { for (size_t i = 0; i < it->inputs().size();) { auto input = it->inputs().at(i); // only handling constants bc of potential side effects if (input->uses().size() == 1 && input->node()->kind() == prim::Constant) { it->removeInput(i); input->node()->destroy(); } else { ++i; } } it.destroyCurrent(); } if (it->kind() == prim::Loop) { if (it->outputs().empty()) { it.destroyCurrent(); } } if (it->kind().toUnqualString() == std::string("data") || it->kind() == prim::NumToTensor || it->kind() == aten::ScalarImplicit || it->kind() == aten::contiguous || it->kind() == aten::dropout || it->kind() == aten::dropout_ || it->kind() == aten::feature_dropout || it->kind() == aten::clone) { it->output()->replaceAllUsesWith(it->input(0)); for (int i = it->inputs().size()-1; i >= 0; i--) { it->removeInput(i); } it.destroyCurrent(); } if (it->kind() == aten::detach || it->kind() == aten::list || it->kind().toDisplayString() == std::string("aten::cpu")) { it->output()->replaceAllUsesWith(it->input(0)); for (int i = it->inputs().size()-1; i >= 0; i--) { it->removeInput(i); } it.destroyCurrent(); } if (it->kind() == aten::to) { auto dst = it->input(1); auto ivalue = toIValue(dst); if(!ivalue->isInt()) { it->output()->replaceAllUsesWith(it->input(0)); for (int i = it->inputs().size()-1; i >= 0; i--) { it->removeInput(i); } it.destroyCurrent(); } } if (it->kind() == aten::slice) { auto start = it->input(2); auto end = it->input(3); // [0 : 1 : INT_MAX] can remove if (toIValue(start) && getValue(start) == 0 && toIValue(end) && getValue(end) == 9223372036854775807) { if (it->inputs().size() > 4) { auto stride = it->input(4); if (toIValue(stride) && getValue(stride) != 1) { continue; } } it->output()->replaceAllUsesWith(it->input(0)); for (int i = it->inputs().size()-1; i >= 0; i--) { it->removeInput(i); } it.destroyCurrent(); } } } } /* We rewrite something like: x = {v0} x.append(v1) foo(x) x.append(v2) bar(x) to: x1 = {v0, v1} foo(x1) x2 = {v0, v1, v2} bar(x2) this is a strengthen version of RemoveListMutation */ void removeListAppend(Graph* graph, Block* block) { for (auto it = block->nodes().begin(); it != block->nodes().end();) { auto* node = *it; it++; for (Block* sub_block : node->blocks()) { removeListAppend(graph, sub_block); } if (!(node->kind() == aten::append && node->inputs().at(0)->node()->kind() == prim::ListConstruct)) { continue; } Value* mutated_value = node->inputs().at(0); Node* list_node = mutated_value->node(); Node* new_list_node = graph->create(prim::ListConstruct, 1); for (Value* input : list_node->inputs()) { new_list_node->addInput(input); } new_list_node->addInput(node->inputs().at(1)); new_list_node->copyMetadata(list_node); new_list_node->insertAfter(node); new_list_node->output()->setType(list_node->output()->type()); mutated_value->replaceAllUsesAfterNodeWith(node, new_list_node->output()); node->destroy(); } } /* We remove all ListConstruct op with only one input and not used by aten::cat, like below: %116 : Tensor?[] = prim::ListConstruct(%115) %alpha0.1 : Tensor = aten::index_put_(%alpha.1, %116, %x.1, %16) ListConstruct used by aten::cat will be reserved like below: %features.2 : Tensor[] = prim::ListConstruct(%input3.4) %concated_features.380 : Tensor = aten::cat(%features.2, %5) Attention: Runing this pass after removeListAppend */ void removeListConstructOps(Block* block) { for (auto it = block->nodes().begin(), end = block->nodes().end(); it != end; ++it) { for (auto b : it->blocks()) { removeUselessOps(b); } if (it->kind() == prim::ListConstruct && it->inputs().size() == 1) { bool remove = true; for (auto use : it->output()->uses()) { if (use.user->kind() == aten::cat) { remove = false; break; } } if (remove) { it->output()->replaceAllUsesWith(it->input(0)); it->removeInput(0); it.destroyCurrent(); } } } } /* We rewrite something like: y = chunk(x) v1, v2, v3 = ListUnpack(y) to: v1, v2, v3 = chunk(x) */ void FuseListUnpack(Block* block) { for (auto it = block->nodes().begin(); it != block->nodes().end();) { auto* node = *it; it++; for (Block* sub_block : node->blocks()) { FuseListUnpack(sub_block); } std::set fusekind = { aten::split, aten::split_with_sizes, aten::split_with_sizes, aten::unsafe_split_with_sizes, aten::unbind, aten::chunk, aten::unsafe_chunk, aten::where, }; if (fusekind.count(it->kind()) && it->outputs().size() == 1 && it->output()->uses().size() == 1) { const auto listunpack = it->output()->uses()[0].user; if (listunpack->kind() == prim::ListUnpack) { // it->i_(Symbol::fromQualString("attr::_outputs"), // static_cast(listunpack->outputs().size())); for (auto i = 0; i < listunpack->outputs().size(); ++i) { auto new_output = it->addOutput(); new_output->copyMetadata(listunpack->output(i)); } listunpack->removeAllInputs(); it->eraseOutput(0); listunpack->replaceAllUsesWith(*it); listunpack->destroy(); } } } } /* We rewrite something like: x = ListConstruct(v1, v2, v3) y = stack(y, axis) to: y = stack(v1, v2, v3, axis) */ void FuseListStack(Block* block) { for (auto it = block->nodes().begin(); it != block->nodes().end();) { auto* node = *it; it++; for (Block* sub_block : node->blocks()) { FuseListUnpack(sub_block); } std::set fusekind = { aten::stack }; if (it->kind() == aten::stack) { auto input = it->input(0)->node(); if (input->kind() == prim::ListConstruct) { auto axis = it->input(1); it->removeAllInputs(); for (int i = 0; i < input->inputs().size(); i++) { it->addInput(input->input(i)); } it->addInput(axis); input->destroy(); } } } } /* We rewrite something like: %y : int, %z : int = prim::Loop(%6, %2, %y.1, %z.1) # :6:4 block0(%i.1 : int, %y.11 : int, %z.11 : int): %y.5 : int = aten::add(%y.11, %i.1) # :7:8 %z.5 : int = aten::mul(%z.11, %5) # :8:8 -> (%2, %y.5, %z.5) to: %y : int, %z : int = prim::Loop(%6, %2, %y.1, %z.1) # :6:4 block0(%i.1 : int, %y.11 : int, %z.11 : int): %y.5 : int = aten::add(%y.1, %i.1) # :7:8 %z.5 : int = aten::mul(%z.1, %5) # :8:8 -> (%2, %y.5, %z.5) */ void LoopBodyLegal(Graph* graph, Block* block) { for (auto it = block->nodes().begin(); it != block->nodes().end();) { auto* node = *it; it++; for (Block* sub_block : node->blocks()) { LoopBodyLegal(graph, sub_block); } if (node->kind() == prim::Loop) { auto body = node->blocks()[0]; for (int i = body->inputs().size() - 1; i > 0; i--) { body->inputs().at(i)->replaceAllUsesWith(node->inputs().at(i + 1)); } } } } /* inference input type, such as below: x: Tensor; y = aten::embedding(_, x); then x's scalar type is int */ void InputTypeInfer(Graph* graph) { // TODO: add more typeOps and propagateOps static std::map> opInputTypes { // aten::embedding(Tensor weight, Tensor indices, int padding_idx, bool scale_grad_by_freq, bool sparse) -> Tensor { aten::embedding, { ScalarType::Float, ScalarType::Int } }, // aten::matmul(Tensor self, Tensor other) -> Tensor { aten::matmul, { ScalarType::Float, ScalarType::Float } }, // aten::linear(Tensor input, Tensor weight, Tensor bias) -> Tensor { aten::linear, { ScalarType::Float, ScalarType::Float, ScalarType::Float } }, // aten::conv2d(Tensor input, Tensor weight, Tensor bias, int[] stride, int[] padding, int[] dilation, int groups) -> Tensor { aten::conv2d, { ScalarType::Float, ScalarType::Float, ScalarType::Float } }, }; static std::set typePropagateOps { // shape change aten::slice, aten::view, aten::transpose, aten::permute, // compute aten::add, aten::sub, aten::mul, aten::div, }; auto mergeType = [](ScalarType type, ScalarType newType) { if (type == newType || newType == c10::ScalarType::Undefined) { return type; } if (type == c10::ScalarType::Undefined) { return newType; } MNN_ASSERT(false); return c10::ScalarType::Undefined; }; std::function getScalarType = [&](Value* input) -> ScalarType { auto inputType = ScalarType::Undefined; for (auto use : input->uses()) { int idx = -1; for (int i = 0; i < use.user->inputs().size(); i++) { if (use.user->input(i) == input) { idx = i; } } auto newType = ScalarType::Undefined; if (typePropagateOps.find(use.user->kind()) != typePropagateOps.end()) { newType = getScalarType(use.user->output()); } else { const auto iter = opInputTypes.find(use.user->kind()); if (iter != opInputTypes.end() && idx >= 0 && idx < iter->second.size()) { newType = iter->second[idx]; } } inputType = mergeType(inputType, newType); } return inputType; }; for (auto input : graph->inputs()) { auto type = input->type()->cast(); if (!type) { continue; } auto scalarType = getScalarType(input); input->setType(type->withScalarType(scalarType)); } } /* Unpack outputs, such as below: return List(x, y); -> return x, y; return Dict('x', x); -> return x; return Tuple(Tuple(x, y), z); return x, y, z; */ void OutputsUnpack(Graph* graph) { std::function&, std::vector&)> flattenTuple = [&flattenTuple](Node* tuple, std::vector& tuples, std::vector& values) -> void { tuples.push_back(tuple); for (auto input : tuple->inputs()) { auto node = input->node(); if (node->kind() == prim::TupleConstruct) { flattenTuple(node, tuples, values); } else { values.push_back(input); } } }; for (int i = 0; i < graph->outputs().size(); i++) { auto node = graph->outputs()[i]->node(); // unpack output switch (node->kind()) { case prim::TupleConstruct: { std::vector tuples; std::vector values; flattenTuple(node, tuples, values); for (auto realOutput : values) { graph->registerOutput(realOutput); } graph->eraseOutput(i); for (auto tuple : tuples) { if (!tuple->hasUses()) { tuple->destroy(); } } break; } case prim::DictConstruct: { graph->registerOutput(node->input(1)); graph->eraseOutput(i); node->destroy(); break; } case prim::ListConstruct: { for (int i = 0; i < node->inputs().size(); i++) { graph->registerOutput(node->input(i)); } graph->eraseOutput(i); node->destroy(); break; } } } } /* distinguish overloaded function, such as below: torch.max(Tensor, Tensor) is compare torch.max(Tensor, int) is reduce */ void overloadDistinguish(Block* block) { auto symb = c10::Symbol::fromQualString("attr::mnn_tag"); for (auto it = block->nodes().begin(); it != block->nodes().end();) { auto* node = *it; it++; for (Block* sub_block : node->blocks()) { overloadDistinguish(sub_block); } switch (node->kind()) { // min/max(Tensor, Tensor) is compare // min/max(Tensor, int) is reduce case aten::min: case aten::max: case aten::sum: if (node->inputs().size() > 1 && (node->input(1)->type()->kind() == c10::TypeKind::IntType || node->input(1)->type()->kind() == c10::TypeKind::ListType)) { node->s_(symb, "reduce"); } else { node->s_(symb, "binary"); } break; case aten::index: if (node->input(1)->node()->kind() == prim::ListConstruct) { node->s_(symb, "stridedslice"); } break; default: // do nothing break; } } } /* fuse as_tensor, such as below: d = prim::dtype(b); c = aten::as_tensor(a, d); -> c = aten::type_as(a, b) */ void FuseAsTensor(Graph* graph, Block* block) { for (auto it = block->nodes().begin(); it != block->nodes().end();) { auto* node = *it; it++; for (Block* sub_block : node->blocks()) { FuseAsTensor(graph, sub_block); } if (node->kind() == prim::dtype) { for (auto use : node->output(0)->uses()) { auto as_tensor = use.user; Node* typeAs = graph->create(aten::type_as, 1); typeAs->addInput(as_tensor->input(0)); typeAs->addInput(node->input(0)); typeAs->output(0)->copyMetadata(as_tensor->output(0)); as_tensor->replaceAllUsesWith(typeAs); as_tensor->removeAllInputs(); as_tensor->destroy(); } } } } /* fuse uniform, such as below: d = aten::empty(shape); c = aten::uniform_(a, low, hight); -> c = aten::uniform_(shape, low, hight) */ void FuseUniform(Graph* graph, Block* block) { for (auto it = block->nodes().begin(); it != block->nodes().end();) { auto* node = *it; it++; for (Block* sub_block : node->blocks()) { FuseUniform(graph, sub_block); } if (it->kind().toUnqualString() == std::string("uniform_")) { auto input = it->input(0)->node(); if (input->kind() == aten::empty) { it->replaceInput(0, input->input(0)); input->destroy(); } } } } std::shared_ptr torchOptPass(Module& module) { module.eval(); module = torch::jit::freeze_module(module); auto graph = module.get_methods()[0].graph(); Inline(*(graph.get())); // normalize, Example: aten::absolute -> aten::abs NormalizeOps(graph); // remove some ops, Example: prim::RaiseException removeUselessOps(graph->block()); removeDropout(module); // Example: x = x + 1; -> x_1 = x + 1; RemoveInplaceOps(graph); // Example: x = {v0}; x.append(v1); -> x = {v0, v1}; // RemoveListMutation(graph); removeListAppend(graph.get(), graph->block()); removeListConstructOps(graph->block()); //RemoveTensorMutation(graph); // elimate dead code EliminateDeadCode(graph, DCESideEffectPolicy::ALLOW_DELETING_NODES_WITH_SIDE_EFFECTS); // constant propagation ConstantPooling(graph); ConstantPropagation(graph); // fuse FuseGraph(graph, true); PeepholeOptimize(graph); FuseAddMM(graph); // FoldConvBatchNorm(module); FuseListUnpack(graph->block()); FuseListStack(graph->block()); // distinguish overload function overloadDistinguish(graph->block()); // legal loop body's var name LoopBodyLegal(graph.get(), graph->block()); // infer input tensor's scalar type by op InputTypeInfer(graph.get()); // split output tensor if wrap with list/tuple OutputsUnpack(graph.get()); // dtype + as_tensor -> type_as FuseAsTensor(graph.get(), graph->block()); // empty + uniform -> uniform FuseUniform(graph.get(), graph->block()); #ifdef MNN_DUMP_TORCHSCRIPT graph->dump(); #endif return graph; } } }