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.
This commit is contained in:
Edward Chen 2022-02-14 14:20:03 -08:00 committed by GitHub
parent 7691e7ed12
commit 3199074ac7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 657 additions and 302 deletions

View file

@ -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"
)

View file

@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include <optional>
#include <string>
#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<NodeInfo> src;
/** Destination node info. If empty, the destination is a graph output. */
std::optional<NodeInfo> 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<NodeInfo>& 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<ExtendedGraphEdge> 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<size_t>(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<ExtendedGraphEdge> 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<size_t>(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

View file

@ -182,6 +182,11 @@ std::vector<std::unique_ptr<GraphTransformer>> GenerateTransformers(
session_options.free_dimension_overrides));
auto cpu_allocator = cpu_execution_provider.GetAllocator(0, OrtMemTypeDefault);
transformers.emplace_back(std::make_unique<TransposeOptimizer>(std::move(cpu_allocator)));
if (!disable_quant_qdq) {
transformers.emplace_back(std::make_unique<QDQPropagationTransformer>());
}
} break;
case TransformerLevel::Level2: {
@ -203,7 +208,6 @@ std::vector<std::unique_ptr<GraphTransformer>> GenerateTransformers(
if (!QDQIsInt8Allowed()) {
transformers.emplace_back(std::make_unique<QDQS8ToU8Transformer>(cpu_ep));
}
transformers.emplace_back(std::make_unique<QDQPropagationTransformer>(cpu_ep));
transformers.emplace_back(std::make_unique<QDQSelectorActionTransformer>());
}

View file

@ -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 <deque>
#include <optional>
#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<NodeArg*>{&data, &scale, zero_point}
: std::vector<NodeArg*>{&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<NodeIndex, int> 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<ExtendedGraphEdge> 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<NodeIndex, int> 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<ExtendedGraphEdge> 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<ExtendedGraphEdge> 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<ExtendedGraphEdge> 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<const NodeIndex> node_indices,
const std::unordered_set<std::string>& 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<NodeArg*>& 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<const NodeIndex> node_indices,
const std::unordered_set<std::string>& 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<NodeArg*>& 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

View file

@ -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

View file

@ -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;
}

View file

@ -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 <vector>
#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<const ONNX_NAMESPACE::TensorProto*(const std::string&)>& get_const_initializer,
const GetConstantInitializerFn& get_const_initializer,
const Path& model_path) {
ConstPointerContainer<std::vector<NodeArg*>> dq_input_defs = dq_node.InputDefs();
ConstPointerContainer<std::vector<NodeArg*>> q_input_defs = q_node.InputDefs();
@ -58,31 +58,56 @@ bool IsQDQPairSupported(
*q_scale.data<float>() == *dq_scale.data<float>();
}
bool IsDQSupported(
const Node& dq_node,
const std::function<const ONNX_NAMESPACE::TensorProto*(const std::string&)>& get_const_initializer) {
ConstPointerContainer<std::vector<NodeArg*>> 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

View file

@ -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<const ONNX_NAMESPACE::TensorProto*(const std::string&)>;
// 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<const ONNX_NAMESPACE::TensorProto*(const std::string&)>& 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<const ONNX_NAMESPACE::TensorProto*(const std::string&)>& 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

View file

@ -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 <functional>
#include <string>
#include <vector>
@ -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<void(ModelTestBuilder& helper)>& 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<OrtValue>& fetches, std::unique_ptr<GraphTransformer> transformer = nullptr) {
auto run_model = [&](TransformerLevel level, std::vector<OrtValue>& fetches,
std::unique_ptr<GraphTransformer> 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<int>(level)) + L".onnx";
session_options.optimized_model_filepath =
ToPathString("model" + std::to_string(static_cast<int>(level)) + ".onnx");
#endif
InferenceSessionWrapper session{session_options, GetEnvironment()};
ASSERT_TRUE(session.Load(model_data.data(), static_cast<int>(model_data.size())).IsOK());
ASSERT_STATUS_OK(session.Load(model_data.data(), static_cast<int>(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<void(ModelTestBuilder& helper)>& buil
};
std::vector<OrtValue> baseline_fetches;
run_model(baseline_level, baseline_fetches);
ASSERT_NO_FATAL_FAILURE(run_model(baseline_level, baseline_fetches));
std::vector<OrtValue> 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<COMPARE_RESULT, std::string> ret =

View file

@ -172,11 +172,9 @@ class ModelTestBuilder {
return AddNode("DequantizeLinear", input_args, {output_arg});
}
template <typename T>
typename std::enable_if<IsTypeDequantLinearCompatible<T>::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<NodeArg*> input_args;
input_args.push_back(input_arg);
input_args.push_back(MakeScalarInitializer<float>(input_scale));

View file

@ -34,7 +34,17 @@
namespace onnxruntime {
namespace test {
#ifndef DISABLE_CONTRIB_OPS
static std::vector<std::string> GetNodeOpTypesInTopologicalOrder(const Graph& graph) {
std::vector<std::string> 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 <typename InputType, typename WeightType, typename BiasType, typename OutputType>
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<std::string> 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<int64_t>& input_shape, size_t maxpool_dim, const std::vector<int64_t>& perms) {
#endif // !defined(DISABLE_CONTRIB_OPS)
TEST(QDQTransformerTests, QDQPropagation_QBackward) {
auto test_case = [&](const std::vector<int64_t>& input_shape,
size_t maxpool_dim,
const std::vector<int64_t>& perms,
bool add_op_boundary,
bool include_zp) {
auto build_test_case = [&](ModelTestBuilder& builder) {
auto* input_arg = builder.MakeInput<float>(input_shape, -1.f, 1.f);
auto* output_arg = builder.MakeOutput();
// add QDQ
auto* qdq_output = AddQDQNodePair<uint8_t>(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<uint8_t>(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<int64_t> pads((maxpool_dim - 2) * 2, 1);
pool_node.AddAttribute("pads", pads);
std::vector<int64_t> kernel_shape(maxpool_dim - 2, 3);
pool_node.AddAttribute("kernel_shape", kernel_shape);
// Reshape
auto* reshape_output = builder.MakeIntermediate();
auto* reshape_shape = builder.Make1DInitializer<int64_t>({-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<uint8_t>(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<std::string> 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<int64_t>& input_shape,
size_t maxpool_dim,
const std::vector<int64_t>& perms,
bool add_op_boundary,
bool include_zp) {
auto build_test_case = [&](ModelTestBuilder& builder) {
auto* input_arg = builder.MakeInput<uint8_t>(input_shape,
std::numeric_limits<uint8_t>::min(),
std::numeric_limits<uint8_t>::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<uint8_t>(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<int64_t> pads((maxpool_dim - 2) * 2, 1);
pool_node.AddAttribute("pads", pads);
std::vector<int64_t> kernel_shape(maxpool_dim - 2, 3);
@ -1701,28 +1800,47 @@ TEST(QDQTransformerTests, QDQPropagation_QDQCancelOut) {
// Reshape
auto* reshape_shape = builder.Make1DInitializer<int64_t>({-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<std::string> 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<int64_t>& input_shape, bool same_scale, bool same_zp) {
auto build_test_case = [&](ModelTestBuilder& builder) {
auto* input_arg = builder.MakeInput<float>(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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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");

View file

@ -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<OrtCudnnConvAlgoSearch>(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<OrtCudnnConvAlgoSearch>(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) {