From 3199074ac786d655d4390b4dcd007a445534633e Mon Sep 17 00:00:00 2001 From: Edward Chen <18449977+edgchen1@users.noreply.github.com> Date: Mon, 14 Feb 2022 14:20:03 -0800 Subject: [PATCH] Update QDQ propagation transformer to insert QDQ nodes (#10487) Update QDQ propagation transformer to insert new QDQ nodes instead of moving the existing one. This creates a more consistent `DQ -> op -> Q` pattern for other components to recognize. Upgrade this transformer to a basic level optimization as it yields a valid ONNX graph. --- cmake/onnxruntime_optimizer.cmake | 2 + onnxruntime/core/graph/extended_graph_edge.h | 128 ++++++ .../core/optimizer/graph_transformer_utils.cc | 6 +- .../qdq_transformer/qdq_propagation.cc | 389 ++++++++++-------- .../qdq_transformer/qdq_propagation.h | 5 +- .../optimizer/qdq_transformer/qdq_s8_to_u8.cc | 10 +- .../optimizer/qdq_transformer/qdq_util.cc | 71 ++-- .../core/optimizer/qdq_transformer/qdq_util.h | 34 +- .../optimizer/graph_transform_test_builder.cc | 41 +- .../optimizer/graph_transform_test_builder.h | 8 +- .../test/optimizer/qdq_transformer_test.cc | 245 ++++++++--- onnxruntime/test/perftest/ort_test_session.cc | 20 +- 12 files changed, 657 insertions(+), 302 deletions(-) create mode 100644 onnxruntime/core/graph/extended_graph_edge.h diff --git a/cmake/onnxruntime_optimizer.cmake b/cmake/onnxruntime_optimizer.cmake index 44d16a1fef..ecf28ffb82 100644 --- a/cmake/onnxruntime_optimizer.cmake +++ b/cmake/onnxruntime_optimizer.cmake @@ -21,6 +21,8 @@ if (onnxruntime_MINIMAL_BUILD) "${ONNXRUNTIME_ROOT}/core/optimizer/qdq_transformer/qdq_util.cc" "${ONNXRUNTIME_ROOT}/core/optimizer/initializer.h" "${ONNXRUNTIME_ROOT}/core/optimizer/initializer.cc" + "${ONNXRUNTIME_ROOT}/core/optimizer/selectors_actions/helpers.h" + "${ONNXRUNTIME_ROOT}/core/optimizer/selectors_actions/helpers.cc" "${ONNXRUNTIME_ROOT}/core/optimizer/utils.h" "${ONNXRUNTIME_ROOT}/core/optimizer/utils.cc" ) diff --git a/onnxruntime/core/graph/extended_graph_edge.h b/onnxruntime/core/graph/extended_graph_edge.h new file mode 100644 index 0000000000..185e133341 --- /dev/null +++ b/onnxruntime/core/graph/extended_graph_edge.h @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +#include "core/graph/basic_types.h" +#include "core/graph/graph.h" +#include "core/graph/graph_utils.h" + +namespace onnxruntime::graph_utils { + +/** + * Represents an edge between two graph nodes, a graph input or initializer -> node connection, or a node -> graph + * output connection. + * Similar to graph_utils::GraphEdge. + */ +struct ExtendedGraphEdge { + enum class End { Source, + Destination }; + + /** Information about a source or destination node of the extended edge. */ + struct NodeInfo { + /** Node index in the graph. */ + NodeIndex node_idx; + /** Node input or output def index. */ + int arg_idx; + }; + + /** Source node info. If empty, the source is a graph input or initializer. */ + std::optional src; + /** Destination node info. If empty, the destination is a graph output. */ + std::optional dst; + /** Edge/connection NodeArg name. */ + std::string arg_name; + + /** Whether this extended edge has a graph input or initializer as the source. */ + bool HasGraphInputOrInitializer() const noexcept { return !src.has_value(); } + + /** Whether this extended edge has a graph output as the destination. */ + bool HasGraphOutput() const noexcept { return !dst.has_value(); } + + /** Gets the NodeInfo at the specified extended edge end. */ + const std::optional& GetNodeInfoAtEnd(End end) const noexcept { + return end == End::Source ? src : dst; + } + + /** Gets the graph node at the specified end if it exists, otherwise nullptr. */ + const Node* GetNodeAtEnd(const Graph& graph, End end) const { + if (const auto& node_info = GetNodeInfoAtEnd(end); node_info.has_value()) { + const Node* node = graph.GetNode(node_info->node_idx); + ORT_ENFORCE(node != nullptr, "Invalid node index ", node_info->node_idx); + return node; + } + return nullptr; + } + + /** Gets the mutable graph node at the specified end if it exists, otherwise nullptr. */ + Node* GetMutableNodeAtEnd(Graph& graph, End end) const { + if (const auto& node_info = GetNodeInfoAtEnd(end); node_info.has_value()) { + Node* node = graph.GetNode(node_info->node_idx); + ORT_ENFORCE(node != nullptr, "Invalid node index ", node_info->node_idx); + return node; + } + return nullptr; + } + + /** + * Creates an extended graph edge from a valid graph_utils::GraphEdge. + * To be valid, `graph_edge` should represent an actual edge between two nodes in the graph. + * Validity is assumed. + */ + static ExtendedGraphEdge CreateFromValidGraphEdge(const graph_utils::GraphEdge& graph_edge) { + return ExtendedGraphEdge{ + NodeInfo{graph_edge.src_node, graph_edge.src_arg_index}, + NodeInfo{graph_edge.dst_node, graph_edge.dst_arg_index}, + graph_edge.arg_name}; + } + + /** + * Attempts to create an extended graph edge from a graph input or initializer to a node. + * Returns nullopt if there is not actually a graph input or initializer at the specified node's input index. + */ + static std::optional TryCreateFromInputOrInitializerToNode( + const Graph& graph, const Node& node, int node_input_def_idx) { + const auto node_inputs = node.InputDefs(); + ORT_ENFORCE(node_input_def_idx >= 0 && + static_cast(node_input_def_idx) < node_inputs.size()); + + const auto* node_input = node_inputs[node_input_def_idx]; + if (!graph.IsInputsIncludingInitializers(node_input)) { + return std::nullopt; + } + + return ExtendedGraphEdge{ + std::nullopt, + NodeInfo{node.Index(), node_input_def_idx}, + node_input->Name()}; + } + + /** + * Attempts to create an extended graph edge from a node to a graph output. + * Returns nullopt if there is not actually a graph output at the specified node's output index. + */ + static std::optional TryCreateFromNodeToOutput( + const Graph& graph, const Node& node, int node_output_def_idx) { + const auto node_outputs = node.OutputDefs(); + ORT_ENFORCE(node_output_def_idx >= 0 && + static_cast(node_output_def_idx) < node_outputs.size()); + + const auto* node_output = node_outputs[node_output_def_idx]; + if (!graph.IsOutput(node_output)) { + return std::nullopt; + } + + return ExtendedGraphEdge{ + NodeInfo{node.Index(), node_output_def_idx}, + std::nullopt, + node_output->Name()}; + } + + // there is also the case where a graph input or initializer is an output + // if that's useful to represent, add another creation function +}; + +} // namespace onnxruntime::graph_utils diff --git a/onnxruntime/core/optimizer/graph_transformer_utils.cc b/onnxruntime/core/optimizer/graph_transformer_utils.cc index 0f1d532409..358941ee8b 100644 --- a/onnxruntime/core/optimizer/graph_transformer_utils.cc +++ b/onnxruntime/core/optimizer/graph_transformer_utils.cc @@ -182,6 +182,11 @@ std::vector> GenerateTransformers( session_options.free_dimension_overrides)); auto cpu_allocator = cpu_execution_provider.GetAllocator(0, OrtMemTypeDefault); transformers.emplace_back(std::make_unique(std::move(cpu_allocator))); + + if (!disable_quant_qdq) { + transformers.emplace_back(std::make_unique()); + } + } break; case TransformerLevel::Level2: { @@ -203,7 +208,6 @@ std::vector> GenerateTransformers( if (!QDQIsInt8Allowed()) { transformers.emplace_back(std::make_unique(cpu_ep)); } - transformers.emplace_back(std::make_unique(cpu_ep)); transformers.emplace_back(std::make_unique()); } diff --git a/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc b/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc index 0a830b74ce..131e29432e 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc @@ -1,246 +1,304 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#include "qdq_propagation.h" +#include "core/optimizer/qdq_transformer/qdq_propagation.h" -#include +#include +#include "core/graph/extended_graph_edge.h" #include "core/graph/graph_utils.h" #include "core/optimizer/initializer.h" #include "core/optimizer/qdq_transformer/qdq_util.h" #include "core/optimizer/utils.h" -namespace onnxruntime { +using onnxruntime::graph_utils::ExtendedGraphEdge; -static bool CanNodePropagate(const Node& node) { +namespace onnxruntime { +namespace { +bool CanNodePropagate(const Node& node) { return graph_utils::IsSupportedOptypeVersionAndDomain(node, "MaxPool", {12}) || graph_utils::IsSupportedOptypeVersionAndDomain(node, "Reshape", {5, 13, 14}) || graph_utils::IsSupportedOptypeVersionAndDomain(node, "Transpose", {1, 13}); } -static bool TryCancelOutDQQPair(Graph& graph, Node& dq_node, Node& q_node) { - auto get_const_initializer = [&graph](const std::string& initializer_name) { - return graph.GetConstantInitializer(initializer_name, true); +// convert this: src_node -> dst_node +// to this: src_node -> Q -> DQ -> dst_node +// assumptions: +// 1. insertion_edge is valid - node indexes refer to valid nodes, arg name refers to a valid NodeArg, and it +// corresponds to an actual graph relationship +// 2. scale_initializer_nodearg and zp_initializer_nodearg_ptr (if not null) are constant initializers +Status InsertQDQPair(Graph& graph, const ExtendedGraphEdge& insertion_edge, + NodeArg& scale_initializer_nodearg, NodeArg* zp_initializer_nodearg_ptr, + const logging::Logger& logger) { + auto* src_node = insertion_edge.GetMutableNodeAtEnd(graph, ExtendedGraphEdge::End::Source); + auto* dst_node = insertion_edge.GetMutableNodeAtEnd(graph, ExtendedGraphEdge::End::Destination); + + ORT_ENFORCE(src_node || dst_node, "At least one graph node must be specified in the propagation edge."); + + const auto& base_name = insertion_edge.arg_name; + auto& base_node_arg = *graph.GetNodeArg(base_name); + + LOGS(logger, VERBOSE) << "Inserting Q/DQ pair between " + << (src_node ? MakeString("node (\"", src_node->Name(), "\", index: ", src_node->Index(), ")") + : "input") + << " and " + << (dst_node ? MakeString("node (\"", dst_node->Name(), "\", index: ", dst_node->Index(), ")") + : "output") + << " at NodeArg \"" << base_name << "\"."; + + // set up new NodeArgs + auto& pre_q_nodearg = insertion_edge.HasGraphInputOrInitializer() + ? base_node_arg + : graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(base_name + "_pre_q"), + nullptr); + + auto& q_to_dq_nodearg = graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(base_name + "_q_to_dq"), + nullptr); + + auto& post_dq_nodearg = insertion_edge.HasGraphOutput() + ? base_node_arg + : graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(base_name + "_post_dq"), + nullptr); + + // set up new Nodes + auto make_q_or_dq_inputs = [](NodeArg& data, NodeArg& scale, NodeArg* zero_point) { + return zero_point ? std::vector{&data, &scale, zero_point} + : std::vector{&data, &scale}; }; - if (!QDQ::IsQDQPairSupported(q_node, dq_node, get_const_initializer, graph.ModelPath())) { - return false; + auto& q_node = graph.AddNode(graph.GenerateNodeName(base_name + "_q"), + QDQ::QOpName, + "Inserted by QDQPropagationTransformer", + // inputs + make_q_or_dq_inputs(pre_q_nodearg, scale_initializer_nodearg, + zp_initializer_nodearg_ptr), + // outputs + {&q_to_dq_nodearg}); + + ORT_RETURN_IF_NOT(graph.SetOpSchemaFromRegistryForNode(q_node), "Failed to set op schema for added Q node."); + + auto& dq_node = graph.AddNode(graph.GenerateNodeName(base_name + "_dq"), + QDQ::DQOpName, + "Inserted by QDQPropagationTransformer", + // inputs + make_q_or_dq_inputs(q_to_dq_nodearg, scale_initializer_nodearg, + zp_initializer_nodearg_ptr), + // outputs + {&post_dq_nodearg}); + + ORT_RETURN_IF_NOT(graph.SetOpSchemaFromRegistryForNode(dq_node), "Failed to set op schema for added DQ node."); + + // set up edges + if (src_node && dst_node) { + graph.RemoveEdge(src_node->Index(), dst_node->Index(), + insertion_edge.src->arg_idx, insertion_edge.dst->arg_idx); } - // check if dq_node has only one output edge and, - // dq_node and q_node output are not graph outputs - if (!optimizer_utils::CheckOutputEdges(graph, dq_node, 1) || - graph.NodeProducesGraphOutput(q_node)) { - return false; + if (src_node) { + src_node->MutableOutputDefs()[insertion_edge.src->arg_idx] = &pre_q_nodearg; + graph.AddEdge(src_node->Index(), q_node.Index(), insertion_edge.src->arg_idx, 0); } - // remove edge between parent of DQ to DQ - std::pair input_edge_info{0, -1}; - auto* dq_input_edge_0 = graph_utils::GetInputEdge(dq_node, 0); - if (dq_input_edge_0) { - input_edge_info.first = dq_input_edge_0->GetNode().Index(); - input_edge_info.second = dq_input_edge_0->GetSrcArgIndex(); - graph.RemoveEdge(dq_input_edge_0->GetNode().Index(), dq_node.Index(), - dq_input_edge_0->GetSrcArgIndex(), dq_input_edge_0->GetDstArgIndex()); + graph.AddEdge(q_node.Index(), dq_node.Index(), 0, 0); + + if (dst_node) { + dst_node->MutableInputDefs()[insertion_edge.dst->arg_idx] = &post_dq_nodearg; + graph.AddEdge(dq_node.Index(), dst_node->Index(), 0, insertion_edge.dst->arg_idx); } - graph_utils::RemoveNodeOutputEdges(graph, dq_node); // Remove DQ node output edges - - auto output_edges = graph_utils::GraphEdge::GetNodeOutputEdges(q_node, 0); - graph_utils::RemoveNodeOutputEdges(graph, q_node); // Remove Q node output edges - for (auto& output_edge : output_edges) { - // set input NodeArg of Q's children to the 1st input of DQ - graph.GetNode(output_edge.dst_node)->MutableInputDefs()[output_edge.dst_arg_index] = - dq_node.MutableInputDefs()[0]; - - // add edge between parent of DQ to children of Q - if (input_edge_info.second != -1) { - graph.AddEdge(input_edge_info.first, output_edge.dst_node, - input_edge_info.second, output_edge.dst_arg_index); - } - } - - graph.RemoveNode(dq_node.Index()); - graph.RemoveNode(q_node.Index()); - return true; + return Status::OK(); } -// A helper function that swap the relationship of 2 nodes. -// @param up_node The original parent node -// @param down_node The original child node -// after calling the function, the up_node will become the child of down_node -// Assumptions of this function are: -// 1. up_node only has one Edge that points to down_node and its output is not graph output. -// 2. NodeArg slots of the edge between up_node and down_node are (0, 0) -static void SwapAdjacentNodes(Graph& graph, Node& up_node, Node& down_node) { - ORT_ENFORCE(optimizer_utils::CheckOutputEdges(graph, up_node, 1), - "up_node should have only one Edge that points to down_node and its output is not graph output"); +std::optional GetPreviousEdge(const Graph& graph, const Node& node) { + // for now we can just consider the first input (index 0) - auto edge_it = up_node.OutputEdgesBegin(); - ORT_ENFORCE(edge_it->GetDstArgIndex() == 0 && - edge_it->GetSrcArgIndex() == 0 && - edge_it->GetNode().Index() == down_node.Index(), - "up_node should be parent of down_node and NodeArg slots of the edge between up_node and down_node should be (0, 0)."); + const auto input_edges = graph_utils::GraphEdge::GetNodeInputEdges(node); + const auto input_edge_it = std::find_if( + input_edges.begin(), input_edges.end(), + [](const graph_utils::GraphEdge& edge) { return edge.dst_arg_index == 0; }); - // ************** Remove edges ************** - // Remove the edge between parent of up_node and up_node, and keep the info of parent of up_node - std::pair up_node_input_edge_info{0, -1}; - auto* up_node_input_edge_0 = graph_utils::GetInputEdge(up_node, 0); - if (up_node_input_edge_0) { - up_node_input_edge_info.first = up_node_input_edge_0->GetNode().Index(); - up_node_input_edge_info.second = up_node_input_edge_0->GetSrcArgIndex(); - graph.RemoveEdge(up_node_input_edge_0->GetNode().Index(), - up_node.Index(), - up_node_input_edge_0->GetSrcArgIndex(), - up_node_input_edge_0->GetDstArgIndex()); + if (input_edge_it == input_edges.end()) { + // maybe edge from input + return ExtendedGraphEdge::TryCreateFromInputOrInitializerToNode(graph, node, 0); } - auto down_node_output_edges_info = graph_utils::GraphEdge::GetNodeOutputEdges(down_node); - graph_utils::RemoveNodeOutputEdges(graph, up_node); - graph_utils::RemoveNodeOutputEdges(graph, down_node); - - // *********** Rebuild NodeArg ****************/ - down_node.MutableInputDefs()[0] = up_node.MutableInputDefs()[0]; - up_node.MutableOutputDefs()[0] = down_node.MutableOutputDefs()[0]; - - NodeArg* new_node_arg = &graph.GetOrCreateNodeArg(graph.GenerateNodeArgName("SwapAdjacentNodes"), nullptr); - down_node.MutableOutputDefs()[0] = new_node_arg; - up_node.MutableInputDefs()[0] = new_node_arg; - - // *********** Rebuild Edges ****************/ - if (up_node_input_edge_info.second >= 0) { - graph.AddEdge(up_node_input_edge_info.first, down_node.Index(), up_node_input_edge_info.second, 0); + const auto& src_node = *graph.GetNode(input_edge_it->src_node); + const auto src_node_output_edges = + graph_utils::GraphEdge::GetNodeOutputEdges(src_node, input_edge_it->src_arg_index); + if (!graph.IsOutput(src_node.OutputDefs()[input_edge_it->src_arg_index]) && + src_node_output_edges.size() == 1) { + // single edge from previous node + return ExtendedGraphEdge::CreateFromValidGraphEdge(*input_edge_it); } - graph.AddEdge(down_node.Index(), up_node.Index(), 0, 0); - - for (auto output_edge_info : down_node_output_edges_info) { - graph.AddEdge(up_node.Index(), output_edge_info.dst_node, 0, output_edge_info.dst_arg_index); - } + return std::nullopt; } -bool QDQPropagationTransformer::PropagateDQForward(Graph& graph) const { - bool is_modified = false; +std::optional GetPreviousPropagationEdge(const Graph& graph, + const ExtendedGraphEdge& edge) { + if (edge.HasGraphInputOrInitializer()) { + return std::nullopt; + } - GraphViewer graph_viewer(graph); - const auto& node_topology_list = graph_viewer.GetNodesInTopologicalOrder(); + const auto* src_node = edge.GetNodeAtEnd(graph, ExtendedGraphEdge::End::Source); + ORT_ENFORCE(src_node != nullptr); - for (auto node_index : node_topology_list) { + if (!CanNodePropagate(*src_node)) { + return std::nullopt; + } + + return GetPreviousEdge(graph, *src_node); +} + +std::optional GetNextEdge(const Graph& graph, const Node& node) { + // for now we can just consider the first output (index 0) + + const auto output_edges = graph_utils::GraphEdge::GetNodeOutputEdges(node, 0); + if (output_edges.empty()) { + // maybe edge to output + return ExtendedGraphEdge::TryCreateFromNodeToOutput(graph, node, 0); + } + + if (!graph.IsOutput(node.OutputDefs()[0]) && output_edges.size() == 1) { + // single edge to next node + return ExtendedGraphEdge::CreateFromValidGraphEdge(output_edges.front()); + } + + return std::nullopt; +} + +std::optional GetNextPropagationEdge(const Graph& graph, + const ExtendedGraphEdge& edge) { + if (edge.HasGraphOutput()) { + return std::nullopt; + } + + const auto* dst_node = edge.GetNodeAtEnd(graph, ExtendedGraphEdge::End::Destination); + ORT_ENFORCE(dst_node != nullptr); + + if (!CanNodePropagate(*dst_node)) { + return std::nullopt; + } + + return GetNextEdge(graph, *dst_node); +} + +class GraphConstantInitializerGetter { + const Graph& graph_; + + public: + GraphConstantInitializerGetter(const Graph& graph) : graph_{graph} {} + const ONNX_NAMESPACE::TensorProto* operator()(const std::string& initializer_name) const { + return graph_utils::GetConstantInitializer(graph_, initializer_name); + } +}; + +Status PropagateDQForward(Graph& graph, gsl::span node_indices, + const std::unordered_set& compatible_eps, + const logging::Logger& logger, + bool& modified) { + for (auto node_index : node_indices) { auto* dq_node_ptr = graph.GetNode(node_index); - if (dq_node_ptr == nullptr) + if (dq_node_ptr == nullptr) { continue; // node removed as part of an earlier fusion + } Node& dq_node = *dq_node_ptr; - if (!graph_utils::IsSupportedOptypeVersionAndDomain(dq_node, "DequantizeLinear", {10, 13}) || - !graph_utils::IsSupportedProvider(dq_node, GetCompatibleExecutionProviders()) || + if (!QDQ::MatchDQNode(dq_node) || + !graph_utils::IsSupportedProvider(dq_node, compatible_eps) || !optimizer_utils::CheckOutputEdges(graph, dq_node, 1)) { continue; } - std::vector& dq_input_defs = dq_node.MutableInputDefs(); - if (dq_input_defs.size() != QDQ::InputIndex::TOTAL_COUNT) { + bool dq_zero_point_exists = false; + if (!QDQ::QOrDQNodeHasConstantScalarScaleAndZeroPoint(dq_node, GraphConstantInitializerGetter{graph}, + dq_zero_point_exists)) { continue; } - if (!optimizer_utils::IsScalar(*dq_input_defs[QDQ::InputIndex::ZERO_POINT_ID]) || - !optimizer_utils::IsScalar(*dq_input_defs[QDQ::InputIndex::SCALE_ID])) { + auto& dq_scale = *dq_node.MutableInputDefs()[QDQ::InputIndex::SCALE_ID]; + auto* dq_zero_point = dq_zero_point_exists + ? dq_node.MutableInputDefs()[QDQ::InputIndex::ZERO_POINT_ID] + : nullptr; + + const auto edge_after_dq = GetNextEdge(graph, dq_node); + if (!edge_after_dq) { continue; } - const ONNX_NAMESPACE::TensorProto* dq_zp_tensor_proto = - graph_utils::GetConstantInitializer(graph, dq_input_defs[QDQ::InputIndex::ZERO_POINT_ID]->Name()); - const ONNX_NAMESPACE::TensorProto* dq_scale_tensor_proto = - graph_utils::GetConstantInitializer(graph, dq_input_defs[QDQ::InputIndex::SCALE_ID]->Name()); - - if (nullptr == dq_zp_tensor_proto || nullptr == dq_scale_tensor_proto) { - continue; - } - - do { - Node& next_node = *graph.GetNode(dq_node.OutputNodesBegin()->Index()); - if (!CanNodePropagate(next_node)) { - // Try canceling out DQ/Q pair - if (graph_utils::IsSupportedOptypeVersionAndDomain(next_node, "QuantizeLinear", {10, 13}) && - graph_utils::IsSupportedProvider(next_node, GetCompatibleExecutionProviders()) && - TryCancelOutDQQPair(graph, dq_node, next_node)) { - is_modified = true; - } - + for (auto curr_edge = GetNextPropagationEdge(graph, *edge_after_dq); + curr_edge.has_value(); + curr_edge = GetNextPropagationEdge(graph, *curr_edge)) { + if (const auto* dst_node = curr_edge->GetNodeAtEnd(graph, ExtendedGraphEdge::End::Destination); + dst_node && QDQ::MatchQNode(*dst_node)) { break; } - SwapAdjacentNodes(graph, dq_node, next_node); - is_modified = true; - } while (optimizer_utils::CheckOutputEdges(graph, dq_node, 1)); + + ORT_RETURN_IF_ERROR(InsertQDQPair(graph, *curr_edge, dq_scale, dq_zero_point, logger)); + modified = true; + } } - return is_modified; + return Status::OK(); } -bool QDQPropagationTransformer::PropagateQBackward(Graph& graph) const { - bool is_modified = false; - - GraphViewer graph_viewer(graph); - const auto& node_topology_list = graph_viewer.GetNodesInTopologicalOrder(); - - for (auto node_index : node_topology_list) { +Status PropagateQBackward(Graph& graph, gsl::span node_indices, + const std::unordered_set& compatible_eps, + const logging::Logger& logger, + bool& modified) { + for (auto node_index : node_indices) { auto* q_node_ptr = graph.GetNode(node_index); - if (q_node_ptr == nullptr) + if (q_node_ptr == nullptr) { continue; // node removed as part of an earlier fusion + } Node& q_node = *q_node_ptr; - if (!graph_utils::IsSupportedOptypeVersionAndDomain(q_node, "QuantizeLinear", {10, 13}) || - !graph_utils::IsSupportedProvider(q_node, GetCompatibleExecutionProviders())) { + if (!QDQ::MatchQNode(q_node) || + !graph_utils::IsSupportedProvider(q_node, compatible_eps)) { continue; } - std::vector& q_input_defs = q_node.MutableInputDefs(); - if (q_input_defs.size() != QDQ::InputIndex::TOTAL_COUNT || - !optimizer_utils::IsScalar(*q_input_defs[QDQ::InputIndex::ZERO_POINT_ID]) || - !optimizer_utils::IsScalar(*q_input_defs[QDQ::InputIndex::SCALE_ID])) { + bool q_zero_point_exists = false; + if (!QDQ::QOrDQNodeHasConstantScalarScaleAndZeroPoint(q_node, GraphConstantInitializerGetter{graph}, + q_zero_point_exists)) { continue; } - const ONNX_NAMESPACE::TensorProto* q_zp_tensor_proto = - graph_utils::GetConstantInitializer(graph, q_input_defs[QDQ::InputIndex::ZERO_POINT_ID]->Name()); - const ONNX_NAMESPACE::TensorProto* q_scale_tensor_proto = - graph_utils::GetConstantInitializer(graph, q_input_defs[QDQ::InputIndex::SCALE_ID]->Name()); + auto& q_scale = *q_node.MutableInputDefs()[QDQ::InputIndex::SCALE_ID]; + auto* q_zero_point = q_zero_point_exists + ? q_node.MutableInputDefs()[QDQ::InputIndex::ZERO_POINT_ID] + : nullptr; - if (nullptr == q_zp_tensor_proto || nullptr == q_scale_tensor_proto) { + const auto edge_before_q = GetPreviousEdge(graph, q_node); + if (!edge_before_q) { continue; } - do { - if (q_node.InputNodesBegin() == q_node.InputNodesEnd()) { - break; - } - Node& prev_node = *graph.GetNode(q_node.InputNodesBegin()->Index()); - if (!optimizer_utils::CheckOutputEdges(graph, prev_node, 1)) break; - if (!CanNodePropagate(prev_node)) { - // Try canceling out DQ/Q pair - Node& dq_node = prev_node; - if (graph_utils::IsSupportedOptypeVersionAndDomain(dq_node, "DequantizeLinear", {10, 13}) && - graph_utils::IsSupportedProvider(dq_node, GetCompatibleExecutionProviders()) && - TryCancelOutDQQPair(graph, dq_node, q_node)) { - is_modified = true; - } + for (auto curr_edge = GetPreviousPropagationEdge(graph, *edge_before_q); + curr_edge.has_value(); + curr_edge = GetPreviousPropagationEdge(graph, *curr_edge)) { + if (auto* src_node = curr_edge->GetNodeAtEnd(graph, ExtendedGraphEdge::End::Source); + src_node && QDQ::MatchDQNode(*src_node)) { break; } - SwapAdjacentNodes(graph, prev_node, q_node); - is_modified = true; - } while (true); + ORT_RETURN_IF_ERROR(InsertQDQPair(graph, *curr_edge, q_scale, q_zero_point, logger)); + modified = true; + } } - return is_modified; + return Status::OK(); } +} // namespace -Status QDQPropagationTransformer::ApplyImpl(Graph& graph, bool& modified, int graph_level, const logging::Logger& logger) const { +Status QDQPropagationTransformer::ApplyImpl(Graph& graph, bool& modified, int graph_level, + const logging::Logger& logger) const { GraphViewer graph_viewer(graph); - const auto& node_topology_list = graph_viewer.GetNodesInTopologicalOrder(); + const auto node_indices = gsl::make_span(graph_viewer.GetNodesInTopologicalOrder()); - for (auto node_index : node_topology_list) { + for (auto node_index : node_indices) { auto* node_ptr = graph.GetNode(node_index); if (node_ptr == nullptr) continue; // node removed as part of an earlier fusion @@ -248,12 +306,11 @@ Status QDQPropagationTransformer::ApplyImpl(Graph& graph, bool& modified, int gr ORT_RETURN_IF_ERROR(Recurse(*node_ptr, modified, graph_level, logger)); } - while (PropagateQBackward(graph) || PropagateDQForward(graph)) { - } + const auto& compatible_eps = GetCompatibleExecutionProviders(); - modified = true; + ORT_RETURN_IF_ERROR(PropagateQBackward(graph, node_indices, compatible_eps, logger, modified)); + ORT_RETURN_IF_ERROR(PropagateDQForward(graph, node_indices, compatible_eps, logger, modified)); return Status::OK(); } - } // namespace onnxruntime diff --git a/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.h b/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.h index 77524b2283..a99a4720b8 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.h +++ b/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.h @@ -11,7 +11,8 @@ namespace onnxruntime { /** @Class QDQPropagationTransformer - Propagate Q backward, DQ forward and remove DQ/Q pair + Propagate Q backward, DQ forward. This is done by inserting Q/DQ pairs that match the starting Q or DQ past each + subsequent (previous for Q, next for DQ) op supporting propagation. */ class QDQPropagationTransformer : public GraphTransformer { public: @@ -21,8 +22,6 @@ class QDQPropagationTransformer : public GraphTransformer { private: Status ApplyImpl(Graph& graph, bool& modified, int graph_level, const logging::Logger& logger) const override; - bool PropagateDQForward(Graph& graph) const; - bool PropagateQBackward(Graph& graph) const; }; } // namespace onnxruntime diff --git a/onnxruntime/core/optimizer/qdq_transformer/qdq_s8_to_u8.cc b/onnxruntime/core/optimizer/qdq_transformer/qdq_s8_to_u8.cc index dd92b7db70..cd5e4e6d98 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/qdq_s8_to_u8.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/qdq_s8_to_u8.cc @@ -1,16 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#include "qdq_s8_to_u8.h" +#include "core/optimizer/qdq_transformer/qdq_s8_to_u8.h" #include "core/graph/graph_utils.h" #include "core/optimizer/initializer.h" +#include "core/optimizer/qdq_transformer/qdq_util.h" #include "core/optimizer/utils.h" namespace onnxruntime { // Convert QuantizeLinear and DequantizeLinear pair with type int8_t to type uint8_t -Status QDQS8ToU8Transformer::ApplyImpl(Graph& graph, bool& modified, int graph_level, const logging::Logger& logger) const { +Status QDQS8ToU8Transformer::ApplyImpl(Graph& graph, bool& modified, int graph_level, + const logging::Logger& logger) const { GraphViewer graph_viewer(graph); const auto& node_topology_list = graph_viewer.GetNodesInTopologicalOrder(); @@ -23,14 +25,14 @@ Status QDQS8ToU8Transformer::ApplyImpl(Graph& graph, bool& modified, int graph_l Node& q_node = *q_node_ptr; ORT_RETURN_IF_ERROR(Recurse(q_node, modified, graph_level, logger)); - if (!graph_utils::IsSupportedOptypeVersionAndDomain(q_node, "QuantizeLinear", {10, 13}) || + if (!QDQ::MatchQNode(q_node) || !graph_utils::IsSupportedProvider(q_node, GetCompatibleExecutionProviders()) || !optimizer_utils::CheckOutputEdges(graph, q_node, 1)) { continue; } Node& dq_node = *graph.GetNode(q_node.OutputNodesBegin()->Index()); - if (!graph_utils::IsSupportedOptypeVersionAndDomain(dq_node, "DequantizeLinear", {10, 13}) || + if (!QDQ::MatchDQNode(dq_node) || !graph_utils::IsSupportedProvider(dq_node, GetCompatibleExecutionProviders())) { continue; } diff --git a/onnxruntime/core/optimizer/qdq_transformer/qdq_util.cc b/onnxruntime/core/optimizer/qdq_transformer/qdq_util.cc index 0c5710b6fd..869566fc78 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/qdq_util.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/qdq_util.cc @@ -1,21 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#include "qdq_util.h" +#include "core/optimizer/qdq_transformer/qdq_util.h" #include +#include "core/common/common.h" #include "core/graph/graph.h" #include "core/graph/graph_utils.h" #include "core/optimizer/initializer.h" #include "core/optimizer/utils.h" -namespace onnxruntime { -namespace QDQ { +namespace onnxruntime::QDQ { bool IsQDQPairSupported( const Node& q_node, const Node& dq_node, - const std::function& get_const_initializer, + const GetConstantInitializerFn& get_const_initializer, const Path& model_path) { ConstPointerContainer> dq_input_defs = dq_node.InputDefs(); ConstPointerContainer> q_input_defs = q_node.InputDefs(); @@ -58,31 +58,56 @@ bool IsQDQPairSupported( *q_scale.data() == *dq_scale.data(); } -bool IsDQSupported( - const Node& dq_node, - const std::function& get_const_initializer) { - ConstPointerContainer> dq_input_defs = dq_node.InputDefs(); - - // DQ contains optional input is not supported - // non-scalar DQ scale and zero point needs are not supported - if (dq_input_defs.size() != InputIndex::TOTAL_COUNT || - !optimizer_utils::IsScalar(*dq_input_defs[InputIndex::SCALE_ID]) || - !optimizer_utils::IsScalar(*dq_input_defs[InputIndex::ZERO_POINT_ID])) { +bool IsDQSupported(const Node& dq_node, const GetConstantInitializerFn& get_const_initializer) { + bool zero_point_exists = false; + if (!QOrDQNodeHasConstantScalarScaleAndZeroPoint(dq_node, get_const_initializer, zero_point_exists)) { return false; } - // if DQ scale and zero point are not constant, return false - const ONNX_NAMESPACE::TensorProto* dq_scale_tensor_proto = - get_const_initializer(dq_input_defs[InputIndex::SCALE_ID]->Name()); - const ONNX_NAMESPACE::TensorProto* dq_zp_tensor_proto = - get_const_initializer(dq_input_defs[InputIndex::ZERO_POINT_ID]->Name()); - if (nullptr == dq_zp_tensor_proto || - nullptr == dq_scale_tensor_proto) { + if (!zero_point_exists) { return false; } return true; } -} // namespace QDQ -} // namespace onnxruntime +bool QOrDQNodeHasConstantScalarScaleAndZeroPoint( + const Node& q_or_dq_node, + const GetConstantInitializerFn& get_const_initializer, + bool& zero_point_exists) { + auto q_or_dq_input_defs = q_or_dq_node.InputDefs(); + + ORT_ENFORCE(q_or_dq_input_defs.size() >= 2); + + zero_point_exists = q_or_dq_input_defs.size() > 2 && + q_or_dq_input_defs[InputIndex::ZERO_POINT_ID]->Exists(); + + auto is_constant_scalar = [&](const NodeArg& node_arg) { + return optimizer_utils::IsScalar(node_arg) && get_const_initializer(node_arg.Name()) != nullptr; + }; + + if (!is_constant_scalar(*q_or_dq_input_defs[InputIndex::SCALE_ID])) { + return false; + } + + if (zero_point_exists && + !is_constant_scalar(*q_or_dq_input_defs[InputIndex::ZERO_POINT_ID])) { + return false; + } + + return true; +} + +#if !defined(ORT_MINIMAL_BUILD) + +bool MatchQNode(const Node& node) { + return graph_utils::IsSupportedOptypeVersionAndDomain(node, QOpName, {10, 13}); +} + +bool MatchDQNode(const Node& node) { + return graph_utils::IsSupportedOptypeVersionAndDomain(node, DQOpName, {10, 13}); +} + +#endif // !defined(ORT_MINIMAL_BUILD) + +} // namespace onnxruntime::QDQ diff --git a/onnxruntime/core/optimizer/qdq_transformer/qdq_util.h b/onnxruntime/core/optimizer/qdq_transformer/qdq_util.h index 22a06e7506..70f6c70c5e 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/qdq_util.h +++ b/onnxruntime/core/optimizer/qdq_transformer/qdq_util.h @@ -17,8 +17,8 @@ class Path; namespace QDQ { -static constexpr const char* QOpName = "QuantizeLinear"; -static constexpr const char* DQOpName = "DequantizeLinear"; +constexpr const char* QOpName = "QuantizeLinear"; +constexpr const char* DQOpName = "DequantizeLinear"; enum InputIndex : int { INPUT_ID = 0, @@ -27,21 +27,39 @@ enum InputIndex : int { TOTAL_COUNT = 3, }; -// Check if Q/DQ pair is supported in the QDQ transformer. It requires: +using GetConstantInitializerFn = std::function; + +// Check if Q/DQ pair is supported in extended level QDQ transformers. It requires: // 1. Q/DQ doesn't have optional input. // 2. scale and zero point is constant scalar // 3. Q and DQ have same scale and zero point bool IsQDQPairSupported( const Node& q_node, const Node& dq_node, - const std::function& get_const_initializer, + const GetConstantInitializerFn& get_const_initializer, const Path& model_path); -// Check if DQ is supported in the QDQ transformer. It requires: +// Check if DQ is supported in extended level QDQ transformers. It requires: // 1. DQ doesn't have optional input. // 2. scale and zero point is constant scalar -bool IsDQSupported( - const Node& dq_node, - const std::function& get_const_initializer); +bool IsDQSupported(const Node& dq_node, const GetConstantInitializerFn& get_const_initializer); + +// Check if Q or DQ node's scale and zero point inputs are constant scalars. +// If the zero point input does not exist, it is assumed to have a default scalar value. +// `zero_point_exists` will indicate if it does exist. +bool QOrDQNodeHasConstantScalarScaleAndZeroPoint( + const Node& q_or_dq_node, + const GetConstantInitializerFn& get_const_initializer, + bool& zero_point_exists); + +#if !defined(ORT_MINIMAL_BUILD) + +// Check Q node op type, version, and domain. +bool MatchQNode(const Node& node); + +// Check DQ node op type, version, and domain. +bool MatchDQNode(const Node& node); + +#endif // !defined(ORT_MINIMAL_BUILD) } // namespace QDQ } // namespace onnxruntime diff --git a/onnxruntime/test/optimizer/graph_transform_test_builder.cc b/onnxruntime/test/optimizer/graph_transform_test_builder.cc index 5731668957..6312547d8a 100644 --- a/onnxruntime/test/optimizer/graph_transform_test_builder.cc +++ b/onnxruntime/test/optimizer/graph_transform_test_builder.cc @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include "test/optimizer/graph_transform_test_builder.h" + #include #include #include @@ -9,10 +11,9 @@ #include "core/session/inference_session.h" #include "test/compare_ortvalue.h" #include "test/test_environment.h" +#include "test/util/include/asserts.h" #include "test/util/include/inference_session_wrapper.h" -#include "graph_transform_test_builder.h" - namespace onnxruntime { namespace test { @@ -34,39 +35,33 @@ void TransformerTester(const std::function& buil ModelTestBuilder helper(graph); build_test_case(helper); helper.SetGraphOutputs(); - ASSERT_TRUE(model.MainGraph().Resolve().IsOK()); + ASSERT_STATUS_OK(model.MainGraph().Resolve()); // Serialize the model to a string. std::string model_data; model.ToProto().SerializeToString(&model_data); - auto run_model = [&](TransformerLevel level, std::vector& fetches, std::unique_ptr transformer = nullptr) { + auto run_model = [&](TransformerLevel level, std::vector& fetches, + std::unique_ptr transformer = nullptr) { SessionOptions session_options; session_options.graph_optimization_level = transformer ? baseline_level : level; #if 0 // enable to dump model for debugging - session_options.optimized_model_filepath = L"model" + std::to_wstring(static_cast(level)) + L".onnx"; + session_options.optimized_model_filepath = + ToPathString("model" + std::to_string(static_cast(level)) + ".onnx"); #endif InferenceSessionWrapper session{session_options, GetEnvironment()}; - ASSERT_TRUE(session.Load(model_data.data(), static_cast(model_data.size())).IsOK()); + ASSERT_STATUS_OK(session.Load(model_data.data(), static_cast(model_data.size()))); if (transformer) { - ASSERT_TRUE(session.RegisterGraphTransformer(std::move(transformer), level).IsOK()); + ASSERT_STATUS_OK(session.RegisterGraphTransformer(std::move(transformer), level)); } - auto status = session.Initialize(); - if (!status.IsOK()) { - std::cout << "Model initialized failed with status message: " << status.ErrorMessage() << std::endl; - } - ASSERT_TRUE(status.IsOK()); + ASSERT_STATUS_OK(session.Initialize()); RunOptions run_options; - status = session.Run(run_options, - helper.feeds_, - helper.output_names_, - &fetches); - if (!status.IsOK()) { - std::cout << "Run failed with status message: " << status.ErrorMessage() << std::endl; - } - ASSERT_TRUE(status.IsOK()); + ASSERT_STATUS_OK(session.Run(run_options, + helper.feeds_, + helper.output_names_, + &fetches)); if (level == target_level) { check_transformed_graph(session); @@ -74,13 +69,13 @@ void TransformerTester(const std::function& buil }; std::vector baseline_fetches; - run_model(baseline_level, baseline_fetches); + ASSERT_NO_FATAL_FAILURE(run_model(baseline_level, baseline_fetches)); std::vector target_fetches; - run_model(target_level, target_fetches, std::move(transformer)); + ASSERT_NO_FATAL_FAILURE(run_model(target_level, target_fetches, std::move(transformer))); size_t num_outputs = baseline_fetches.size(); - ASSERT_TRUE(num_outputs == target_fetches.size()); + ASSERT_EQ(num_outputs, target_fetches.size()); for (size_t i = 0; i < num_outputs; i++) { std::pair ret = diff --git a/onnxruntime/test/optimizer/graph_transform_test_builder.h b/onnxruntime/test/optimizer/graph_transform_test_builder.h index 5050f344e9..13936b7f2e 100644 --- a/onnxruntime/test/optimizer/graph_transform_test_builder.h +++ b/onnxruntime/test/optimizer/graph_transform_test_builder.h @@ -172,11 +172,9 @@ class ModelTestBuilder { return AddNode("DequantizeLinear", input_args, {output_arg}); } - template - typename std::enable_if::value, Node&>::type - AddDequantizeLinearNode(NodeArg* input_arg, - float input_scale, - NodeArg* output_arg) { + Node& AddDequantizeLinearNode(NodeArg* input_arg, + float input_scale, + NodeArg* output_arg) { std::vector input_args; input_args.push_back(input_arg); input_args.push_back(MakeScalarInitializer(input_scale)); diff --git a/onnxruntime/test/optimizer/qdq_transformer_test.cc b/onnxruntime/test/optimizer/qdq_transformer_test.cc index 9eef77aa53..ca59772275 100644 --- a/onnxruntime/test/optimizer/qdq_transformer_test.cc +++ b/onnxruntime/test/optimizer/qdq_transformer_test.cc @@ -34,7 +34,17 @@ namespace onnxruntime { namespace test { -#ifndef DISABLE_CONTRIB_OPS +static std::vector GetNodeOpTypesInTopologicalOrder(const Graph& graph) { + std::vector op_types{}; + GraphViewer graph_viewer{graph}; + const auto& ordering = graph_viewer.GetNodesInTopologicalOrder(); + for (const auto node_idx : ordering) { + op_types.push_back(graph.GetNode(node_idx)->OpType()); + } + return op_types; +} + +#if !defined(DISABLE_CONTRIB_OPS) template void QDQTransformerConvTests() { @@ -198,12 +208,13 @@ TEST(QDQTransformerTests, ConvMaxPoolReshape_Int8) { }; auto check_graph = [&](InferenceSessionWrapper& session) { - auto op_to_count = CountOpsInGraph(session.GetGraph()); - EXPECT_EQ(op_to_count["QLinearConv"], 1); - EXPECT_EQ(op_to_count["MaxPool"], 1); - EXPECT_EQ(op_to_count["Reshape"], 1); - EXPECT_EQ(op_to_count["QuantizeLinear"], 1); - EXPECT_EQ(op_to_count["DequantizeLinear"], 0); + const std::vector expected_op_types_in_order{ + "QuantizeLinear", + "QLinearConv", + "MaxPool", + "Reshape"}; + const auto op_types_in_order = GetNodeOpTypesInTopologicalOrder(session.GetGraph()); + EXPECT_EQ(op_types_in_order, expected_op_types_in_order); }; TransformerTester(build_test_case, check_graph, TransformerLevel::Level1, TransformerLevel::Level2); @@ -1673,27 +1684,115 @@ TEST(QDQTransformerTests, Concat) { test_case({{1, 6, 36}, {1, 6, 8}, {1, 6, 2}}, 2, false, false, true); } -TEST(QDQTransformerTests, QDQPropagation_QDQCancelOut) { - auto test_case = [&](const std::vector& input_shape, size_t maxpool_dim, const std::vector& perms) { +#endif // !defined(DISABLE_CONTRIB_OPS) + +TEST(QDQTransformerTests, QDQPropagation_QBackward) { + auto test_case = [&](const std::vector& input_shape, + size_t maxpool_dim, + const std::vector& perms, + bool add_op_boundary, + bool include_zp) { auto build_test_case = [&](ModelTestBuilder& builder) { auto* input_arg = builder.MakeInput(input_shape, -1.f, 1.f); auto* output_arg = builder.MakeOutput(); - // add QDQ - auto* qdq_output = AddQDQNodePair(builder, input_arg, .004f, 129); + auto* transpose_input = add_op_boundary ? builder.MakeIntermediate() : input_arg; + if (add_op_boundary) { + // add Sign as boundary for QDQ propagation + builder.AddNode("Sign", {input_arg}, {transpose_input}); + } // add Transpose auto* transpose_output = builder.MakeIntermediate(); - Node& transpose_node = builder.AddNode("Transpose", {qdq_output}, {transpose_output}); + Node& transpose_node = builder.AddNode("Transpose", {transpose_input}, {transpose_output}); transpose_node.AddAttribute("perm", perms); - // add Q - auto* q_output = builder.MakeIntermediate(); - builder.AddQuantizeLinearNode(transpose_output, .004f, 129, q_output); - // add MaxPool auto* maxpool_output = builder.MakeIntermediate(); - Node& pool_node = builder.AddNode("MaxPool", {q_output}, {maxpool_output}); + Node& pool_node = builder.AddNode("MaxPool", {transpose_output}, {maxpool_output}); + std::vector pads((maxpool_dim - 2) * 2, 1); + pool_node.AddAttribute("pads", pads); + std::vector kernel_shape(maxpool_dim - 2, 3); + pool_node.AddAttribute("kernel_shape", kernel_shape); + + // Reshape + auto* reshape_output = builder.MakeIntermediate(); + auto* reshape_shape = builder.Make1DInitializer({-1, 0}); + builder.AddNode("Reshape", {maxpool_output, reshape_shape}, {reshape_output}); + + // add Q + constexpr float qdq_scale = 0.004f; + if (include_zp) { + constexpr uint8_t qdq_zero_point = 129; + builder.AddQuantizeLinearNode(reshape_output, qdq_scale, qdq_zero_point, output_arg); + } else { + builder.AddQuantizeLinearNode(reshape_output, qdq_scale, output_arg); + } + }; + + auto check_graph = [&](InferenceSessionWrapper& session) { + std::vector expected_op_types_in_order{}; + if (add_op_boundary) { + expected_op_types_in_order.push_back("Sign"); + } + expected_op_types_in_order.insert( + expected_op_types_in_order.end(), + {"QuantizeLinear", "DequantizeLinear", + "Transpose", + "QuantizeLinear", "DequantizeLinear", + "MaxPool", + "QuantizeLinear", "DequantizeLinear", + "Reshape", + "QuantizeLinear"}); + + const auto op_types_in_order = GetNodeOpTypesInTopologicalOrder(session.GetGraph()); + EXPECT_EQ(op_types_in_order, expected_op_types_in_order); + }; + + TransformerTester(build_test_case, + check_graph, + TransformerLevel::Default, + TransformerLevel::Level1); + }; + + // TODO re-enable tests after updating ONNX to get QuantizeLinear shape inference fix + // https://github.com/onnx/onnx/pull/3806 + //test_case({1, 13, 13, 23}, 4, {0, 3, 1, 2}, false, false); + test_case({1, 13, 13, 23}, 4, {0, 3, 1, 2}, false, true); + //test_case({1, 13, 13, 23}, 4, {0, 3, 1, 2}, true, false); + test_case({1, 13, 13, 23}, 4, {0, 3, 1, 2}, true, true); +} + +TEST(QDQTransformerTests, QDQPropagation_DQForward) { + auto test_case = [&](const std::vector& input_shape, + size_t maxpool_dim, + const std::vector& perms, + bool add_op_boundary, + bool include_zp) { + auto build_test_case = [&](ModelTestBuilder& builder) { + auto* input_arg = builder.MakeInput(input_shape, + std::numeric_limits::min(), + std::numeric_limits::max()); + auto* output_arg = builder.MakeOutput(); + + // add DQ + constexpr float qdq_scale = 0.004f; + auto* dq_output = builder.MakeIntermediate(); + if (include_zp) { + constexpr uint8_t qdq_zero_point = 129; + builder.AddDequantizeLinearNode(input_arg, qdq_scale, qdq_zero_point, dq_output); + } else { + builder.AddDequantizeLinearNode(input_arg, qdq_scale, dq_output); + } + + // add Transpose + auto* transpose_output = builder.MakeIntermediate(); + Node& transpose_node = builder.AddNode("Transpose", {dq_output}, {transpose_output}); + transpose_node.AddAttribute("perm", perms); + + // add MaxPool + auto* maxpool_output = builder.MakeIntermediate(); + Node& pool_node = builder.AddNode("MaxPool", {transpose_output}, {maxpool_output}); std::vector pads((maxpool_dim - 2) * 2, 1); pool_node.AddAttribute("pads", pads); std::vector kernel_shape(maxpool_dim - 2, 3); @@ -1701,28 +1800,47 @@ TEST(QDQTransformerTests, QDQPropagation_QDQCancelOut) { // Reshape auto* reshape_shape = builder.Make1DInitializer({-1, 0}); - builder.AddNode("Reshape", {maxpool_output, reshape_shape}, {output_arg}); + auto* reshape_output = add_op_boundary ? builder.MakeIntermediate() : output_arg; + builder.AddNode("Reshape", {maxpool_output, reshape_shape}, {reshape_output}); + + if (add_op_boundary) { + // add Sign as boundary for QDQ propagation + builder.AddNode("Sign", {reshape_output}, {output_arg}); + } }; auto check_graph = [&](InferenceSessionWrapper& session) { - auto op_to_count = CountOpsInGraph(session.GetGraph()); - EXPECT_EQ(op_to_count["MaxPool"], 1); - EXPECT_EQ(op_to_count["Reshape"], 1); - EXPECT_EQ(op_to_count["Transpose"], 1); - EXPECT_EQ(op_to_count["QuantizeLinear"], 1); - EXPECT_EQ(op_to_count["DequantizeLinear"], 0); + std::vector expected_op_types_in_order{ + "DequantizeLinear", + "Transpose", + "QuantizeLinear", "DequantizeLinear", + "MaxPool", + "QuantizeLinear", "DequantizeLinear", + "Reshape", + "QuantizeLinear", "DequantizeLinear"}; + if (add_op_boundary) { + expected_op_types_in_order.push_back("Sign"); + } + + const auto op_types_in_order = GetNodeOpTypesInTopologicalOrder(session.GetGraph()); + EXPECT_EQ(op_types_in_order, expected_op_types_in_order); }; TransformerTester(build_test_case, check_graph, - TransformerLevel::Level1, - TransformerLevel::Level2); + TransformerLevel::Default, + TransformerLevel::Level1); }; - test_case({1, 13, 13, 23}, 4, {0, 3, 1, 2}); + // TODO re-enable tests after updating ONNX to get QuantizeLinear shape inference fix + // https://github.com/onnx/onnx/pull/3806 + //test_case({1, 13, 13, 23}, 4, {0, 3, 1, 2}, false, false); + test_case({1, 13, 13, 23}, 4, {0, 3, 1, 2}, false, true); + //test_case({1, 13, 13, 23}, 4, {0, 3, 1, 2}, true, false); + test_case({1, 13, 13, 23}, 4, {0, 3, 1, 2}, true, true); } -TEST(QDQTransformerTests, QDQPropagation_QDQ_CancelOut_More) { +TEST(QDQTransformerTests, QDQPropagation_StopAtOtherQDQ) { auto test_case = [&](const std::vector& input_shape, bool same_scale, bool same_zp) { auto build_test_case = [&](ModelTestBuilder& builder) { auto* input_arg = builder.MakeInput(input_shape, -1.f, 1.f); @@ -1741,16 +1859,18 @@ TEST(QDQTransformerTests, QDQPropagation_QDQ_CancelOut_More) { }; auto check_graph = [&](InferenceSessionWrapper& session) { - auto op_to_count = CountOpsInGraph(session.GetGraph()); - EXPECT_EQ(op_to_count["Reshape"], 1); - EXPECT_EQ(op_to_count["QuantizeLinear"], same_scale && same_zp ? 1 : 2); - EXPECT_EQ(op_to_count["DequantizeLinear"], same_scale && same_zp ? 0 : 1); + const std::vector expected_op_types_in_order{ + "QuantizeLinear", "DequantizeLinear", + "Reshape", + "QuantizeLinear"}; + const auto op_types_in_order = GetNodeOpTypesInTopologicalOrder(session.GetGraph()); + EXPECT_EQ(op_types_in_order, expected_op_types_in_order); }; TransformerTester(build_test_case, check_graph, - TransformerLevel::Level1, - TransformerLevel::Level2); + TransformerLevel::Default, + TransformerLevel::Level1); }; test_case({1, 13, 13, 23}, false, false); @@ -1775,16 +1895,18 @@ TEST(QDQTransformerTests, QDQPropagation_Q_No_Parent) { }; auto check_graph = [&](InferenceSessionWrapper& session) { - GraphViewer graph_viewer(session.GetGraph()); - const auto& node_topology_list = graph_viewer.GetNodesInTopologicalOrder(); - EXPECT_EQ(graph_viewer.GetNode(node_topology_list[0])->OpType(), "QuantizeLinear"); - EXPECT_EQ(graph_viewer.GetNode(node_topology_list[1])->OpType(), "Transpose"); + const std::vector expected_op_types_in_order{ + "QuantizeLinear", "DequantizeLinear", + "Transpose", + "QuantizeLinear"}; + const auto op_types_in_order = GetNodeOpTypesInTopologicalOrder(session.GetGraph()); + EXPECT_EQ(op_types_in_order, expected_op_types_in_order); }; TransformerTester(build_test_case, check_graph, - TransformerLevel::Level1, - TransformerLevel::Level2); + TransformerLevel::Default, + TransformerLevel::Level1); }; test_case({1, 13, 13, 23}, {0, 2, 3, 1}); @@ -1808,17 +1930,18 @@ TEST(QDQTransformerTests, QDQPropagation_DQ_No_Children) { }; auto check_graph = [&](InferenceSessionWrapper& session) { - auto op_to_count = CountOpsInGraph(session.GetGraph()); - GraphViewer graph_viewer(session.GetGraph()); - const auto& node_topology_list = graph_viewer.GetNodesInTopologicalOrder(); - EXPECT_EQ(graph_viewer.GetNode(node_topology_list[0])->OpType(), "Transpose"); - EXPECT_EQ(graph_viewer.GetNode(node_topology_list[1])->OpType(), "DequantizeLinear"); + const std::vector expected_op_types_in_order{ + "DequantizeLinear", + "Transpose", + "QuantizeLinear", "DequantizeLinear"}; + const auto op_types_in_order = GetNodeOpTypesInTopologicalOrder(session.GetGraph()); + EXPECT_EQ(op_types_in_order, expected_op_types_in_order); }; TransformerTester(build_test_case, check_graph, - TransformerLevel::Level1, - TransformerLevel::Level2); + TransformerLevel::Default, + TransformerLevel::Level1); }; test_case({1, 13, 13, 23}, {0, 2, 3, 1}); @@ -1844,17 +1967,17 @@ TEST(QDQTransformerTests, QDQPropagation_Per_Layer_No_Propagation) { }; auto check_graph = [&](InferenceSessionWrapper& session) { - auto op_to_count = CountOpsInGraph(session.GetGraph()); - GraphViewer graph_viewer(session.GetGraph()); - const auto& node_topology_list = graph_viewer.GetNodesInTopologicalOrder(); - EXPECT_EQ(graph_viewer.GetNode(node_topology_list[0])->OpType(), "DequantizeLinear"); - EXPECT_EQ(graph_viewer.GetNode(node_topology_list[1])->OpType(), "Transpose"); + const std::vector expected_op_types_in_order{ + "DequantizeLinear", + "Transpose"}; + const auto op_types_in_order = GetNodeOpTypesInTopologicalOrder(session.GetGraph()); + EXPECT_EQ(op_types_in_order, expected_op_types_in_order); }; TransformerTester(build_test_case, check_graph, - TransformerLevel::Level1, - TransformerLevel::Level2); + TransformerLevel::Default, + TransformerLevel::Level1); }; test_case({1, 13, 13, 23}, {0, 2, 3, 1}); @@ -1877,22 +2000,22 @@ TEST(QDQTransformerTests, QDQPropagation_DQ_Q) { }; auto check_graph = [&](InferenceSessionWrapper& session) { - auto op_to_count = CountOpsInGraph(session.GetGraph()); - EXPECT_EQ(op_to_count["QuantizeLinear"], 1); - EXPECT_EQ(op_to_count["DequantizeLinear"], 1); + const std::vector expected_op_types_in_order{ + "DequantizeLinear", + "QuantizeLinear"}; + const auto op_types_in_order = GetNodeOpTypesInTopologicalOrder(session.GetGraph()); + EXPECT_EQ(op_types_in_order, expected_op_types_in_order); }; TransformerTester(build_test_case, check_graph, - TransformerLevel::Level1, - TransformerLevel::Level2); + TransformerLevel::Default, + TransformerLevel::Level1); }; test_case({1, 13, 13, 23}); } -#endif // DISABLE_CONTRIB_OPS - TEST(QDQTransformerTests, QDQ_Selector_Test) { const ORTCHAR_T* model_file_name = ORT_TSTR("testdata/transform/qdq_conv.onnx"); diff --git a/onnxruntime/test/perftest/ort_test_session.cc b/onnxruntime/test/perftest/ort_test_session.cc index 2334f9e11a..2a288f8575 100644 --- a/onnxruntime/test/perftest/ort_test_session.cc +++ b/onnxruntime/test/perftest/ort_test_session.cc @@ -60,7 +60,7 @@ OnnxRuntimeTestSession::OnnxRuntimeTestSession(Ort::Env& env, std::random_device } else if (provider_name == onnxruntime::kTensorrtExecutionProvider) { #ifdef USE_TENSORRT int device_id = 0; - int trt_max_partition_iterations = 1000; + int trt_max_partition_iterations = 1000; int trt_min_subgraph_size = 1; size_t trt_max_workspace_size = 1 << 30; bool trt_fp16_enable = false; @@ -76,11 +76,11 @@ OnnxRuntimeTestSession::OnnxRuntimeTestSession(Ort::Env& env, std::random_device std::string trt_engine_decryption_lib_path = ""; bool trt_force_sequential_engine_build = false; - #ifdef _MSC_VER +#ifdef _MSC_VER std::string ov_string = ToUTF8String(performance_test_config.run_config.ep_runtime_config_string); - #else +#else std::string ov_string = performance_test_config.run_config.ep_runtime_config_string; - #endif +#endif std::istringstream ss(ov_string); std::string token; while (ss >> token) { @@ -215,7 +215,7 @@ OnnxRuntimeTestSession::OnnxRuntimeTestSession(Ort::Env& env, std::random_device tensorrt_options.has_user_compute_stream = 0; tensorrt_options.user_compute_stream = nullptr; tensorrt_options.trt_max_partition_iterations = trt_max_partition_iterations; - tensorrt_options.trt_min_subgraph_size = trt_min_subgraph_size; + tensorrt_options.trt_min_subgraph_size = trt_min_subgraph_size; tensorrt_options.trt_max_workspace_size = trt_max_workspace_size; tensorrt_options.trt_fp16_enable = trt_fp16_enable; tensorrt_options.trt_int8_enable = trt_int8_enable; @@ -232,9 +232,9 @@ OnnxRuntimeTestSession::OnnxRuntimeTestSession(Ort::Env& env, std::random_device session_options.AppendExecutionProvider_TensorRT_V2(tensorrt_options); OrtCUDAProviderOptions cuda_options; - cuda_options.device_id=device_id; - cuda_options.cudnn_conv_algo_search=static_cast(performance_test_config.run_config.cudnn_conv_algo); - cuda_options.do_copy_in_default_stream=!performance_test_config.run_config.do_cuda_copy_in_separate_stream; + cuda_options.device_id = device_id; + cuda_options.cudnn_conv_algo_search = static_cast(performance_test_config.run_config.cudnn_conv_algo); + cuda_options.do_copy_in_default_stream = !performance_test_config.run_config.do_cuda_copy_in_separate_stream; // TODO: Support arena configuration for users of perf test session_options.AppendExecutionProvider_CUDA(cuda_options); #else @@ -325,7 +325,11 @@ OnnxRuntimeTestSession::OnnxRuntimeTestSession(Ort::Env& env, std::random_device } else if (provider_name == onnxruntime::kNnapiExecutionProvider) { #ifdef USE_NNAPI uint32_t nnapi_flags = 0; +#ifdef _MSC_VER + std::string ov_string = ToUTF8String(performance_test_config.run_config.ep_runtime_config_string); +#else std::string ov_string = performance_test_config.run_config.ep_runtime_config_string; +#endif std::istringstream ss(ov_string); std::string key; while (ss >> key) {