// This file is part of OpenCV project. // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html. // // Copyright (C) 2018 Intel Corporation #include "precomp.hpp" #include #include // list #include // setw, etc #include // ofstream #include #include #include // contains #include // chain #include "opencv2/gapi/util/optional.hpp" // util::optional #include "logger.hpp" // GAPI_LOG #include "compiler/gmodel.hpp" #include "compiler/gislandmodel.hpp" #include "compiler/passes/passes.hpp" #include "compiler/passes/helpers.hpp" #include "compiler/transactions.hpp" //////////////////////////////////////////////////////////////////////////////// // // N.B. // Merge is a binary operation (LHS `Merge` RHS) where LHS may be arbitrary // // After every merge, the procedure starts from the beginning (in the topological // order), thus trying to merge next "unmerged" island to the latest merged one. // //////////////////////////////////////////////////////////////////////////////// // Uncomment to dump more info on merge process // FIXME: make it user-configurable run-time option // #define DEBUG_MERGE namespace cv { namespace gimpl { namespace { bool fusionIsTrivial(const ade::Graph &src_graph) { // Fusion is considered trivial if there only one // active backend and no user-defined islands // FIXME: // Also check the cases backend can't handle // (e.x. GScalar connecting two fluid ops should split the graph) const GModel::ConstGraph g(src_graph); const auto& active_backends = g.metadata().get().backends; return active_backends.size() == 1 && ade::util::all_of(g.nodes(), [&](ade::NodeHandle nh) { return !g.metadata(nh).contains(); }); } void fuseTrivial(GIslandModel::Graph &g, const ade::Graph &src_graph) { const GModel::ConstGraph src_g(src_graph); const auto& backend = *src_g.metadata().get().backends.cbegin(); const auto& proto = src_g.metadata().get(); GIsland::node_set all, in_ops, out_ops; all.insert(src_g.nodes().begin(), src_g.nodes().end()); for (const auto nh : proto.in_nhs) { all.erase(nh); in_ops.insert(nh->outNodes().begin(), nh->outNodes().end()); } for (const auto nh : proto.out_nhs) { all.erase(nh); out_ops.insert(nh->inNodes().begin(), nh->inNodes().end()); } auto isl = std::make_shared(backend, std::move(all), std::move(in_ops), std::move(out_ops), util::optional{}); auto ih = GIslandModel::mkIslandNode(g, std::move(isl)); for (const auto nh : proto.in_nhs) { auto slot = GIslandModel::mkSlotNode(g, nh); g.link(slot, ih); } for (const auto nh : proto.out_nhs) { auto slot = GIslandModel::mkSlotNode(g, nh); g.link(ih, slot); } } struct MergeContext { using CycleCausers = std::pair< std::shared_ptr, std::shared_ptr >; struct CycleHasher final { std::size_t operator()(const CycleCausers& p) const { std::size_t a = std::hash()(p.first.get()); std::size_t b = std::hash()(p.second.get()); return a ^ (b << 1); } }; // Set of Islands pairs which cause a cycle if merged. // Every new merge produces a new Island, and if Islands were // merged (and thus dropped from GIslandModel), the objects may // still be alive as included into this set. std::unordered_set cycle_causers; }; bool canMerge(const GIslandModel::Graph &g, const ade::NodeHandle a_nh, const ade::NodeHandle /*slot_nh*/, const ade::NodeHandle b_nh, const MergeContext &ctx = MergeContext()) { auto a_ptr = g.metadata(a_nh).get().object; auto b_ptr = g.metadata(b_nh).get().object; GAPI_Assert(a_ptr.get()); GAPI_Assert(b_ptr.get()); // Islands with different affinity can't be merged if (a_ptr->backend() != b_ptr->backend()) return false; // Islands which cause a cycle can't be merged as well // (since the flag is set, the procedure already tried to // merge these islands in the past) if (ade::util::contains(ctx.cycle_causers, std::make_pair(a_ptr, b_ptr))|| ade::util::contains(ctx.cycle_causers, std::make_pair(b_ptr, a_ptr))) return false; // There may be user-defined islands. Initially user-defined // islands also are built from single operations and then merged // by this procedure, but there is some exceptions. // User-specified island can't be merged to an internal island if ( ( a_ptr->is_user_specified() && !b_ptr->is_user_specified()) || (!a_ptr->is_user_specified() && b_ptr->is_user_specified())) { return false; } else if (a_ptr->is_user_specified() && b_ptr->is_user_specified()) { // These islads are _different_ user-specified Islands // FIXME: today it may only differ by name if (a_ptr->name() != b_ptr->name()) return false; } // FIXME: add a backend-specified merge checker return true; } inline bool isProducedBy(const ade::NodeHandle &slot, const ade::NodeHandle &island) { // A data slot may have only 0 or 1 producer if (slot->inNodes().size() == 0) return false; return slot->inNodes().front() == island; } inline bool isConsumedBy(const ade::NodeHandle &slot, const ade::NodeHandle &island) { auto it = std::find_if(slot->outNodes().begin(), slot->outNodes().end(), [&](const ade::NodeHandle &nh) { return nh == island; }); return it != slot->outNodes().end(); } /** * Find a candidate Island for merge for the given Island nh. * * @param g Island Model where merge occurs * @param nh GIsland node, either LHS or RHS of probable merge * @param ctx Merge context, may contain some cached stuff to avoid * double/triple/etc checking * @return Tuple of Island handle, Data slot handle (which connects them), * and a position of found handle with respect to nh (IN/OUT) */ std::tuple findCandidate(const GIslandModel::Graph &g, ade::NodeHandle nh, const MergeContext &ctx = MergeContext()) { using namespace std::placeholders; // Find a first matching candidate GIsland for merge // among inputs for (const auto& input_data_nh : nh->inNodes()) { if (input_data_nh->inNodes().size() != 0) { // Data node must have a single producer only GAPI_DbgAssert(input_data_nh->inNodes().size() == 1); auto input_data_prod_nh = input_data_nh->inNodes().front(); if (canMerge(g, input_data_prod_nh, input_data_nh, nh, ctx)) return std::make_tuple(input_data_prod_nh, input_data_nh, Direction::In); } } // for(inNodes) // Ok, now try to find it among the outputs for (const auto& output_data_nh : nh->outNodes()) { auto mergeTest = [&](ade::NodeHandle cons_nh) -> bool { return canMerge(g, nh, output_data_nh, cons_nh, ctx); }; auto cand_it = std::find_if(output_data_nh->outNodes().begin(), output_data_nh->outNodes().end(), mergeTest); if (cand_it != output_data_nh->outNodes().end()) return std::make_tuple(*cand_it, output_data_nh, Direction::Out); } // for(outNodes) // Empty handle, no good candidates return std::make_tuple(ade::NodeHandle(), ade::NodeHandle(), Direction::Invalid); } // A cancellable merge of two GIslands, "a" and "b", connected via "slot" class MergeAction { ade::Graph &m_g; const ade::Graph &m_orig_g; GIslandModel::Graph m_gim; ade::NodeHandle m_prod; ade::NodeHandle m_slot; ade::NodeHandle m_cons; Change::List m_changes; struct MergeObjects { using NS = GIsland::node_set; NS all; // same as in GIsland NS in_ops; // same as in GIsland NS out_ops; // same as in GIsland NS opt_interim_slots; // can be dropped (optimized out) NS non_opt_interim_slots;// can't be dropped (extern. link) }; MergeObjects identify() const; public: MergeAction(ade::Graph &g, const ade::Graph &orig_g, ade::NodeHandle prod, ade::NodeHandle slot, ade::NodeHandle cons) : m_g(g) , m_orig_g(orig_g) , m_gim(GIslandModel::Graph(m_g)) , m_prod(prod) , m_slot(slot) , m_cons(cons) { } void tryMerge(); // Try to merge islands Prod and Cons void rollback(); // Roll-back changes if merge has been done but broke the model void commit(); // Fix changes in the model after successful merge }; // Merge proceduce is a replacement of two Islands, Prod and Cons, // connected via !Slot!, with a new Island, which contain all Prod // nodes + all Cons nodes, and reconnected in the graph properly: // // Merge(Prod, !Slot!, Cons) // // [Slot 2] // : // v // ... [Slot 0] -> Prod -> !Slot! -> Cons -> [Slot 3] -> ... // ... [Slot 1] -' : '-> [Slot 4] -> ... // V // ... // results into: // // ... [Slot 0] -> Merged -> [Slot 3] ... // ... [Slot 1] : :-> [Slot 4] ... // ... [Slot 2] ' '-> !Slot! ... // // The rules are the following: // 1) All Prod input slots become Merged input slots; // - Example: Slot 0 Slot 1 // 2) Any Cons input slots which come from Islands different to Prod // also become Merged input slots; // - Example: Slot 2 // 3) All Cons output slots become Merged output slots; // - Example: Slot 3, Slot 4 // 4) All Prod output slots which are not consumed by Cons // also become Merged output slots; // - (not shown on the example) // 5) If the !Slot! which connects Prod and Cons is consumed // exclusively by Cons, it is optimized out (dropped) from the model; // 6) If the !Slot! is used by consumers other by Cons, it // becomes an extra output of Merged // 7) !Slot! may be not the only connection between Prod and Cons, // but as a result of merge operation, all such connections // should be handles as described for !Slot! MergeAction::MergeObjects MergeAction::identify() const { auto lhs = m_gim.metadata(m_prod).get().object; auto rhs = m_gim.metadata(m_cons).get().object; GIsland::node_set interim_slots; GIsland::node_set merged_all(lhs->contents()); merged_all.insert(rhs->contents().begin(), rhs->contents().end()); GIsland::node_set merged_in_ops(lhs->in_ops()); // (1) for (auto cons_in_slot_nh : m_cons->inNodes()) // (2) { if (isProducedBy(cons_in_slot_nh, m_prod)) { interim_slots.insert(cons_in_slot_nh); // at this point, interim_slots are not sync with merged_all // (merged_all will be updated with interim_slots which // will be optimized out). } else { const auto extra_in_ops = rhs->consumers(m_g, cons_in_slot_nh); merged_in_ops.insert(extra_in_ops.begin(), extra_in_ops.end()); } } GIsland::node_set merged_out_ops(rhs->out_ops()); // (3) for (auto prod_out_slot_nh : m_prod->outNodes()) // (4) { if (!isConsumedBy(prod_out_slot_nh, m_cons)) { merged_out_ops.insert(lhs->producer(m_g, prod_out_slot_nh)); } } // (5,6,7) GIsland::node_set opt_interim_slots; GIsland::node_set non_opt_interim_slots; auto is_non_opt = [&](const ade::NodeHandle &slot_nh) { // If a data object associated with this slot is a part // of GComputation _output_ protocol, it can't be optimzied out const auto data_nh = m_gim.metadata(slot_nh).get().original_data_node; const auto &data = GModel::ConstGraph(m_orig_g).metadata(data_nh).get(); if (data.storage == Data::Storage::OUTPUT) return true; // Otherwise, a non-optimizeable data slot is the one consumed // by some other island than "cons" const auto it = std::find_if(slot_nh->outNodes().begin(), slot_nh->outNodes().end(), [&](ade::NodeHandle &&nh) {return nh != m_cons;}); return it != slot_nh->outNodes().end(); }; for (auto slot_nh : interim_slots) { // Put all intermediate data nodes (which are BOTH optimized // and not-optimized out) to Island contents. merged_all.insert(m_gim.metadata(slot_nh) .get().original_data_node); GIsland::node_set &dst = is_non_opt(slot_nh) ? non_opt_interim_slots // there are consumers other than m_cons : opt_interim_slots; // there's no consumers other than m_cons dst.insert(slot_nh); } // (4+6). // BTW, (4) could be "All Prod output slots read NOT ONLY by Cons" for (auto non_opt_slot_nh : non_opt_interim_slots) { merged_out_ops.insert(lhs->producer(m_g, non_opt_slot_nh)); } return MergeObjects{ merged_all, merged_in_ops, merged_out_ops, opt_interim_slots, non_opt_interim_slots }; } // FIXME(DM): Probably this procedure will be refactored dramatically one day... void MergeAction::tryMerge() { // _: Understand the contents and I/O connections of a new merged Island MergeObjects mo = identify(); auto lhs_obj = m_gim.metadata(m_prod).get().object; auto rhs_obj = m_gim.metadata(m_cons).get().object; GAPI_Assert( ( lhs_obj->is_user_specified() && rhs_obj->is_user_specified()) || (!lhs_obj->is_user_specified() && !rhs_obj->is_user_specified())); cv::util::optional maybe_user_tag; if (lhs_obj->is_user_specified() && rhs_obj->is_user_specified()) { GAPI_Assert(lhs_obj->name() == rhs_obj->name()); maybe_user_tag = cv::util::make_optional(lhs_obj->name()); } // A: Create a new Island and add it to the graph auto backend = m_gim.metadata(m_prod).get() .object->backend(); auto merged = std::make_shared(backend, std::move(mo.all), std::move(mo.in_ops), std::move(mo.out_ops), std::move(maybe_user_tag)); // FIXME: move this debugging to some user-controllable log-level #ifdef DEBUG_MERGE merged->debug(); #endif auto new_nh = GIslandModel::mkIslandNode(m_gim, std::move(merged)); m_changes.enqueue(new_nh); // B: Disconnect all Prod's input Slots from Prod, // connect it to Merged std::vector input_edges(m_prod->inEdges().begin(), m_prod->inEdges().end()); for (auto in_edge : input_edges) { m_changes.enqueue(m_g, in_edge->srcNode(), new_nh); m_changes.enqueue(m_g, m_prod, in_edge); } // C: Disconnect all Cons' output Slots from Cons, // connect it to Merged std::vector output_edges(m_cons->outEdges().begin(), m_cons->outEdges().end()); for (auto out_edge : output_edges) { m_changes.enqueue(m_g, new_nh, out_edge->dstNode()); m_changes.enqueue(m_g, m_cons, out_edge); } // D: Process the intermediate slots (betweed Prod n Cons). // D/1 - Those which are optimized out are just removed from the model for (auto opt_slot_nh : mo.opt_interim_slots) { GAPI_Assert(1 == opt_slot_nh->inNodes().size() ); GAPI_Assert(m_prod == opt_slot_nh->inNodes().front()); std::vector edges_to_drop; ade::util::copy(ade::util::chain(opt_slot_nh->inEdges(), opt_slot_nh->outEdges()), std::back_inserter(edges_to_drop)); for (auto eh : edges_to_drop) { m_changes.enqueue(m_g, opt_slot_nh, eh); } m_changes.enqueue(opt_slot_nh); } // D/2 - Those which are used externally are connected to new nh // as outputs. for (auto non_opt_slot_nh : mo.non_opt_interim_slots) { GAPI_Assert(1 == non_opt_slot_nh->inNodes().size() ); GAPI_Assert(m_prod == non_opt_slot_nh->inNodes().front()); m_changes.enqueue(m_g, non_opt_slot_nh, non_opt_slot_nh->inEdges().front()); std::vector edges_to_probably_drop (non_opt_slot_nh->outEdges().begin(), non_opt_slot_nh->outEdges().end());; for (auto eh : edges_to_probably_drop) { if (eh->dstNode() == m_cons) { // drop only edges to m_cons, as there's other consumers m_changes.enqueue(m_g, non_opt_slot_nh, eh); } } m_changes.enqueue(m_g, new_nh, non_opt_slot_nh); } // E. All Prod's output edges which are directly related to Merge (e.g. // connected to Cons) were processed on step (D). // Relink the remaining output links std::vector prod_extra_out_edges (m_prod->outEdges().begin(), m_prod->outEdges().end()); for (auto extra_out : prod_extra_out_edges) { m_changes.enqueue(m_g, new_nh, extra_out->dstNode()); m_changes.enqueue(m_g, m_prod, extra_out); } // F. All Cons' input edges which are directly related to merge (e.g. // connected to Prod) were processed on step (D) as well, // remaining should become Merged island's input edges std::vector cons_extra_in_edges (m_cons->inEdges().begin(), m_cons->inEdges().end()); for (auto extra_in : cons_extra_in_edges) { m_changes.enqueue(m_g, extra_in->srcNode(), new_nh); m_changes.enqueue(m_g, m_cons, extra_in); } // G. Finally, drop the original Island nodes. DropNode() does // the sanity check for us (both nodes should have 0 edges). m_changes.enqueue(m_prod); m_changes.enqueue(m_cons); } void MergeAction::rollback() { m_changes.rollback(m_g); } void MergeAction::commit() { m_changes.commit(m_g); } #ifdef DEBUG_MERGE void merge_debug(const ade::Graph &g, int iteration) { std::stringstream filename; filename << "fusion_" << static_cast(&g) << "_" << std::setw(4) << std::setfill('0') << iteration << ".dot"; std::ofstream ofs(filename.str()); passes::dumpDot(g, ofs); } #endif void fuseGeneral(ade::Graph& im, const ade::Graph& g) { GIslandModel::Graph gim(im); MergeContext mc; bool there_was_a_merge = false; std::size_t iteration = 0u; do { there_was_a_merge = false; // FIXME: move this debugging to some user-controllable log level #ifdef DEBUG_MERGE GAPI_LOG_INFO(NULL, "Before next merge attempt " << iteration << "..."); merge_debug(g, iteration); #endif iteration++; auto sorted = pass_helpers::topoSort(im); for (auto nh : sorted) { if (NodeKind::ISLAND == gim.metadata(nh).get().k) { ade::NodeHandle cand_nh; ade::NodeHandle cand_slot; Direction dir = Direction::Invalid; std::tie(cand_nh, cand_slot, dir) = findCandidate(gim, nh, mc); if (cand_nh != nullptr && dir != Direction::Invalid) { auto lhs_nh = (dir == Direction::In ? cand_nh : nh); auto rhs_nh = (dir == Direction::Out ? cand_nh : nh); auto l_obj = gim.metadata(lhs_nh).get().object; auto r_obj = gim.metadata(rhs_nh).get().object; GAPI_LOG_INFO(NULL, r_obj->name() << " can be merged into " << l_obj->name()); // Try to do a merge. If merge was successful, check if the // graph have cycles (cycles are prohibited at this point). // If there are cycles, roll-back the merge and mark a pair of // these Islands with a special tag - "cycle-causing". MergeAction action(im, g, lhs_nh, cand_slot, rhs_nh); action.tryMerge(); if (pass_helpers::hasCycles(im)) { GAPI_LOG_INFO(NULL, "merge(" << l_obj->name() << "," << r_obj->name() << ") caused cycle, rolling back..."); action.rollback(); // don't try to merge these two islands next time (findCandidate will use that) mc.cycle_causers.insert({l_obj, r_obj}); } else { GAPI_LOG_INFO(NULL, "merge(" << l_obj->name() << "," << r_obj->name() << ") was successful!"); action.commit(); #ifdef DEBUG_MERGE GIslandModel::syncIslandTags(gim, g); #endif there_was_a_merge = true; break; // start do{}while from the beginning } } // if(can merge) } // if(ISLAND) } // for(all nodes) } while (there_was_a_merge); } } // anonymous namespace void passes::fuseIslands(ade::passes::PassContext &ctx) { std::shared_ptr gptr(new ade::Graph); GIslandModel::Graph gim(*gptr); if (fusionIsTrivial(ctx.graph)) { fuseTrivial(gim, ctx.graph); } else { GIslandModel::generateInitial(gim, ctx.graph); fuseGeneral(*gptr.get(), ctx.graph); } GModel::Graph(ctx.graph).metadata().set(IslandModel{std::move(gptr)}); } void passes::syncIslandTags(ade::passes::PassContext &ctx) { GModel::Graph gm(ctx.graph); std::shared_ptr gptr(gm.metadata().get().model); GIslandModel::Graph gim(*gptr); GIslandModel::syncIslandTags(gim, ctx.graph); } }} // namespace cv::gimpl