From 01f4e9e3926dd56ee2a501994fbdf1f3f723c45a Mon Sep 17 00:00:00 2001 From: kanvi-nervana Date: Wed, 15 Jan 2020 08:12:33 -0800 Subject: [PATCH 1/5] upgrade to ngraphv0.28.0-rc-1 (#437) --- CMakeLists.txt | 2 +- README.md | 2 +- bazel/WORKSPACE | 8 ++++---- bazel/ngraph.BUILD | 6 +++--- build_ngtf.py | 2 +- ngraph_bridge/version.cc | 2 +- python/setup.in.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 822185312..353e88df1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -248,7 +248,7 @@ if (NOT USE_PRE_BUILT_NGRAPH) ExternalProject_Add( ext_ngraph GIT_REPOSITORY https://github.com/NervanaSystems/ngraph - GIT_TAG v0.28.0-rc.0 + GIT_TAG v0.28.0-rc.1 CMAKE_ARGS -DNGRAPH_DISTRIBUTED_ENABLE=${NGRAPH_DISTRIBUTED_ENABLE} -DNGRAPH_INSTALL_PREFIX=${NGRAPH_ARTIFACTS_DIR} diff --git a/README.md b/README.md index 4358048c7..d5923cb49 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Once TensorFlow's dependencies are installed, clone the `ngraph-bridge` repo: git clone https://github.com/tensorflow/ngraph-bridge.git cd ngraph-bridge - git checkout v0.22.0-rc2 + git checkout v0.22.0-rc3 Run the following Python script to build TensorFlow, nGraph, and the bridge. Use Python 3.5: diff --git a/bazel/WORKSPACE b/bazel/WORKSPACE index 541bb891e..eb19be550 100644 --- a/bazel/WORKSPACE +++ b/bazel/WORKSPACE @@ -55,11 +55,11 @@ tf_workspace(path_prefix = "", tf_repo_name = "org_tensorflow") http_archive( name = "ngraph", build_file = "//:bazel/ngraph.BUILD", - sha256 = "be59da1e04840b3980d3bcba7925b99ac2007e37e3952ef7c77caedba9734654", - strip_prefix = "ngraph-0.28.0-rc.0", + sha256 = "24e723f7ed47e2c0068fb5868d8ca88542948a13c9de063d8108f2b67785b089", + strip_prefix = "ngraph-0.28.0-rc.1", urls = [ - "https://mirror.bazel.build/github.com/NervanaSystems/ngraph/archive/v0.28.0-rc.0.tar.gz", - "https://github.com/NervanaSystems/ngraph/archive/v0.28.0-rc.0.tar.gz" + "https://mirror.bazel.build/github.com/NervanaSystems/ngraph/archive/v0.28.0-rc.1.tar.gz", + "https://github.com/NervanaSystems/ngraph/archive/v0.28.0-rc.1.tar.gz" ], ) diff --git a/bazel/ngraph.BUILD b/bazel/ngraph.BUILD index ddc6b081d..cc84b81a0 100644 --- a/bazel/ngraph.BUILD +++ b/bazel/ngraph.BUILD @@ -68,7 +68,7 @@ cc_library( "-fstack-protector-all", '-D SHARED_LIB_PREFIX=\\"lib\\"', '-D SHARED_LIB_SUFFIX=\\".so\\"', - '-D NGRAPH_VERSION=\\"v0.28.0-rc.0\\"', + '-D NGRAPH_VERSION=\\"v0.28.0-rc.1\\"', "-D NGRAPH_DEX_ONLY", '-D PROJECT_ROOT_DIR=\\"\\"', '-D NGRAPH_STATIC_LIB_ENABLE', @@ -112,7 +112,7 @@ cc_library( "-fstack-protector-all", '-D SHARED_LIB_PREFIX=\\"lib\\"', '-D SHARED_LIB_SUFFIX=\\".so\\"', - '-D NGRAPH_VERSION=\\"v0.28.0-rc.0\\"', + '-D NGRAPH_VERSION=\\"v0.28.0-rc.1\\"', "-D NGRAPH_DEX_ONLY", '-D PROJECT_ROOT_DIR=\\"\\"', '-D NGRAPH_USE_LEGACY_MKLDNN', @@ -272,7 +272,7 @@ cc_library( "-fstack-protector-all", '-D SHARED_LIB_PREFIX=\\"lib\\"', '-D SHARED_LIB_SUFFIX=\\".so\\"', - '-D NGRAPH_VERSION=\\"v0.28.0-rc.0\\"', + '-D NGRAPH_VERSION=\\"v0.28.0-rc.1\\"', "-D NGRAPH_DEX_ONLY", '-D PROJECT_ROOT_DIR=\\"\\"', '-D NGRAPH_CPU_STATIC_LIB_ENABLE', diff --git a/build_ngtf.py b/build_ngtf.py index 71f7f0220..f0667c55d 100755 --- a/build_ngtf.py +++ b/build_ngtf.py @@ -53,7 +53,7 @@ def main(): ''' # Component versions - ngraph_version = "v0.28.0-rc.0" + ngraph_version = "v0.28.0-rc.1" tf_version = "v1.14.0" # Command line parser options diff --git a/ngraph_bridge/version.cc b/ngraph_bridge/version.cc index 506421f7e..569391a99 100644 --- a/ngraph_bridge/version.cc +++ b/ngraph_bridge/version.cc @@ -32,7 +32,7 @@ // candidate such as v0.7.0-rc0 // The code in master will always have the last released version number // with a suffix of '-master' -#define NG_TF_VERSION_SUFFIX "-rc2" +#define NG_TF_VERSION_SUFFIX "-rc3" #define VERSION_STR_HELPER(x) #x #define VERSION_STR(x) VERSION_STR_HELPER(x) diff --git a/python/setup.in.py b/python/setup.in.py index c87d36e88..6e0a37428 100644 --- a/python/setup.in.py +++ b/python/setup.in.py @@ -59,7 +59,7 @@ def get_tag(self): setup( name='ngraph_tensorflow_bridge', - version='0.22.0rc2', + version='0.22.0rc3', description='Intel nGraph compiler and runtime for TensorFlow', long_description=long_description, long_description_content_type="text/markdown", From 164021eb3f2f4794d8944948f1bc9e15e080e805 Mon Sep 17 00:00:00 2001 From: Shrestha Malik Date: Thu, 16 Jan 2020 08:46:21 -0800 Subject: [PATCH 2/5] fix segfault (#440) Segfault was seen if NGRAPH_TF_USE_PREFETCH was set in a script that did not have a MakeIterator/PrefetchOp --- .../enable_variable_ops/ngraph_capture_variables.cc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ngraph_bridge/enable_variable_ops/ngraph_capture_variables.cc b/ngraph_bridge/enable_variable_ops/ngraph_capture_variables.cc index e2ea7daad..ef26b10ba 100644 --- a/ngraph_bridge/enable_variable_ops/ngraph_capture_variables.cc +++ b/ngraph_bridge/enable_variable_ops/ngraph_capture_variables.cc @@ -114,6 +114,13 @@ Status CaptureVariables(Graph* graph, std::set skip_these_nodes) { // If Prefetch is requested if (std::getenv(NGraphPrefetchSharedResouce::NGRAPH_TF_USE_PREFETCH) != nullptr) { + if (make_iterator_nodes.size() == 0) { + // No MakeIterator Op found in the Graph + make_iterator_nodes.clear(); + nodes_to_capture.clear(); + return Status::OK(); + } + if (make_iterator_nodes.size() > 1) { return errors::Internal( "Found more than 1 MakeIterator nodes. This case is not supported."); From 244d4e8e8059f210af9cb7586d9f2e915adfb078 Mon Sep 17 00:00:00 2001 From: Sayantan Sarkar Date: Fri, 17 Jan 2020 09:26:37 -0800 Subject: [PATCH 3/5] Sarkars/add analysis pass mode to encapsulate cluster (#426) --- ngraph_bridge/ngraph_encapsulate_clusters.cc | 1037 +++++++++-------- ngraph_bridge/ngraph_encapsulate_clusters.h | 108 +- .../encapsulate_clusters_test.cc | 199 +++- 3 files changed, 869 insertions(+), 475 deletions(-) diff --git a/ngraph_bridge/ngraph_encapsulate_clusters.cc b/ngraph_bridge/ngraph_encapsulate_clusters.cc index 168e539c6..d9e506894 100644 --- a/ngraph_bridge/ngraph_encapsulate_clusters.cc +++ b/ngraph_bridge/ngraph_encapsulate_clusters.cc @@ -62,7 +62,7 @@ namespace ngraph_bridge { // // begin code copied and pasted (and modified) from graph.cc... -static void AddInput(NodeDef* dst, StringPiece src_name, int src_slot) { +void Encapsulator::AddInput(NodeDef* dst, StringPiece src_name, int src_slot) { if (src_slot == Graph::kControlSlot) { dst->add_input(strings::StrCat("^", src_name)); } else if (src_slot == 0) { @@ -75,30 +75,287 @@ static void AddInput(NodeDef* dst, StringPiece src_name, int src_slot) { Status EncapsulateClusters( Graph* graph, int graph_id, FunctionDefLibrary* fdeflib, - std::unordered_map device_config, - AOTInfo aot_info) { - // A map from cluster indices to the expected device name for nodes - // in that cluster. - std::map device_name_map; - - // We *should* eventually have a way of monitoring the device and the backend - // together - std::map backend_name_map; - - // As we build the graph we will be tracking the.. TODO(amprocte): finish - // this comment. - std::map, std::tuple> output_remap_map; - std::map, int> input_remap_map; - std::map, string> input_rename_map; - - // A map from cluster indices to a vector of input data types. - std::map>> cluster_input_map; - // A map from cluster indices to a vector of output data types. - std::map> cluster_output_dt_map; - - // A map from cluster indices to corresponding NGraphEncapsulate nodes. - std::map cluster_node_map; + const std::unordered_map& device_config, + const AOTInfo& aot_info) { + Encapsulator enc(graph); + NGRAPH_VLOG(3) << "Running AnalysisPass in EncapsulateClusters"; + TF_RETURN_IF_ERROR(enc.AnalysisPass()); + NGRAPH_VLOG(3) << "Running RewritePass in EncapsulateClusters"; + TF_RETURN_IF_ERROR(enc.RewritePass(fdeflib, graph_id, device_config)); + NGRAPH_VLOG(3) << "Performing AOT in EncapsulateClusters"; + TF_RETURN_IF_ERROR(PerformAOTOnEncapsulates(graph, aot_info)); + + set newly_created_cluster_ids; + TF_RETURN_IF_ERROR(enc.GetNewClusterIDs(newly_created_cluster_ids)); + // Pass 9 (optional, only run if environment variable + // NGRAPH_TF_DUMP_CLUSTERS is set): validate the graph def, and + // make sure we can construct a graph from it. + if (std::getenv("NGRAPH_TF_DUMP_CLUSTERS")) { + for (auto& cluster_idx : newly_created_cluster_ids) { + TF_RETURN_IF_ERROR(graph::ValidateGraphDef( + *NGraphClusterManager::GetClusterGraph(cluster_idx), + *OpRegistry::Global())); + + Graph g(OpRegistry::Global()); + GraphConstructorOptions opts; + opts.allow_internal_ops = true; + TF_RETURN_IF_ERROR(ConvertGraphDefToGraph( + opts, *NGraphClusterManager::GetClusterGraph(cluster_idx), &g)); + + std::stringstream ss; + ss << "ngraph_cluster_" << cluster_idx; + std::string filename_prefix = ss.str(); + + GraphToPbTextFile(&g, filename_prefix + ".pbtxt"); + GraphToDotFile(&g, filename_prefix + ".dot", + "nGraph Cluster Dump: " + filename_prefix); + } + } + + return Status::OK(); +} + +Status PerformAOTOnEncapsulates(Graph* graph, const AOTInfo& aot_info) { + bool aot_requested; + set performed_aot_on_enc; + std::set>> node_shapes_hints_sets; + std::tie(aot_requested, node_shapes_hints_sets) = aot_info; + if (aot_requested) { + NGRAPH_VLOG(3) << "AOT requested"; + if (!ngraph_tf_is_grappler_enabled()) { + return errors::Internal( + "AOT requested for non grappler build. Please use grappler build if " + "AOT is required"); + } + string input_node_type = "Placeholder"; + // In case of grappler, we have Placeholder, which might contain shape info, + // so it is possible we can aot without any provided shapes + // in normal pass its args. unless shapes are provided there is no chance of + // reading shapes from args. + + // map between node name and the PartialShape it contains + std::map node_partial_shape_map = + GetShapesFromTFInputnodes(graph, input_node_type); + + // If no shape hints are provided but the placeholders contain complete + // shape, then we still need to enter the for loop below to compute AOT. + // Hence adding the shapes from placeholders as hints. + + if (node_shapes_hints_sets.size() == 0) { + NGRAPH_VLOG(5) << "Using shapes from placeholders as hint"; + + std::map> shape_from_placeholders_as_hints; + for (auto itr : node_partial_shape_map) { + shape_from_placeholders_as_hints.insert( + {itr.first, itr.second.get_shape_vector()}); + } + node_shapes_hints_sets.insert(shape_from_placeholders_as_hints); + } + // TODO: .....CHECK ABOVE IF + + std::map> inputs_node_shapes_for_compilation; + // Iterate over each shape hint and see if they can be used + for (ShapeHintMap single_hint : node_shapes_hints_sets) { + // A boolean to determine if we can AOT for this single_hint + bool can_aot = true; + + for (auto itr_single_hint : single_hint) { + if (node_partial_shape_map.find(itr_single_hint.first) == + node_partial_shape_map.end()) { + return errors::Internal("Passed hint for node ", + itr_single_hint.first, + " but there is no input with that name"); + } + } + + for (auto node : graph->op_nodes()) { + if (node->type_string() == input_node_type) { + PartialShape partial_shape_from_node = + node_partial_shape_map.at(node->name()); + + PartialShape combined_shape_info = CombineNodeInfoAndHint( + node, partial_shape_from_node, single_hint); + + can_aot = combined_shape_info.is_valid() && + combined_shape_info.is_concrete(); + if (can_aot) { + inputs_node_shapes_for_compilation[node->name()] = + combined_shape_info.get_shape_vector(); + } else { + // TODO: necessarily break? Maybe some things can be AOT, others + // maybe not + string fail_reason = + (combined_shape_info.is_valid() + ? (node->name() + " could not be concretized") + : "it is invalid for " + node->name()); + return errors::Internal("Cannot AOT using this hint (", + HintAsString(single_hint), ") as ", + fail_reason); + break; + } + } // end of if (node->type_string() == input_node_type) + } // End of for loop that goes through all nodes + + // Did we manage to concretize all input shapes? + for (auto itr : node_partial_shape_map) { // iterate over all inputs + if (inputs_node_shapes_for_compilation.find(itr.first) == + inputs_node_shapes_for_compilation.end()) { + can_aot = false; + // TODO: print "this" hint + return errors::Internal("Cannot AOT using this hint (", + HintAsString(single_hint), ") for ", + (itr.first), " was not concretized"); + } + } + + if (!can_aot) { + return errors::Internal( + "AOT requested, but could not perform AOT for hint = ", + HintAsString(single_hint)); + } + + // At this point we have collected all the AOT information and now we are + // ready to translate and compile + for (auto node : graph->op_nodes()) { + if (node->type_string() == "NGraphEncapsulate") { + // Check inputs of the encapsulates. They can only be fed by fully + // concrete shapes (after going through the shape hints) or consts + std::vector st_inputs; + GetStaticInputs(node, &st_inputs); + // Current assumption is that only encapsulates without static + // inputs are AOT + if (st_inputs.size() != 0) { + return errors::Internal( + "AOT requested. Found an encapsulate with static inputs, but " + "that is not supported"); + } + + // get backend. + // TODO: these sections can be hoisted out of the main loop + std::string backend_name; + TF_RETURN_IF_ERROR( + GetNodeAttr(node->attrs(), "ngraph_backend", &backend_name)); + std::string device_id; + TF_RETURN_IF_ERROR( + GetNodeAttr(node->attrs(), "ngraph_device_id", &device_id)); + string op_backend_name; + try { + op_backend_name = BackendManager::GetBackendCreationString( + backend_name, device_id); + } catch (const std::exception& exp) { + return errors::Internal( + "Caught exception while creating backend string ", exp.what(), + "\n"); + } + TF_RETURN_IF_ERROR(BackendManager::CreateBackend( + op_backend_name)); // Created a backend here. must free it + // TranslateGraph must be called AFTER CreateBackend because some TF + // ops like CNMS and gather use backend specific nodes + std::unordered_map additional_attribute_map; + for (auto itr : node->attrs()) { + // Find the optional attributes to be sent to the backend. + // The optional attributes have '_ngraph_' appended to the start + // so we need to get rid of that and only send the remaining + // string + // since the backend will only look for that. + // '_ngraph_' is only appended for the bridge. + // For e.g. _ngraph_ice_cores --> ice_cores + if (itr.first.find("_ngraph_") != std::string::npos) { + // leave out _ngraph_aot_requested + if (itr.first.find("_ngraph_aot_requested") == + std::string::npos) { + additional_attribute_map.insert( + {itr.first.substr(strlen("_ngraph_")), itr.second.s()}); + } + } + } + BackendManager::SetConfig(op_backend_name, additional_attribute_map); + + // Backend has been created and setup. Now translate + string signature; + std::shared_ptr ng_function; + TF_RETURN_IF_ERROR( + PerformTranslation(node, inputs_node_shapes_for_compilation, + signature, ng_function)); + int json_indentation = 4; + string serialized_ngfunc( + ngraph::serialize(ng_function, json_indentation)); + + // Translation done, now compile + ng::runtime::Backend* op_backend = nullptr; + try { + op_backend = BackendManager::GetBackend(op_backend_name); + } catch (const std::out_of_range& e) { + NGRAPH_VLOG(5) << "Exception: " << e.what(); + BackendManager::ReleaseBackend(op_backend_name); + throw; + } + BackendManager::LockBackend(op_backend_name); + std::shared_ptr ng_exec; + try { + ng_exec = op_backend->compile(ng_function); + } catch (...) { + BackendManager::UnlockBackend(op_backend_name); + Status st = + NgraphSerialize("tf_function_error_aot.json", ng_function); + BackendManager::ReleaseBackend(op_backend_name); + return errors::Internal( + "Failed to compile ng_function for AOT.", + (st.ok() ? "" + : " Failed to serialize as well with error: " + + st.error_message())); + } + BackendManager::UnlockBackend(op_backend_name); + BackendManager::ReleaseBackend(op_backend_name); + + // Compilation done, now serialize and attach as attribute + stringstream exec_dump; + ng_exec->save(exec_dump); + // ng function attached as debugging information + node->AddAttr("_ngraph_aot_ngfunction_" + signature, + serialized_ngfunc); + // Compute will use this ngexec + node->AddAttr("_ngraph_aot_ngexec_" + signature, exec_dump.str()); + // We do not need to add "_ngraph_aot_requested" attribute since it + // already is already present in device_config and inserted into the + // currently created NGraphEncapsulate + // TODO: create a separate namespace of node attributes for backend + // and for bridge + performed_aot_on_enc.insert(node->name()); + NGRAPH_VLOG(5) << "Performed AOT on " << node->name(); + } + } + } // end of for (ShapeHintMap single_hint : node_shapes_hints_sets) + + // In the end assert that all encapsulates have performed AOT + for (auto node : graph->op_nodes()) { + if (node->type_string() == "NGraphEncapsulate") { + if (performed_aot_on_enc.find(node->name()) == + performed_aot_on_enc.end()) { + return errors::Internal("Requested AOT, but did not perform AOT on ", + node->name()); + } + } + } + } // end of if (aot_requested) + return Status::OK(); +} + +Encapsulator::Encapsulator(Graph* g) + : graph(g), analysis_done(false), rewrite_done(false) {} + +Status Encapsulator::AnalysisPass() { + if (rewrite_done) { + return errors::Internal( + "In Encapsulator, AnalysisPass called after RewritePass was already " + "done"); + } + + if (analysis_done) { + return errors::Internal( + "In Encapsulator, AnalysisPass called more than once"); + } // Pass 1: Populate the cluster-index-to-device name map for each existing // cluster. PIGGYBACKING BACKEND TEST HERE, THEY WILL GET COMBINED INTO ONE for (auto node : graph->op_nodes()) { @@ -309,22 +566,127 @@ Status EncapsulateClusters( } } - // Pass 3: Create encapsulation nodes for all clusters. - for (auto& kv : device_name_map) { - int cluster_idx = kv.first; - string cluster_backend = backend_name_map[cluster_idx]; - - std::stringstream ss; - ss << "ngraph_cluster_" << cluster_idx; + // Pass 5: Make copies of all clustered nodes inside the cluster graphs, + // rewiring the inputs in their NodeDefs as we go. - std::vector input_types; - std::vector inputs; + // Originally Pass 5 ran after Pass 4 ofcourse. But now calling it right after + // Pass 2 in the Analysis Phase. + // Pass 4 took care of removing some inter-cluster control edges, so by the + // time Pass 5 was run, those control inputs would have been removed + // But now since Pass 5 is running before Pass 4, we must take special care to + // not add inter-cluster (or TF to cluster) control edges in the graphdef we + // copy into the ClusterManager + // This is taken care of in the "if (edge->IsControlEdge())" line in the for + // loop over all edges + for (auto node : graph->op_nodes()) { + int cluster_idx; - for (auto& tup : cluster_input_map[cluster_idx]) { - int src_node_id; - int src_output_idx; - DataType dt; - std::tie(src_node_id, src_output_idx, dt) = tup; + if (GetNodeAttr(node->attrs(), "_ngraph_cluster", &cluster_idx) != + Status::OK()) { + continue; + } + + // Because the input names may have changed from the original node def, + // we will need to borrow some code from Graph::ToGraphDefSubRange in + // tensorflow/core/graph/graph.cc that rewrites the node's input list. + + // begin code copied and pasted (and modified) from graph.cc... + NodeDef original_def = node->def(); + + // Get the inputs for this Node. We make sure control inputs are + // after data inputs, as required by GraphDef. + std::vector inputs; + inputs.resize(node->num_inputs(), nullptr); + for (const Edge* edge : node->in_edges()) { + if (edge->IsControlEdge()) { + int src_cluster_idx; + auto ctrl_src = edge->src(); + auto st = GetNodeCluster(ctrl_src, &src_cluster_idx); + if (st.ok()) { + if (src_cluster_idx == cluster_idx) { + inputs.push_back(edge); + } + } + } else { + CHECK(inputs[edge->dst_input()] == nullptr) + << "Edge " << edge->src()->DebugString() << ":" + << edge->dst()->DebugString() << " with dst_input " + << edge->dst_input() << " and had pre-existing input edge " + << inputs[edge->dst_input()]->src()->DebugString() << ":" + << inputs[edge->dst_input()]->dst()->DebugString(); + + inputs[edge->dst_input()] = edge; + } + } + original_def.clear_input(); + original_def.mutable_input()->Reserve(inputs.size()); + + for (size_t i = 0; i < inputs.size(); ++i) { + const Edge* edge = inputs[i]; + if (edge == nullptr) { + if (i < node->requested_inputs().size()) { + original_def.add_input(node->requested_inputs()[i]); + } else { + original_def.add_input(""); + } + } else { + const Node* src = edge->src(); + if (!src->IsOp()) continue; + AddInput(&original_def, src->name(), edge->src_output()); + } + } + // ...end code copied and pasted (and modified) from graph.cc + + auto node_def = + NGraphClusterManager::GetClusterGraph(cluster_idx)->add_node(); + cluster_indices_for_this_graph.insert(cluster_idx); + *node_def = original_def; + + for (auto& input : *(node_def->mutable_input())) { + TensorId tensor_id = ParseTensorName(input); + + string tensor_name(tensor_id.first); + auto it = input_rename_map.find( + std::make_tuple(cluster_idx, tensor_name, tensor_id.second)); + + if (it != input_rename_map.end()) { + input = it->second; + } + } + } + + analysis_done = true; + + return Status::OK(); +} + +Status Encapsulator::RewritePass( + FunctionDefLibrary* fdeflib, int graph_id, + const std::unordered_map& device_config) { + if (!analysis_done) { + return errors::Internal( + "In Encapsulator, called RewritePass without calling AnalysisPass"); + } + if (rewrite_done) { + return errors::Internal( + "In Encapsulator, called RewritePass more than once"); + } + // Pass 3: Create encapsulation nodes for all clusters. + for (auto& kv : device_name_map) { + int cluster_idx = kv.first; + string cluster_backend = backend_name_map[cluster_idx]; + + std::stringstream ss; + ss << "ngraph_cluster_" << cluster_idx; + + std::vector input_types; + std::vector inputs; + + for (auto& tup : cluster_input_map[cluster_idx]) { + int src_node_id; + int src_output_idx; + DataType dt; + std::tie(src_node_id, src_output_idx, dt) = tup; input_types.push_back(dt); @@ -420,79 +782,6 @@ Status EncapsulateClusters( } } - // Pass 5: Make copies of all clustered nodes inside the cluster graphs, - // rewiring the inputs in their NodeDefs as we go. - std::set cluster_indices_for_this_graph; - for (auto node : graph->op_nodes()) { - int cluster_idx; - - if (GetNodeAttr(node->attrs(), "_ngraph_cluster", &cluster_idx) != - Status::OK()) { - continue; - } - - // Because the input names may have changed from the original node def, - // we will need to borrow some code from Graph::ToGraphDefSubRange in - // tensorflow/core/graph/graph.cc that rewrites the node's input list. - - // begin code copied and pasted (and modified) from graph.cc... - NodeDef original_def = node->def(); - - // Get the inputs for this Node. We make sure control inputs are - // after data inputs, as required by GraphDef. - std::vector inputs; - inputs.resize(node->num_inputs(), nullptr); - for (const Edge* edge : node->in_edges()) { - if (edge->IsControlEdge()) { - inputs.push_back(edge); - } else { - CHECK(inputs[edge->dst_input()] == nullptr) - << "Edge " << edge->src()->DebugString() << ":" - << edge->dst()->DebugString() << " with dst_input " - << edge->dst_input() << " and had pre-existing input edge " - << inputs[edge->dst_input()]->src()->DebugString() << ":" - << inputs[edge->dst_input()]->dst()->DebugString(); - - inputs[edge->dst_input()] = edge; - } - } - original_def.clear_input(); - original_def.mutable_input()->Reserve(inputs.size()); - - for (size_t i = 0; i < inputs.size(); ++i) { - const Edge* edge = inputs[i]; - if (edge == nullptr) { - if (i < node->requested_inputs().size()) { - original_def.add_input(node->requested_inputs()[i]); - } else { - original_def.add_input(""); - } - } else { - const Node* src = edge->src(); - if (!src->IsOp()) continue; - AddInput(&original_def, src->name(), edge->src_output()); - } - } - // ...end code copied and pasted (and modified) from graph.cc - - auto node_def = - NGraphClusterManager::GetClusterGraph(cluster_idx)->add_node(); - cluster_indices_for_this_graph.insert(cluster_idx); - *node_def = original_def; - - for (auto& input : *(node_def->mutable_input())) { - TensorId tensor_id = ParseTensorName(input); - - string tensor_name(tensor_id.first); - auto it = input_rename_map.find( - std::make_tuple(cluster_idx, tensor_name, tensor_id.second)); - - if (it != input_rename_map.end()) { - input = it->second; - } - } - } - // Pass 6: Remove clustered nodes from the graph. std::vector nodes_to_remove; for (auto node : graph->op_nodes()) { @@ -529,382 +818,186 @@ Status EncapsulateClusters( subgraph, strings::StrCat("ngraph_cluster_", to_string(cluster_idx)), fdef)); } + rewrite_done = true; + return Status::OK(); +} - // Pass 8: - bool aot_requested; - set performed_aot_on_enc; - std::set>> node_shapes_hints_sets; - std::tie(aot_requested, node_shapes_hints_sets) = aot_info; - if (aot_requested) { - NGRAPH_VLOG(3) << "AOT requested"; - if (!ngraph_tf_is_grappler_enabled()) { - return errors::Internal( - "AOT requested for non grappler build. Please use grappler build if " - "AOT is required"); - } - string input_node_type = "Placeholder"; - // In case of grappler, we have Placeholder, which might contain shape info, - // so it is possible we can aot without any provided shapes - // in normal pass its args. unless shapes are provided there is no chance of - // reading shapes from args. +Status Encapsulator::GetNewClusterIDs(set& result) { + if (!analysis_done) { + return errors::Internal( + "In Encapsulator, called GetNewClusterIDs without calling " + "AnalysisPass"); + } + result.clear(); + for (auto it = device_name_map.begin(); it != device_name_map.end(); ++it) { + result.insert(it->first); + } + return Status::OK(); +} - auto get_shape_for_node_from_shape_hint = [](Node* node, - ShapeHintMap single_hint) { - auto find_itr = single_hint.find(node->name()); - return find_itr == single_hint.end() ? PartialShape() - : PartialShape(find_itr->second); - }; - - auto hint_as_string = [](ShapeHintMap single_hint) { - string hint_str; - for (auto itr_node : single_hint) { - hint_str += - ((itr_node.first) + ":[" + ng::join(itr_node.second) + "],"); - } - return hint_str; - }; +std::string HintAsString(ShapeHintMap single_hint) { + string hint_str; + for (auto itr_node : single_hint) { + hint_str += ((itr_node.first) + ":[" + ng::join(itr_node.second) + "],"); + } + return hint_str; +} - std::map> inputs_node_shapes_for_compilation; - // map between node name and the PartialShape it contains - std::map node_partial_shape_map; - // This is a map of placeholder names and the shapes we can infer from them - std::map> shape_from_placeholders_as_hints; - for (auto node : graph->op_nodes()) { - if (node->type_string() == input_node_type) { - NGRAPH_VLOG(5) << "Checking input for AOT: " << node->name() << "(" - << node->type_string() - << "): " << node->attrs().SummarizeNode(); - // TODO: need to confirm if its _output_shapes or shape - auto shape_field = node->attrs().Find("_output_shapes"); - if (shape_field == nullptr) { - shape_field = node->attrs().Find("shape"); - } - // It seems that _output_shapes is not found and hence the shape is - // inferred only from the hints. however if "shape" is present, it is - // empty, and in that case the empty shape and the rank!=0 hint fuse - // to give an invalid shape according to our current logic. have to - // modify that - PartialShape partial_shape_from_node; - if (shape_field != nullptr) { - // Get shape from the node - partial_shape_from_node = PartialShape(shape_field->shape()); - } - NGRAPH_VLOG(5) << "For node " << node->name() - << " got shape from nose: " - << partial_shape_from_node.to_string(); - node_partial_shape_map.insert({node->name(), partial_shape_from_node}); - shape_from_placeholders_as_hints.insert( - {node->name(), partial_shape_from_node.get_shape_vector()}); +PartialShape CombineNodeInfoAndHint(Node* node, + PartialShape partial_shape_from_node, + const ShapeHintMap& single_hint) { + auto get_shape_for_node_from_shape_hint = [](Node* node, + ShapeHintMap single_hint) { + auto find_itr = single_hint.find(node->name()); + return find_itr == single_hint.end() ? PartialShape() + : PartialShape(find_itr->second); + }; + + PartialShape shape_hint_for_node = + get_shape_for_node_from_shape_hint(node, single_hint); + + // If a shape has been found in the input node, match with + // shape_hints if they exist + PartialShape combined_shape_info; + if (shape_hint_for_node.is_valid()) { + NGRAPH_VLOG(5) << "For node " << node->name() << " shape hint (", + HintAsString(single_hint), + ") for node is valid and is: " + shape_hint_for_node.to_string(); + if (partial_shape_from_node.is_valid()) { + NGRAPH_VLOG(5) << "Partial shape from node is also valid. So " + "will attempt to concretize if possible"; + if (partial_shape_from_node.size() == 0) { + // TODO: revisit this if-else + NGRAPH_VLOG(5) << "Partial shape from node is empty, so will " + "use shape from hint"; + combined_shape_info = shape_hint_for_node; + } else { + NGRAPH_VLOG(5) << "Concretizing shape " + + partial_shape_from_node.to_string() + + "from node with hint for node, " + + shape_hint_for_node.to_string(); + partial_shape_from_node.concretize(shape_hint_for_node); + combined_shape_info = partial_shape_from_node; } + } else { + NGRAPH_VLOG(5) << "Partial shape from node is invalid. So using " + "hint for the node as shape"; + combined_shape_info = shape_hint_for_node; } - - // If no shape hints are provided but the placeholders contain complete - // shape, then we still need to enter the for loop below to compute AOT. - // Hence adding the shapes from placeholders as hints. - if (node_shapes_hints_sets.size() == 0) { - NGRAPH_VLOG(5) << "Using shapes from placeholders as hint"; - node_shapes_hints_sets.insert(shape_from_placeholders_as_hints); + } else { + NGRAPH_VLOG(5) << "For node " << node->name() + << " shape hint (" + HintAsString(single_hint) + + ") for node is invalid"; + if (partial_shape_from_node.is_valid()) { + // No shape hints found. But the node itself has some shape info + NGRAPH_VLOG(5) << "Partial shape from node is valid and is: " + + partial_shape_from_node.to_string(); + combined_shape_info = partial_shape_from_node; + } else { + NGRAPH_VLOG(5) << "Partial shape from node is invalid"; + combined_shape_info = PartialShape(); } - // TODO: .....CHECK ABOVE IF - - // Iterate over each shape hint and see if they can be used - for (ShapeHintMap single_hint : node_shapes_hints_sets) { - // A boolean to determine if we can AOT for this single_hint - bool can_aot = true; - - for (auto itr_single_hint : single_hint) { - if (shape_from_placeholders_as_hints.find(itr_single_hint.first) == - shape_from_placeholders_as_hints.end()) { - return errors::Internal("Passed hint for node ", - itr_single_hint.first, - " but there is no input with that name"); - } - } - - for (auto node : graph->op_nodes()) { - if (node->type_string() == input_node_type) { - PartialShape partial_shape_from_node = - node_partial_shape_map.at(node->name()); - - PartialShape shape_hint_for_node = - get_shape_for_node_from_shape_hint(node, single_hint); - - // If a shape has been found in the input node, match with - // shape_hints if they exist - PartialShape combined_shape_info; - if (shape_hint_for_node.is_valid()) { - NGRAPH_VLOG(5) << "For node " << node->name() << " shape hint (", - hint_as_string(single_hint), - ") for node is valid and is: " + - shape_hint_for_node.to_string(); - if (partial_shape_from_node.is_valid()) { - NGRAPH_VLOG(5) << "Partial shape from node is also valid. So " - "will attempt to concretize if possible"; - if (partial_shape_from_node.size() == 0) { - // TODO: revisit this if-else - NGRAPH_VLOG(5) << "Partial shape from node is empty, so will " - "use shape from hint"; - combined_shape_info = shape_hint_for_node; - } else { - NGRAPH_VLOG(5) << "Concretizing shape " + - partial_shape_from_node.to_string() + - "from node with hint for node, " + - shape_hint_for_node.to_string(); - partial_shape_from_node.concretize(shape_hint_for_node); - combined_shape_info = partial_shape_from_node; - } - } else { - NGRAPH_VLOG(5) << "Partial shape from node is invalid. So using " - "hint for the node as shape"; - combined_shape_info = shape_hint_for_node; - } - } else { - NGRAPH_VLOG(5) << "For node " << node->name() - << " shape hint (" + hint_as_string(single_hint) + - ") for node is invalid"; - if (partial_shape_from_node.is_valid()) { - // No shape hints found. But the node itself has some shape info - NGRAPH_VLOG(5) << "Partial shape from node is valid and is: " + - partial_shape_from_node.to_string(); - combined_shape_info = partial_shape_from_node; - } else { - NGRAPH_VLOG(5) << "Partial shape from node is invalid"; - combined_shape_info = PartialShape(); - } - } - - can_aot = combined_shape_info.is_valid() && - combined_shape_info.is_concrete(); - if (can_aot) { - inputs_node_shapes_for_compilation[node->name()] = - combined_shape_info.get_shape_vector(); - } else { - // TODO: necessarily break? Maybe some things can be AOT, others - // maybe not - string fail_reason = - (combined_shape_info.is_valid() - ? (node->name() + " could not be concretized") - : "it is invalid for " + node->name()); - return errors::Internal("Cannot AOT using this hint (", - hint_as_string(single_hint), ") as ", - fail_reason); - break; - } - } // end of if (node->type_string() == input_node_type) - } // End of for loop that goes through all nodes - - // Did we manage to concretize all input shapes? - for (auto itr : node_partial_shape_map) { // iterate over all inputs - if (inputs_node_shapes_for_compilation.find(itr.first) == - inputs_node_shapes_for_compilation.end()) { - can_aot = false; - // TODO: print "this" hint - return errors::Internal("Cannot AOT using this hint (", - hint_as_string(single_hint), ") for ", - (itr.first), " was not concretized"); - } - } + } + return combined_shape_info; +} - if (!can_aot) { - return errors::Internal("AOT requested, but could not perform AOT"); +std::map GetShapesFromTFInputnodes( + Graph* graph, const string& input_node_type) { + // map between node name and the PartialShape it contains + std::map node_partial_shape_map; + for (auto node : graph->op_nodes()) { + if (node->type_string() == input_node_type) { + NGRAPH_VLOG(5) << "Checking input for AOT: " << node->name() << "(" + << node->type_string() + << "): " << node->attrs().SummarizeNode(); + // TODO: need to confirm if its _output_shapes or shape + auto shape_field = node->attrs().Find("_output_shapes"); + if (shape_field == nullptr) { + shape_field = node->attrs().Find("shape"); } - for (auto node : graph->op_nodes()) { - if (node->type_string() == "NGraphEncapsulate") { - // Check inputs of the encapsulates. They can only be fed by fully - // concrete shapes (after going through the shape hints) or consts - std::vector st_inputs; - GetStaticInputs(node, &st_inputs); - // Current assumption is that only encapsulates without static - // inputs are AOT - if (st_inputs.size() != 0) { - return errors::Internal( - "AOT requested. Found an encapsulate with static inputs, but " - "that is not supported"); - } - - std::vector input_shapes; - std::stringstream signature_ss; - for (auto in_node : node->in_nodes()) { - if (!in_node->IsSource()) { - auto itr_shape = - inputs_node_shapes_for_compilation.find(in_node->name()); - if (itr_shape == inputs_node_shapes_for_compilation.end()) { - // TODO: this error could potentially happen due to 2 reasons: - // 1. Enough valid shape hints were not passed - // 2. It is an encapsulate that has atleast 1 input fed by a - // non-placeholder (like another TF node or another - // encapsulate) - // Later provide more explicit debug message (reason 1 or 2 or - // anything else) - return errors::Internal( - "AOT requested. Found an encapsulate that has a " - "non-concrete input"); - } else { - std::vector converted_to_int64(itr_shape->second.begin(), - itr_shape->second.end()); - input_shapes.push_back(TensorShape(converted_to_int64)); - for (auto itr1 : itr_shape->second) { - signature_ss << itr1 << ","; - } - signature_ss << ";"; - } - } - } - - signature_ss << "/"; - string signature = signature_ss.str(); - NGRAPH_VLOG(3) << "Performing AOT for " << node->name() - << " for signature = " << signature << "\n"; - - std::vector static_input_map; - std::shared_ptr ng_function; - int cluster_idx; - TF_RETURN_IF_ERROR( - GetNodeAttr(node->attrs(), "ngraph_cluster", &cluster_idx)); - GraphDef* gdef_for_current_encapsulate; - gdef_for_current_encapsulate = - NGraphClusterManager::GetClusterGraph(cluster_idx); - GraphConstructorOptions opts; - opts.allow_internal_ops = true; - Graph graph_for_current_encapsulate(OpRegistry::Global()); - TF_RETURN_IF_ERROR( - ConvertGraphDefToGraph(opts, *gdef_for_current_encapsulate, - &graph_for_current_encapsulate)); - - // get backend. - // TODO: Note that this is code duplication of some stuff present - // in NGraphEncapsulateOp - // Once NGraphEncapsulateOp is refactored, this code should be - // removed and a common function should be used - - // TODO: these sections can be hoisted out of the main loop - std::string backend_name; - TF_RETURN_IF_ERROR( - GetNodeAttr(node->attrs(), "ngraph_backend", &backend_name)); - std::string device_id; - TF_RETURN_IF_ERROR( - GetNodeAttr(node->attrs(), "ngraph_device_id", &device_id)); - - string op_backend_name; - try { - op_backend_name = BackendManager::GetBackendCreationString( - backend_name, device_id); - } catch (const std::exception& exp) { - return errors::Internal( - "Caught exception while creating backend string ", exp.what(), - "\n"); - } - TF_RETURN_IF_ERROR(BackendManager::CreateBackend( - op_backend_name)); // Created a backend here. must free it - // TranslateGraph must be called AFTER CreateBackend because some TF - // ops like CNMS and gather use backend specific nodes - TF_RETURN_IF_ERROR(Builder::TranslateGraph( - input_shapes, static_input_map, &graph_for_current_encapsulate, - ng_function)); - int json_indentation = 4; - string serialized_ngfunc( - ngraph::serialize(ng_function, json_indentation)); - std::unordered_map additional_attribute_map; - for (auto itr : node->attrs()) { - // Find the optional attributes to be sent to the backend. - // The optional attributes have '_ngraph_' appended to the start - // so we need to get rid of that and only send the remaining - // string - // since the backend will only look for that. - // '_ngraph_' is only appended for the bridge. - // For e.g. _ngraph_ice_cores --> ice_cores - if (itr.first.find("_ngraph_") != std::string::npos) { - // leave out _ngraph_aot_requested - if (itr.first.find("_ngraph_aot_requested") == - std::string::npos) { - additional_attribute_map.insert( - {itr.first.substr(strlen("_ngraph_")), itr.second.s()}); - } - } - } - BackendManager::SetConfig(op_backend_name, additional_attribute_map); - ng::runtime::Backend* op_backend = nullptr; - try { - op_backend = BackendManager::GetBackend(op_backend_name); - } catch (const std::out_of_range& e) { - NGRAPH_VLOG(5) << "Exception: " << e.what(); - BackendManager::ReleaseBackend(op_backend_name); - throw; - } - BackendManager::LockBackend(op_backend_name); - std::shared_ptr ng_exec; - try { - ng_exec = op_backend->compile(ng_function); - } catch (...) { - BackendManager::UnlockBackend(op_backend_name); - Status st = - NgraphSerialize("tf_function_error_aot.json", ng_function); - BackendManager::ReleaseBackend(op_backend_name); - return errors::Internal( - "Failed to compile ng_function for AOT.", - (st.ok() ? "" - : " Failed to serialize as well with error: " + - st.error_message())); - } - BackendManager::UnlockBackend(op_backend_name); - BackendManager::ReleaseBackend(op_backend_name); - - stringstream exec_dump; - ng_exec->save(exec_dump); - // ng function attached as debugging information - node->AddAttr("_ngraph_aot_ngfunction_" + signature, - serialized_ngfunc); - // Compute will use this ngexec - node->AddAttr("_ngraph_aot_ngexec_" + signature, exec_dump.str()); - // We do not need to add "_ngraph_aot_requested" attribute since it - // already is already present in device_config and inserted into the - // currently created NGraphEncapsulate - // TODO: create a separate namespace of node attributes for backend - // and for bridge - performed_aot_on_enc.insert(node->name()); - NGRAPH_VLOG(5) << "Performed AOT on " << node->name(); - } + // It seems that _output_shapes is not found and hence the shape is + // inferred only from the hints. however if "shape" is present, it is + // empty, and in that case the empty shape and the rank!=0 hint fuse + // to give an invalid shape according to our current logic. have to + // modify that + PartialShape partial_shape_from_node; + if (shape_field != nullptr) { + // Get shape from the node + partial_shape_from_node = PartialShape(shape_field->shape()); } - } // end of for (ShapeHintMap single_hint : node_shapes_hints_sets) + NGRAPH_VLOG(5) << "For node " << node->name() << " got shape from node: " + << partial_shape_from_node.to_string(); + node_partial_shape_map.insert({node->name(), partial_shape_from_node}); + } + } + return node_partial_shape_map; +} - // In the end assert that all encapsulates have performed AOT - for (auto node : graph->op_nodes()) { - if (node->type_string() == "NGraphEncapsulate") { - if (performed_aot_on_enc.find(node->name()) == - performed_aot_on_enc.end()) { - return errors::Internal("Requested AOT, but did not perform AOT on ", - node->name()); +Status PerformTranslation(Node* node, const std::map>& + inputs_node_shapes_for_compilation, + string& signature, + std::shared_ptr& ng_function) { + if (node->type_string() != "NGraphEncapsulate") { + return errors::Internal( + "This function should only be called on an NGraphEncapsulate, but was " + "called on ", + node->name(), " which is of type ", node->type_string()); + } + std::vector input_shapes; + std::stringstream signature_ss; + for (auto in_node : node->in_nodes()) { + if (!in_node->IsSource()) { + auto itr_shape = inputs_node_shapes_for_compilation.find(in_node->name()); + if (itr_shape == inputs_node_shapes_for_compilation.end()) { + // TODO: this error could potentially happen due to 2 reasons: + // 1. Enough valid shape hints were not passed + // 2. It is an encapsulate that has atleast 1 input fed by a + // non-placeholder (like another TF node or another + // encapsulate) + // Later provide more explicit debug message (reason 1 or 2 or + // anything else) + return errors::Internal( + "AOT requested. Found an encapsulate that has a " + "non-concrete input"); + } else { + std::vector converted_to_int64(itr_shape->second.begin(), + itr_shape->second.end()); + input_shapes.push_back(TensorShape(converted_to_int64)); + for (auto itr1 : itr_shape->second) { + signature_ss << itr1 << ","; } + signature_ss << ";"; } } - } // end of if (aot_requested) - - // Pass 9 (optional, only run if environment variable - // NGRAPH_TF_DUMP_CLUSTERS is set): validate the graph def, and - // make sure we can construct a graph from it. - if (std::getenv("NGRAPH_TF_DUMP_CLUSTERS")) { - for (auto& kv : device_name_map) { - int cluster_idx = kv.first; - TF_RETURN_IF_ERROR(graph::ValidateGraphDef( - *NGraphClusterManager::GetClusterGraph(cluster_idx), - *OpRegistry::Global())); - - Graph g(OpRegistry::Global()); - GraphConstructorOptions opts; - opts.allow_internal_ops = true; - TF_RETURN_IF_ERROR(ConvertGraphDefToGraph( - opts, *NGraphClusterManager::GetClusterGraph(cluster_idx), &g)); - - std::stringstream ss; - ss << "ngraph_cluster_" << cluster_idx; - std::string filename_prefix = ss.str(); - - GraphToPbTextFile(&g, filename_prefix + ".pbtxt"); - GraphToDotFile(&g, filename_prefix + ".dot", - "nGraph Cluster Dump: " + filename_prefix); - } } + signature_ss << "/"; + signature = signature_ss.str(); + NGRAPH_VLOG(3) << "Performing AOT for " << node->name() + << " for signature = " << signature << "\n"; + std::vector static_input_map; + + int cluster_idx; + TF_RETURN_IF_ERROR( + GetNodeAttr(node->attrs(), "ngraph_cluster", &cluster_idx)); + GraphDef* gdef_for_current_encapsulate; + gdef_for_current_encapsulate = + NGraphClusterManager::GetClusterGraph(cluster_idx); + GraphConstructorOptions opts; + opts.allow_internal_ops = true; + Graph graph_for_current_encapsulate(OpRegistry::Global()); + TF_RETURN_IF_ERROR(ConvertGraphDefToGraph(opts, *gdef_for_current_encapsulate, + &graph_for_current_encapsulate)); + + // TODO: Note that this is code duplication of some stuff present + // in NGraphEncapsulateOp + // Once NGraphEncapsulateOp is refactored, this code should be + // removed and a common function should be used + + TF_RETURN_IF_ERROR(Builder::TranslateGraph(input_shapes, static_input_map, + &graph_for_current_encapsulate, + ng_function)); + return Status::OK(); } diff --git a/ngraph_bridge/ngraph_encapsulate_clusters.h b/ngraph_bridge/ngraph_encapsulate_clusters.h index c2aea6111..a4fe2adec 100644 --- a/ngraph_bridge/ngraph_encapsulate_clusters.h +++ b/ngraph_bridge/ngraph_encapsulate_clusters.h @@ -26,6 +26,10 @@ #include #include "tensorflow/core/graph/graph.h" +#include "ngraph/ngraph.hpp" + +#include "ngraph_bridge/ngraph_partial_shapes.h" + namespace tensorflow { namespace ngraph_bridge { @@ -35,10 +39,110 @@ typedef std::map> ShapeHintMap; // the integer represent AOT level requested. typedef std::pair> AOTInfo; +// TODO: an optimization would be to separate the analysis and rewriting passes +// cleanly, so that analysis pass is run in mark_for_clustering, and its +// information is reused here instead of recalculating +// To do that an Encapsulator object with AnalysisPass run can be created in +// MarkForClustering, and that can be passed to EncapsulateClusters + +/// Takes a TF graph where ngraph_cluster attributes has been marked in a +/// preceeding pass (assign_clusters), then replaces TF subgraphs and inserts +/// encapsulate ops in their place. Optionally can perform ahead of time +/// compilation. Status EncapsulateClusters( Graph* graph, int graph_id, FunctionDefLibrary* fdeflib, - std::unordered_map device_config, - AOTInfo aot_info); + const std::unordered_map& device_config, + const AOTInfo& aot_info); + +// TODO Encapsulator is dependent on ClusterManager. They could be made +// independent. + +// A class to perform analysis (identify subgraphs) +// and rewriting (create encapsulates and splice them in) +// Order of calling: construction -> AnalysisPass -> RewritePass +// | +// v +// NewClusterIds +// Any other order of calling will generate errors +// Cannot be copied/moved or reset +class Encapsulator { + public: + Encapsulator(Graph* g); + // Populate ClusterManager with the subgraphs for each potential encapsulate + Status AnalysisPass(); + // Perform the actual graph surgery + Status RewritePass( + FunctionDefLibrary* fdeflib, int graph_id, + const std::unordered_map& device_config); + // Returns the newly created cluster ids after AnalysisPass is done + // Needed because ClusterManager (CM) might have contained old stuff, + // so it might not be possible to query the CM itself to get this + Status GetNewClusterIDs(std::set& result); + + Encapsulator(const Encapsulator&) = delete; + Encapsulator(Encapsulator&&) = delete; + Encapsulator& operator=(const Encapsulator&) = delete; + Encapsulator& operator=(Encapsulator&&) = delete; + + private: + Graph* graph; + // boolean to indicate if analysis has been done + // If not rewritepass should not be called + bool analysis_done; + // boolean to indicate that rewrite is done; + bool rewrite_done; + // A map from cluster indices to the expected device name for nodes + // in that cluster. + std::map device_name_map; + + // We *should* eventually have a way of monitoring the device and the backend + // together + std::map backend_name_map; + + // As we build the graph we will be tracking the.. TODO(amprocte): finish + // this comment. + std::map, std::tuple> output_remap_map; + std::map, int> input_remap_map; + std::map, string> input_rename_map; + + // A map from cluster indices to a vector of input data types. + std::map>> cluster_input_map; + // A map from cluster indices to a vector of output data types. + std::map> cluster_output_dt_map; + + // A map from cluster indices to corresponding NGraphEncapsulate nodes. + std::map cluster_node_map; + + std::set cluster_indices_for_this_graph; + + static void AddInput(NodeDef* dst, StringPiece src_name, int src_slot); +}; + +// Translates TF subgraph to ng function then compiles it +Status PerformAOTOnEncapsulates(Graph* graph, const AOTInfo& aot_info); + +std::string HintAsString(ShapeHintMap single_hint); + +// Given a node, partialshape info from TF (present in the .pb itself) and a +// shape hint, combine all that information +PartialShape CombineNodeInfoAndHint(Node* node, + PartialShape partial_shape_from_node, + const ShapeHintMap& single_hint); + +// Given a TF graph, it scans it for inputs and finds what TF is saying about +// their shapes (in the .pb itself) +// Creates a map between input node names and PartialShape information we get +// from the TF graph +std::map GetShapesFromTFInputnodes( + Graph* graph, const string& input_node_type); + +// Given an encapsulate node, and the input shapes, +// performs TranslateGraph and returns an ng function and a signature +Status PerformTranslation(Node* node, + const std::map>& + inputs_node_shapes_for_compilation, + std::string& signature, + std::shared_ptr& ng_function); } // namespace ngraph_bridge } // namespace tensorflow diff --git a/test/graph_rewrites/encapsulate_clusters_test.cc b/test/graph_rewrites/encapsulate_clusters_test.cc index 061c0bc35..f787598b5 100644 --- a/test/graph_rewrites/encapsulate_clusters_test.cc +++ b/test/graph_rewrites/encapsulate_clusters_test.cc @@ -32,6 +32,196 @@ namespace ngraph_bridge { namespace testing { +// Test that calls the functions of encapsulator in the wrong order +// Non-OK statuses are expected +TEST(EncapsulateClusters, EncapsulatorFail) { + Encapsulator enc{nullptr}; + std::unordered_map device_config; + ASSERT_NOT_OK(enc.RewritePass(nullptr, 0, device_config)); + set result; + ASSERT_NOT_OK(enc.GetNewClusterIDs(result)); +} + +// abs +// ^ +// | +// const(0) ---> add(1) <---const(1) +TEST(EncapsulateClusters, EncapsulatorPass) { + auto num_graphs_in_cluster_manager = []() { + int num = 0; + while (true) { + if (NGraphClusterManager::GetClusterGraph(num) == nullptr) { + break; + } else { + num++; + } + } + return num; + }; + NGraphClusterManager::EvictAllClusters(); + ASSERT_EQ(num_graphs_in_cluster_manager(), 0); + Graph g(OpRegistry::Global()); + + Tensor t_input_0(DT_FLOAT, TensorShape{2, 3}); + Tensor t_input_1(DT_INT32, TensorShape{2}); + t_input_1.flat().data()[0] = 3; + t_input_1.flat().data()[1] = 2; + + int cluster_idx_0 = NGraphClusterManager::NewCluster(); + ; + + Node* node1; + ASSERT_OK(NodeBuilder("node1", "Const") + .Attr("dtype", DT_FLOAT) + .Attr("value", t_input_0) + .Attr("_ngraph_marked_for_clustering", true) + .Attr("_ngraph_cluster", cluster_idx_0) + .Attr("_ngraph_backend", "CPU") + .Finalize(&g, &node1)); + + int cluster_idx_1 = NGraphClusterManager::NewCluster(); + ASSERT_EQ(num_graphs_in_cluster_manager(), 2); + + Node* node2; + ASSERT_OK(NodeBuilder("node2", "Const") + .Attr("dtype", DT_FLOAT) + .Attr("value", t_input_1) + .Attr("_ngraph_marked_for_clustering", true) + .Attr("_ngraph_cluster", cluster_idx_1) + .Attr("_ngraph_backend", "CPU") + .Finalize(&g, &node2)); + + Node* node3; + ASSERT_OK(NodeBuilder("node3", "Add") + .Input(node1, 0) + .Input(node2, 0) + .Attr("T", DT_FLOAT) + .Attr("_ngraph_marked_for_clustering", true) + .Attr("_ngraph_cluster", cluster_idx_1) + .Attr("_ngraph_backend", "CPU") + .Finalize(&g, &node3)); + + Node* node4; + ASSERT_OK(NodeBuilder("node4", "Abs") + .Input(node3, 0) + .Attr("T", DT_FLOAT) + .Finalize(&g, &node4)); + + Node* source = g.source_node(); + Node* sink = g.sink_node(); + g.AddEdge(source, Graph::kControlSlot, node1, Graph::kControlSlot); + g.AddEdge(source, Graph::kControlSlot, node2, Graph::kControlSlot); + g.AddEdge(node4, Graph::kControlSlot, sink, Graph::kControlSlot); + + ASSERT_EQ(g.num_edges(), 7); + ASSERT_EQ(g.num_op_nodes(), 4); + ASSERT_EQ(g.num_nodes(), 6); + + Encapsulator enc(&g); + + // Initially ClusterManager is empty + for (int i = 0; i < 2; i++) { + ASSERT_EQ(NGraphClusterManager::GetClusterGraph(i)->node_size(), 0); + } + ASSERT_OK(enc.AnalysisPass()); + // After AnalysisPass ClusterManager is populated + // const and retval + ASSERT_EQ(NGraphClusterManager::GetClusterGraph(0)->node_size(), 2); + // arg, const, add and retval + ASSERT_EQ(NGraphClusterManager::GetClusterGraph(1)->node_size(), 4); + // But the graph structure stays same. No rewriting yet + ASSERT_EQ(g.num_edges(), 7); + ASSERT_EQ(g.num_op_nodes(), 4); + ASSERT_EQ(g.num_nodes(), 6); + + set newly_created_cluster_ids; + ASSERT_OK(enc.GetNewClusterIDs(newly_created_cluster_ids)); + set expected{0, 1}; + ASSERT_EQ(newly_created_cluster_ids, expected); + + auto subgraph_0 = NGraphClusterManager::GetClusterGraph(0); + auto subgraph_1 = NGraphClusterManager::GetClusterGraph(1); + auto subgraph_2 = NGraphClusterManager::GetClusterGraph(2); + // Assert that there are only 2 subgraphs + ASSERT_EQ(subgraph_2, nullptr); + + int num_encapsulates = 0; + int num_tf_nodes = 0; + + // count number of nodes and encapsulates + auto node_counter = [](Graph* g) { + int num_encapsulates = 0, num_tf_nodes = 0; + for (auto itr : g->nodes()) { + auto node_type = itr->type_string(); + num_encapsulates += (node_type == "NGraphEncapsulate" ? 1 : 0); + num_tf_nodes += + ((node_type == "Add" || node_type == "Const" || node_type == "Abs") + ? 1 + : 0); + } + return make_pair(num_encapsulates, num_tf_nodes); + }; + + std::tie(num_encapsulates, num_tf_nodes) = node_counter(&g); + + // All the Add/Const/Abs nodes are left in the graph, since it is an analysis + // pass + ASSERT_EQ(num_tf_nodes, 4); + + // Number of encapsulates == number of functions == 0 + ASSERT_EQ(num_encapsulates, 0); + + // In analysis pass cluster manager should be populated with the subgraphs + // Now analyse subgraph_0 and subgraph_1, which we got from ClusterManager + ASSERT_EQ(subgraph_0->node_size(), 2); + ASSERT_EQ(subgraph_1->node_size(), 4); + + // helper function to get nodes and their types from a graphdef + auto get_node_name_and_types = + [](GraphDef* subgraph) -> set> { + set> node_info; + for (int i = 0; i < subgraph->node_size(); i++) { + node_info.insert({subgraph->node(i).name(), subgraph->node(i).op()}); + } + return node_info; + }; + + ASSERT_EQ(get_node_name_and_types(subgraph_0), + (set>{{"ngraph_output_0", "_Retval"}, + {"node1", "Const"}})); + ASSERT_EQ(get_node_name_and_types(subgraph_1), + (set>{{"ngraph_input_0", "_Arg"}, + {"ngraph_output_0", "_Retval"}, + {"node2", "Const"}, + {"node3", "Add"}})); + + // Now perform the actual rewrite + std::unordered_map config_map; + config_map["ngraph_device_id"] = ""; + FunctionDefLibrary* fdeflib_new = new FunctionDefLibrary(); + ASSERT_OK(enc.RewritePass(fdeflib_new, 0, config_map)); + + std::tie(num_encapsulates, num_tf_nodes) = node_counter(&g); + ASSERT_EQ(num_tf_nodes, 1); // Only Abs is left + ASSERT_EQ(num_encapsulates, 2); + // Number of encapsulates == number of functions + ASSERT_EQ(num_encapsulates, fdeflib_new->function_size()); + + // After RewritePass, the number of clusters is still 2 and it contains + // populated graphdefs + ASSERT_EQ(num_graphs_in_cluster_manager(), 2); + ASSERT_EQ(NGraphClusterManager::GetClusterGraph(0)->node_size(), 2); + ASSERT_EQ(NGraphClusterManager::GetClusterGraph(1)->node_size(), 4); + + // The graph structure should have changed after RewritePass + ASSERT_EQ(g.num_edges(), 6); + ASSERT_EQ(g.num_op_nodes(), 3); + ASSERT_EQ(g.num_nodes(), 5); + + free(fdeflib_new); +} + +// const(0) ---> add(0) <---const(0) TEST(EncapsulateClusters, PopulateLibrary) { NGraphClusterManager::EvictAllClusters(); Graph g(OpRegistry::Global()); @@ -81,8 +271,15 @@ TEST(EncapsulateClusters, PopulateLibrary) { std::unordered_map config_map; config_map["ngraph_device_id"] = ""; + ASSERT_EQ(g.num_edges(), 6); + ASSERT_EQ(g.num_op_nodes(), 3); + ASSERT_EQ(g.num_nodes(), 5); ASSERT_OK(EncapsulateClusters(&g, 0, fdeflib_new, config_map, {0, {}})); + ASSERT_EQ(g.num_edges(), 3); + ASSERT_EQ(g.num_op_nodes(), 1); + ASSERT_EQ(g.num_nodes(), 3); + int num_encapsulates = 0; int num_tf_nodes = 0; for (auto itr : g.nodes()) { @@ -646,4 +843,4 @@ TEST(EncapsulateClusters, AOT4) { // should create 1 exec only). } } -} \ No newline at end of file +} From d20cd252c1e0607936e76503e458828e2145eabd Mon Sep 17 00:00:00 2001 From: Abolfazl Shahbazi <12436063+ashahba@users.noreply.github.com> Date: Tue, 21 Jan 2020 16:00:23 -0800 Subject: [PATCH 4/5] Peg pip version to 19.3.1 (#450) * Peg pip version to 19.3.1 * Pep pip version prior to install 'ngraph_bridge' * Pep pip to 19.3.1 after TensorFlow install --- test/ci/buildkite/ngtf-cpu_centos-grappler.yaml | 1 + test/ci/buildkite/ngtf-cpu_centos.yaml | 1 + test/ci/buildkite/ngtf-cpu_macos.yaml | 2 +- test/ci/buildkite/ngtf-cpu_model-test.yaml | 1 + test/ci/buildkite/ngtf-cpu_ubuntu-bin-build.yaml | 1 + test/ci/buildkite/ngtf-cpu_ubuntu-external-fork.yaml | 1 + test/ci/buildkite/ngtf-cpu_ubuntu-variables.yaml | 1 + test/ci/buildkite/ngtf-cpu_ubuntu.yaml | 1 + test/ci/buildkite/ngtf-cpu_ubuntu_18_04.yaml | 1 + test/ci/buildkite/ngtf-gpu_ubuntu.yaml | 1 + tools/build_utils.py | 2 +- 11 files changed, 11 insertions(+), 2 deletions(-) diff --git a/test/ci/buildkite/ngtf-cpu_centos-grappler.yaml b/test/ci/buildkite/ngtf-cpu_centos-grappler.yaml index 15d2f3e62..9d768f4d9 100644 --- a/test/ci/buildkite/ngtf-cpu_centos-grappler.yaml +++ b/test/ci/buildkite/ngtf-cpu_centos-grappler.yaml @@ -56,6 +56,7 @@ source /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install psutil && pip install -U \ /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/tensorflow/tensorflow-1.14.0-cp36-cp36m-linux_x86_64.whl + pip install -U pip==19.3.1 pip install -U /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl label: ":gear: Install" diff --git a/test/ci/buildkite/ngtf-cpu_centos.yaml b/test/ci/buildkite/ngtf-cpu_centos.yaml index a3db778bf..89fc89c47 100644 --- a/test/ci/buildkite/ngtf-cpu_centos.yaml +++ b/test/ci/buildkite/ngtf-cpu_centos.yaml @@ -55,6 +55,7 @@ source /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install psutil && pip install -U \ /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/tensorflow/tensorflow-1.14.0-cp36-cp36m-linux_x86_64.whl + pip install -U pip==19.3.1 pip install -U /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl label: ":gear: Install" diff --git a/test/ci/buildkite/ngtf-cpu_macos.yaml b/test/ci/buildkite/ngtf-cpu_macos.yaml index ba7c15e11..254d7785d 100644 --- a/test/ci/buildkite/ngtf-cpu_macos.yaml +++ b/test/ci/buildkite/ngtf-cpu_macos.yaml @@ -39,7 +39,7 @@ source /usr/local/var/buildkite-agent/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install -U \ /usr/local/var/buildkite-agent/tf_1.14.0_install/artifacts/tensorflow/tensorflow-1.14.0-cp37-cp37m-macosx_10_7_x86_64.whl - pip install psutil && \ + pip install psutil pip==19.3.1 && \ pip install -U /usr/local/var/buildkite-agent/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl label: ":gear: Install" diff --git a/test/ci/buildkite/ngtf-cpu_model-test.yaml b/test/ci/buildkite/ngtf-cpu_model-test.yaml index e2018f804..0377fabd0 100644 --- a/test/ci/buildkite/ngtf-cpu_model-test.yaml +++ b/test/ci/buildkite/ngtf-cpu_model-test.yaml @@ -26,6 +26,7 @@ source /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install psutil && pip install -U \ /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/tensorflow/tensorflow-1.14.0-cp36-cp36m-linux_x86_64.whl + pip install -U pip==19.3.1 pip install -U /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl label: ":gear: Install" diff --git a/test/ci/buildkite/ngtf-cpu_ubuntu-bin-build.yaml b/test/ci/buildkite/ngtf-cpu_ubuntu-bin-build.yaml index 94a9ddd58..6729c1fba 100644 --- a/test/ci/buildkite/ngtf-cpu_ubuntu-bin-build.yaml +++ b/test/ci/buildkite/ngtf-cpu_ubuntu-bin-build.yaml @@ -38,6 +38,7 @@ - command: | source /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install psutil + pip install -U pip==19.3.1 pip install -U /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl PYTHONPATH=`pwd` python3 test/ci/buildkite/test_runner.py \ --artifacts /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID --test_resnet diff --git a/test/ci/buildkite/ngtf-cpu_ubuntu-external-fork.yaml b/test/ci/buildkite/ngtf-cpu_ubuntu-external-fork.yaml index 7865cae5f..2372b7716 100644 --- a/test/ci/buildkite/ngtf-cpu_ubuntu-external-fork.yaml +++ b/test/ci/buildkite/ngtf-cpu_ubuntu-external-fork.yaml @@ -68,6 +68,7 @@ source /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install psutil && pip install -U \ /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/tensorflow/tensorflow-1.14.0-cp36-cp36m-linux_x86_64.whl + pip install -U pip==19.3.1 pip install -U /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl label: ":gear: Install" diff --git a/test/ci/buildkite/ngtf-cpu_ubuntu-variables.yaml b/test/ci/buildkite/ngtf-cpu_ubuntu-variables.yaml index d240feefa..7cfd40f65 100644 --- a/test/ci/buildkite/ngtf-cpu_ubuntu-variables.yaml +++ b/test/ci/buildkite/ngtf-cpu_ubuntu-variables.yaml @@ -54,6 +54,7 @@ source /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install psutil && pip install -U \ /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/tensorflow/tensorflow-1.14.0-cp36-cp36m-linux_x86_64.whl + pip install -U pip==19.3.1 pip install -U /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl label: ":gear: Install" diff --git a/test/ci/buildkite/ngtf-cpu_ubuntu.yaml b/test/ci/buildkite/ngtf-cpu_ubuntu.yaml index c590a4a99..ae2f82bfd 100644 --- a/test/ci/buildkite/ngtf-cpu_ubuntu.yaml +++ b/test/ci/buildkite/ngtf-cpu_ubuntu.yaml @@ -68,6 +68,7 @@ source /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install psutil && pip install -U \ /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/tensorflow/tensorflow-1.14.0-cp36-cp36m-linux_x86_64.whl + pip install -U pip==19.3.1 pip install -U /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl label: ":gear: Install" diff --git a/test/ci/buildkite/ngtf-cpu_ubuntu_18_04.yaml b/test/ci/buildkite/ngtf-cpu_ubuntu_18_04.yaml index f9a97d011..5bca0faa4 100644 --- a/test/ci/buildkite/ngtf-cpu_ubuntu_18_04.yaml +++ b/test/ci/buildkite/ngtf-cpu_ubuntu_18_04.yaml @@ -63,6 +63,7 @@ source /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install psutil && pip install -U \ /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/tensorflow/tensorflow-1.14.0-cp36-cp36m-linux_x86_64.whl + pip install -U pip==19.3.1 pip install -U /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl label: ":gear: Install" diff --git a/test/ci/buildkite/ngtf-gpu_ubuntu.yaml b/test/ci/buildkite/ngtf-gpu_ubuntu.yaml index 798cfd9ff..1f3dfd031 100644 --- a/test/ci/buildkite/ngtf-gpu_ubuntu.yaml +++ b/test/ci/buildkite/ngtf-gpu_ubuntu.yaml @@ -42,6 +42,7 @@ source /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/venv/bin/activate pip install psutil && pip install -U \ /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/tensorflow/tensorflow-1.14.0-cp??-cp??m-linux_x86_64.whl + pip install -U pip==19.3.1 pip install -U /localdisk/buildkite/artifacts/$BUILDKITE_BUILD_ID/ngraph_tensorflow_bridge-*.whl label: ":gear: Install" diff --git a/tools/build_utils.py b/tools/build_utils.py index 8aab3a43c..36c70ee72 100755 --- a/tools/build_utils.py +++ b/tools/build_utils.py @@ -148,7 +148,7 @@ def setup_venv(venv_dir): "pip", "install", "-U", - "pip", + "pip==19.3.1", "setuptools", "psutil", "six>=1.10.0", From a7db58c168c1eb20cb9551c7f7a031f58d5a2f17 Mon Sep 17 00:00:00 2001 From: Shrestha Malik Date: Tue, 21 Jan 2020 16:07:37 -0800 Subject: [PATCH 5/5] change version for release tag --- README.md | 2 +- ngraph_bridge/version.cc | 2 +- python/setup.in.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5923cb49..7db783454 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Once TensorFlow's dependencies are installed, clone the `ngraph-bridge` repo: git clone https://github.com/tensorflow/ngraph-bridge.git cd ngraph-bridge - git checkout v0.22.0-rc3 + git checkout v0.22.0-rc4 Run the following Python script to build TensorFlow, nGraph, and the bridge. Use Python 3.5: diff --git a/ngraph_bridge/version.cc b/ngraph_bridge/version.cc index 569391a99..bf79d92c5 100644 --- a/ngraph_bridge/version.cc +++ b/ngraph_bridge/version.cc @@ -32,7 +32,7 @@ // candidate such as v0.7.0-rc0 // The code in master will always have the last released version number // with a suffix of '-master' -#define NG_TF_VERSION_SUFFIX "-rc3" +#define NG_TF_VERSION_SUFFIX "-rc4" #define VERSION_STR_HELPER(x) #x #define VERSION_STR(x) VERSION_STR_HELPER(x) diff --git a/python/setup.in.py b/python/setup.in.py index 6e0a37428..9d4d713ee 100644 --- a/python/setup.in.py +++ b/python/setup.in.py @@ -59,7 +59,7 @@ def get_tag(self): setup( name='ngraph_tensorflow_bridge', - version='0.22.0rc3', + version='0.22.0rc4', description='Intel nGraph compiler and runtime for TensorFlow', long_description=long_description, long_description_content_type="text/markdown",