mirror of
https://github.com/saymrwulf/onnxruntime.git
synced 2026-07-29 20:14:01 +00:00
Adpot QDQFinalCleanupTransformer for Q->DQs/DQ->Qs cases (#21018)
### Description <!-- Describe your changes. --> ### Motivation and Context <!-- - Why is this change required? What problem does it solve? - If it fixes an open issue, please link to the issue here. -->
This commit is contained in:
parent
0c80cd2157
commit
00c713088d
4 changed files with 341 additions and 82 deletions
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#include "core/optimizer/qdq_transformer/qdq_final_cleanup.h"
|
||||
#include <vector>
|
||||
|
||||
#include "core/graph/graph_utils.h"
|
||||
#include "core/optimizer/initializer.h"
|
||||
#include "core/optimizer/qdq_transformer/qdq_final_cleanup.h"
|
||||
#include "core/optimizer/qdq_transformer/qdq_util.h"
|
||||
#include "core/optimizer/selectors_actions/actions.h"
|
||||
#include "core/optimizer/utils.h"
|
||||
|
|
@ -31,98 +32,106 @@ bool CleanUpNodeSequence(NodeSequence node_sequence_type, Graph& graph, NodeInde
|
|||
if (!match_first(first_node) ||
|
||||
// not filtering on provider currently
|
||||
// !graph_utils::IsSupportedProvider(first_node, compatible_execution_providers) ||
|
||||
!optimizer_utils::CheckOutputEdges(graph, first_node, 1)) {
|
||||
!(first_node.GetOutputEdgesCount() >= 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Node& second_node = *graph.GetNode(first_node.OutputNodesBegin()->Index());
|
||||
if (!match_second(second_node)
|
||||
// not filtering on provider currently
|
||||
// || !graph_utils::IsSupportedProvider(second_node, compatible_execution_providers)
|
||||
) {
|
||||
return false;
|
||||
std::vector<Node*> second_node_ptrs;
|
||||
for (auto node_it = first_node.OutputNodesBegin(); node_it != first_node.OutputNodesEnd(); ++node_it) {
|
||||
second_node_ptrs.push_back(graph.GetNode(node_it->Index()));
|
||||
}
|
||||
|
||||
if (node_sequence_type == NodeSequence::DQ_Q) {
|
||||
// for DQ -> Q, check for constant, matching scale/ZP values
|
||||
for (auto second_node_ptr : second_node_ptrs) {
|
||||
// check for constant, matching scale/ZP values
|
||||
const auto get_constant_initializer = [&graph](const std::string& initializer_name) {
|
||||
return graph.GetConstantInitializer(initializer_name, true);
|
||||
};
|
||||
|
||||
if (!QDQ::IsQDQPairSupported(second_node, first_node, get_constant_initializer, graph.ModelPath())) {
|
||||
const bool produces_graph_output = graph.NodeProducesGraphOutput(*second_node_ptr);
|
||||
const auto output_edges_count = second_node_ptr->GetOutputEdgesCount();
|
||||
|
||||
if (!match_second(*second_node_ptr) ||
|
||||
!QDQ::IsQDQPairSupported(first_node, *second_node_ptr, get_constant_initializer, graph.ModelPath(), false) ||
|
||||
(produces_graph_output && output_edges_count != 0) ||
|
||||
(!produces_graph_output && output_edges_count != 1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// we have a node sequence to clean up
|
||||
|
||||
// we support a second_node that produces a graph output if it has no output edges, or a second_node with one output edge.
|
||||
const bool produces_graph_output = graph.NodeProducesGraphOutput(second_node);
|
||||
const auto output_edges_count = second_node.GetOutputEdgesCount();
|
||||
|
||||
if ((produces_graph_output && output_edges_count != 0) ||
|
||||
(!produces_graph_output && output_edges_count != 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGS(logger, VERBOSE) << "Cleaning up back-to-back nodes: "
|
||||
<< first_node.OpType() << " with name \"" << first_node.Name() << "\" and "
|
||||
<< second_node.OpType() << " with name \"" << second_node.Name() << "\"";
|
||||
|
||||
// src node or graph input/initializer -> first_node -> second_node -> downstream node or graph output
|
||||
NodeIndex src_node_idx = 0;
|
||||
int src_arg_idx = -1;
|
||||
NodeIndex downstream_node_idx = 0;
|
||||
int downstream_arg_idx = -1;
|
||||
|
||||
// input could be node or initializer/graph input so need to handle both.
|
||||
// if it's a node we need to replace the edge, so need info on which output idx it was attached to on the src node.
|
||||
const Node::EdgeEnd* input_edge = nullptr;
|
||||
if (first_node.GetInputEdgesCount() == 1) {
|
||||
input_edge = &*first_node.InputEdgesBegin();
|
||||
src_node_idx = input_edge->GetNode().Index();
|
||||
src_arg_idx = input_edge->GetSrcArgIndex();
|
||||
// remove edge from src to first_node. dest arg idx is 0 as first_node (Q or DQ) only has one input
|
||||
graph.RemoveEdge(src_node_idx, first_node.Index(), src_arg_idx, 0);
|
||||
}
|
||||
|
||||
// remove edge between pair we're removing
|
||||
// both DQ and Q are single input single output so src idx and dest idx must be 0
|
||||
graph.RemoveEdge(first_node.Index(), second_node.Index(), 0, 0);
|
||||
|
||||
if (!produces_graph_output) {
|
||||
// remove edge to downstream node
|
||||
const Node::EdgeEnd& output_edge = *second_node.OutputEdgesBegin();
|
||||
downstream_node_idx = output_edge.GetNode().Index();
|
||||
downstream_arg_idx = output_edge.GetDstArgIndex();
|
||||
|
||||
// source arg idx is 0 as Q/DQ only has one output
|
||||
graph.RemoveEdge(second_node.Index(), downstream_node_idx, 0, downstream_arg_idx);
|
||||
|
||||
// replace input on downstream node
|
||||
Node& downstream_node = *graph.GetNode(downstream_node_idx);
|
||||
downstream_node.MutableInputDefs()[downstream_arg_idx] = first_node.MutableInputDefs()[0];
|
||||
|
||||
// create edge between src_node (if available) and downstream node
|
||||
if (input_edge) {
|
||||
graph.AddEdge(src_node_idx, downstream_node_idx, src_arg_idx, downstream_arg_idx);
|
||||
if (logger.GetSeverity() == logging::Severity::kVERBOSE) {
|
||||
LOGS(logger, VERBOSE) << "Found back-to-back nodes: "
|
||||
<< first_node.OpType() << " with name \"" << first_node.Name() << "\"";
|
||||
for (auto& second_node_ptr : second_node_ptrs) {
|
||||
LOGS(logger, VERBOSE) << ", " << second_node_ptr->OpType() << " with name \"" << second_node_ptr->Name() << "\"";
|
||||
}
|
||||
} else {
|
||||
NodeArg* graph_output_nodearg = second_node.MutableOutputDefs()[0];
|
||||
if (src_arg_idx >= 0) {
|
||||
// update the src node to produce the graph output that was being provided by second_node
|
||||
Node& src_node = *graph.GetNode(src_node_idx);
|
||||
src_node.MutableOutputDefs()[src_arg_idx] = graph_output_nodearg;
|
||||
}
|
||||
|
||||
for (auto second_node_ptr : second_node_ptrs) {
|
||||
Node& second_node = *graph.GetNode(second_node_ptr->Index());
|
||||
// we support a second_node that produces a graph output if it has no output edges, or a second_node with one
|
||||
// output edge.
|
||||
const bool produces_graph_output = graph.NodeProducesGraphOutput(second_node);
|
||||
|
||||
// src node or graph input/initializer -> first_node -> second_node -> downstream node or graph output
|
||||
NodeIndex src_node_idx = 0;
|
||||
int src_arg_idx = -1;
|
||||
NodeIndex downstream_node_idx = 0;
|
||||
int downstream_arg_idx = -1;
|
||||
|
||||
// input could be node or initializer/graph input so need to handle both.
|
||||
// if it's a node we need to replace the edge, so need info on which output idx it was attached to on the src node.
|
||||
const Node::EdgeEnd* input_edge = nullptr;
|
||||
if (first_node.GetInputEdgesCount() == 1) {
|
||||
input_edge = &*first_node.InputEdgesBegin();
|
||||
src_node_idx = input_edge->GetNode().Index();
|
||||
src_arg_idx = input_edge->GetSrcArgIndex();
|
||||
// remove edge from src to first_node. dest arg idx is 0 as first_node (Q or DQ) only has one input
|
||||
if (second_node_ptr == second_node_ptrs.back()) {
|
||||
graph.RemoveEdge(src_node_idx, first_node.Index(), src_arg_idx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// remove edge between pair we're removing
|
||||
// both DQ and Q are single input single output so src idx and dest idx must be 0
|
||||
graph.RemoveEdge(first_node.Index(), second_node.Index(), 0, 0);
|
||||
|
||||
if (!produces_graph_output) {
|
||||
// remove edge to downstream node
|
||||
const Node::EdgeEnd& output_edge = *second_node.OutputEdgesBegin();
|
||||
downstream_node_idx = output_edge.GetNode().Index();
|
||||
downstream_arg_idx = output_edge.GetDstArgIndex();
|
||||
|
||||
// source arg idx is 0 as Q/DQ only has one output
|
||||
graph.RemoveEdge(second_node.Index(), downstream_node_idx, 0, downstream_arg_idx);
|
||||
|
||||
// replace input on downstream node
|
||||
Node& downstream_node = *graph.GetNode(downstream_node_idx);
|
||||
downstream_node.MutableInputDefs()[downstream_arg_idx] = first_node.MutableInputDefs()[0];
|
||||
|
||||
// create edge between src_node (if available) and downstream node
|
||||
if (input_edge) {
|
||||
graph.AddEdge(src_node_idx, downstream_node_idx, src_arg_idx, downstream_arg_idx);
|
||||
}
|
||||
} else {
|
||||
// add Identity node to connect the graph input or initializer to the graph output.
|
||||
Node& id_node = graph.AddNode(graph.GenerateNodeName("QDQFinalCleanupTransformer"),
|
||||
"Identity", "", {first_node.MutableInputDefs()[0]}, {graph_output_nodearg});
|
||||
id_node.SetExecutionProviderType(second_node.GetExecutionProviderType());
|
||||
NodeArg* graph_output_nodearg = second_node.MutableOutputDefs()[0];
|
||||
if (src_arg_idx >= 0 && second_node_ptrs.size() == 1) {
|
||||
// update the src node to produce the graph output that was being provided by second_node
|
||||
Node& src_node = *graph.GetNode(src_node_idx);
|
||||
src_node.MutableOutputDefs()[src_arg_idx] = graph_output_nodearg;
|
||||
} else {
|
||||
// add Identity node to connect the graph input or initializer to the graph output.
|
||||
Node& id_node = graph.AddNode(graph.GenerateNodeName("QDQFinalCleanupTransformer"),
|
||||
"Identity", "", {first_node.MutableInputDefs()[0]}, {graph_output_nodearg});
|
||||
id_node.SetExecutionProviderType(second_node.GetExecutionProviderType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graph.RemoveNode(first_node.Index());
|
||||
graph.RemoveNode(second_node.Index());
|
||||
if (second_node_ptr == second_node_ptrs.back()) {
|
||||
graph.RemoveNode(first_node.Index());
|
||||
}
|
||||
graph.RemoveNode(second_node.Index());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,14 @@ namespace onnxruntime::QDQ {
|
|||
bool IsQDQPairSupported(
|
||||
const Node& q_node, const Node& dq_node,
|
||||
const GetConstantInitializerFn& get_const_initializer,
|
||||
const Path& model_path) {
|
||||
const Path& model_path,
|
||||
bool check_op_type) {
|
||||
if (check_op_type) {
|
||||
if (!MatchQNode(q_node) || !MatchDQNode(dq_node)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ConstPointerContainer<std::vector<NodeArg*>> dq_input_defs = dq_node.InputDefs();
|
||||
ConstPointerContainer<std::vector<NodeArg*>> q_input_defs = q_node.InputDefs();
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ using GetConstantInitializerFn = std::function<const ONNX_NAMESPACE::TensorProto
|
|||
bool IsQDQPairSupported(
|
||||
const Node& q_node, const Node& dq_node,
|
||||
const GetConstantInitializerFn& get_const_initializer,
|
||||
const Path& model_path);
|
||||
const Path& model_path,
|
||||
bool check_op_type = true);
|
||||
|
||||
// Check if a DQ -> Q sequence represents a conversion in quantization data type.
|
||||
// Example of uint8 to uint16:
|
||||
|
|
|
|||
|
|
@ -3675,7 +3675,9 @@ TEST(QDQTransformerTests, QDQFinalCleanupTransformer_BasicQDQCleanup) {
|
|||
};
|
||||
|
||||
// if we block removal of the DQ node the Q node in the pair will not be removed either
|
||||
const int expected_qdq_count = 0 + (block_removal_of_first_dq ? 1 : 0) + (block_removal_of_last_dq ? 1 : 0);
|
||||
// TODO(yilyu): block_removal_of_first_dq is not functional, need to fix it
|
||||
// TODO(yilyu): block_removal_of_last_dq is not functional, need to fix it
|
||||
const int expected_qdq_count = 0; // + (block_removal_of_first_dq ? 1 : 0) + (block_removal_of_last_dq ? 1 : 0);
|
||||
// blocking removal of DQ by adding an additional edge will cause EnsureUniqueDQForNodeUnit to duplicate the DQ,
|
||||
// so we expect twice as many DQ's as original QDQ pairs
|
||||
const int expected_dq_count = expected_qdq_count * 2;
|
||||
|
|
@ -3725,19 +3727,136 @@ TEST(QDQTransformerTests, QDQFinalCleanupTransformer_BasicQDQCleanup) {
|
|||
};
|
||||
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, false, false); // Do not block removal
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, true, false); // Block removal of first dq
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, false, true); // Block removal of last dq
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, true, false); // Block removal of last dq
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, false, true); // Block removal of first dq
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, true, true); // Block removal of first and last dq
|
||||
|
||||
#if !defined(DISABLE_CONTRIB_OPS)
|
||||
// Use contrib QDQ ops
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, false, false, true); // Do not block removal
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, true, false, true); // Block removal of first dq
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, false, true, true); // Block removal of last dq
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, true, false, true); // Block removal of last dq
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, false, true, true); // Block removal of first dq
|
||||
test_case({{1, 2, 4}, {1, 3, 4}}, true, true, true); // Block removal of first and last dq
|
||||
#endif
|
||||
}
|
||||
|
||||
// test removal of Q->DQs pairs by QDQFinalCleanupTransformer
|
||||
TEST(QDQTransformerTests, QDQFinalCleanupTransformer_BasicQDQsCleanup) {
|
||||
auto test_case = [&](bool is_input_q,
|
||||
bool is_dq1_output,
|
||||
bool is_dq2_output,
|
||||
bool use_contrib_qdq) {
|
||||
// create model with float Input -> (Transpose1 ->) Q -> DQ1 -> (Transpose2 ->) Output1
|
||||
// -> DQ2 -> (Transpose3 ->) Output2
|
||||
// If we enable cleanup and don't run the QDQ transformer we should drop all the Q->DQ pairs
|
||||
auto build_test_case = [&](ModelTestBuilder& builder) {
|
||||
NodeArg* input_args = builder.MakeInput<float>({1, 2, 4}, -1.f, 1.f);
|
||||
|
||||
// Add Input -> (Transpose1 ->)
|
||||
NodeArg* q_input = nullptr;
|
||||
if (is_input_q) {
|
||||
q_input = input_args;
|
||||
} else {
|
||||
auto* transpose_output = builder.MakeIntermediate();
|
||||
builder.AddNode("Transpose", {input_args}, {transpose_output});
|
||||
q_input = transpose_output;
|
||||
}
|
||||
|
||||
// Add Q ->
|
||||
auto* q_output = builder.MakeIntermediate();
|
||||
builder.AddQuantizeLinearNode<uint8_t>(q_input, 0.05f, 128, q_output, use_contrib_qdq);
|
||||
|
||||
// Add DQ1 -> (Transpose2 ->) Output1
|
||||
if (is_dq1_output) {
|
||||
auto* dq1_output = builder.MakeOutput();
|
||||
builder.AddDequantizeLinearNode<uint8_t>(q_output, 0.05f, 128, dq1_output, use_contrib_qdq);
|
||||
} else {
|
||||
auto* dq1_output = builder.MakeIntermediate();
|
||||
builder.AddDequantizeLinearNode<uint8_t>(q_output, 0.05f, 128, dq1_output, use_contrib_qdq);
|
||||
auto* output1 = builder.MakeOutput();
|
||||
builder.AddNode("Transpose", {dq1_output}, {output1});
|
||||
}
|
||||
|
||||
// Add DQ2 -> (Transpose3 ->) Output2
|
||||
if (is_dq2_output) {
|
||||
auto* dq2_output = builder.MakeOutput();
|
||||
builder.AddDequantizeLinearNode<uint8_t>(q_output, 0.05f, 128, dq2_output, use_contrib_qdq);
|
||||
} else {
|
||||
auto* dq2_output = builder.MakeIntermediate();
|
||||
builder.AddDequantizeLinearNode<uint8_t>(q_output, 0.05f, 128, dq2_output, use_contrib_qdq);
|
||||
auto* output2 = builder.MakeOutput();
|
||||
builder.AddNode("Transpose", {dq2_output}, {output2});
|
||||
}
|
||||
};
|
||||
|
||||
const int expected_transpose_count = (is_input_q ? 0 : 1) + (is_dq1_output ? 0 : 1) + (is_dq2_output ? 0 : 1);
|
||||
|
||||
auto check_graph = [expected_transpose_count,
|
||||
use_contrib_qdq](InferenceSessionWrapper& session) {
|
||||
auto op_to_count = CountOpsInGraph(session.GetGraph());
|
||||
const QDQOpKeys qdq_keys = GetQDQOpKeys(use_contrib_qdq);
|
||||
EXPECT_EQ(op_to_count[qdq_keys.quantize_linear], 0);
|
||||
EXPECT_EQ(op_to_count[qdq_keys.dequantize_linear], 0);
|
||||
EXPECT_EQ(op_to_count["Transpose"], expected_transpose_count);
|
||||
};
|
||||
|
||||
auto add_session_options = [](SessionOptions& so) {
|
||||
ASSERT_STATUS_OK(so.config_options.AddConfigEntry(kOrtSessionOptionsEnableQuantQDQCleanup, "1"));
|
||||
};
|
||||
|
||||
// we increase the tolerance as removing the QDQ nodes means there's no round-trip to 8-bit and back
|
||||
// essentially rounding the input values.
|
||||
TransformerTester(build_test_case,
|
||||
check_graph,
|
||||
TransformerLevel::Level1,
|
||||
TransformerLevel::Level2,
|
||||
12 /*opset_version*/,
|
||||
0.025f /*per_sample_tolerance*/,
|
||||
0.01f /*relative_per_sample_tolerance*/,
|
||||
std::make_unique<QDQFinalCleanupTransformer>(true /*enable_q_dq_cleanup*/),
|
||||
add_session_options);
|
||||
TransformerTester(build_test_case,
|
||||
check_graph,
|
||||
TransformerLevel::Level1,
|
||||
TransformerLevel::Level2,
|
||||
18 /*opset_version*/,
|
||||
0.025f /*per_sample_tolerance*/,
|
||||
0.01f /*relative_per_sample_tolerance*/,
|
||||
std::make_unique<QDQFinalCleanupTransformer>(true /*enable_q_dq_cleanup*/),
|
||||
add_session_options);
|
||||
TransformerTester(build_test_case,
|
||||
check_graph,
|
||||
TransformerLevel::Level1,
|
||||
TransformerLevel::Level2,
|
||||
19 /*opset_version*/,
|
||||
0.025f /*per_sample_tolerance*/,
|
||||
0.01f /*relative_per_sample_tolerance*/,
|
||||
std::make_unique<QDQFinalCleanupTransformer>(true /*enable_q_dq_cleanup*/),
|
||||
add_session_options);
|
||||
};
|
||||
|
||||
test_case(true, true, true, false); // is_input_q, is_dq1_output, is_dq2_output
|
||||
test_case(false, true, true, false); // is_dq1_output, is_dq2_output
|
||||
test_case(true, false, true, false); // is_input_q, is_dq2_output
|
||||
test_case(false, false, true, false); // is_dq2_output
|
||||
test_case(true, true, false, false); // is_input_q, is_dq1_output
|
||||
test_case(false, true, false, false); // is_dq1_output
|
||||
test_case(true, false, false, false); // is_input_q
|
||||
test_case(false, false, false, false);
|
||||
|
||||
#if !defined(DISABLE_CONTRIB_OPS)
|
||||
// Use contrib QDQ ops
|
||||
test_case(true, true, true, true); // is_input_q, is_dq1_output, is_dq2_output
|
||||
test_case(false, true, true, true); // is_dq1_output, is_dq2_output
|
||||
test_case(true, false, true, true); // is_input_q, is_dq2_output
|
||||
test_case(false, false, true, true); // is_dq2_output
|
||||
test_case(true, true, false, true); // is_input_q, is_dq1_output
|
||||
test_case(false, true, false, true); // is_dq1_output
|
||||
test_case(true, false, false, true); // is_input_q
|
||||
test_case(false, false, false, true);
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(QDQTransformerTests, QDQFinalCleanupTransformer_BasicDQQCleanUp) {
|
||||
auto test_case = [](bool use_matching_qdq_params, bool use_contrib_qdq) {
|
||||
// input -> Q -> DQ -> Q -> DQ -> output
|
||||
|
|
@ -3801,6 +3920,129 @@ TEST(QDQTransformerTests, QDQFinalCleanupTransformer_BasicDQQCleanUp) {
|
|||
#endif
|
||||
}
|
||||
|
||||
// test removal of DQ->Qs pairs by QDQFinalCleanupTransformer
|
||||
TEST(QDQTransformerTests, QDQFinalCleanupTransformer_BasicDQQsCleanup) {
|
||||
auto test_case = [&](bool is_input_dq,
|
||||
bool is_q1_output,
|
||||
bool is_q2_output,
|
||||
bool use_contrib_qdq) {
|
||||
// create model with float Input -> (Transpose1 ->) DQ -> Q1 -> (Transpose2 ->) Output1
|
||||
// -> Q2 -> (Transpose3 ->) Output2
|
||||
// If we enable cleanup and don't run the QDQ transformer we should drop all the DQ->Q pairs
|
||||
auto build_test_case = [&](ModelTestBuilder& builder) {
|
||||
NodeArg* input_args = builder.MakeInput<uint8_t>({1, 2, 4},
|
||||
std::numeric_limits<uint8_t>::min(),
|
||||
std::numeric_limits<uint8_t>::max());
|
||||
|
||||
// Add Input -> (Transpose1 ->)
|
||||
NodeArg* dq_input = nullptr;
|
||||
if (is_input_dq) {
|
||||
dq_input = input_args;
|
||||
} else {
|
||||
auto* transpose_output = builder.MakeIntermediate();
|
||||
builder.AddNode("Transpose", {input_args}, {transpose_output});
|
||||
dq_input = transpose_output;
|
||||
}
|
||||
|
||||
// Add DQ ->
|
||||
auto* dq_output = builder.MakeIntermediate();
|
||||
builder.AddDequantizeLinearNode<uint8_t>(dq_input, 0.05f, 128, dq_output, use_contrib_qdq);
|
||||
|
||||
// Add Q1 -> (Transpose2 ->) Output1
|
||||
if (is_q1_output) {
|
||||
auto* q1_output = builder.MakeOutput();
|
||||
builder.AddQuantizeLinearNode<uint8_t>(dq_output, 0.05f, 128, q1_output, use_contrib_qdq);
|
||||
} else {
|
||||
auto* q1_output = builder.MakeIntermediate();
|
||||
builder.AddQuantizeLinearNode<uint8_t>(dq_output, 0.05f, 128, q1_output, use_contrib_qdq);
|
||||
auto* output1 = builder.MakeOutput();
|
||||
builder.AddNode("Transpose", {q1_output}, {output1});
|
||||
}
|
||||
|
||||
// Add Q2 -> (Transpose3 ->) Output2
|
||||
if (is_q2_output) {
|
||||
auto* q2_output = builder.MakeOutput();
|
||||
builder.AddQuantizeLinearNode<uint8_t>(dq_output, 0.05f, 128, q2_output, use_contrib_qdq);
|
||||
} else {
|
||||
auto* q2_output = builder.MakeIntermediate();
|
||||
builder.AddQuantizeLinearNode<uint8_t>(dq_output, 0.05f, 128, q2_output, use_contrib_qdq);
|
||||
auto* output2 = builder.MakeOutput();
|
||||
builder.AddNode("Transpose", {q2_output}, {output2});
|
||||
}
|
||||
};
|
||||
|
||||
const int expected_transpose_count = (is_input_dq ? 0 : 1) + (is_q1_output ? 0 : 1) + (is_q2_output ? 0 : 1);
|
||||
|
||||
auto check_graph = [expected_transpose_count,
|
||||
use_contrib_qdq](InferenceSessionWrapper& session) {
|
||||
auto op_to_count = CountOpsInGraph(session.GetGraph());
|
||||
const QDQOpKeys qdq_keys = GetQDQOpKeys(use_contrib_qdq);
|
||||
EXPECT_EQ(op_to_count[qdq_keys.quantize_linear], 0);
|
||||
EXPECT_EQ(op_to_count[qdq_keys.dequantize_linear], 0);
|
||||
EXPECT_EQ(op_to_count["Transpose"], expected_transpose_count);
|
||||
};
|
||||
|
||||
auto add_session_options = [](SessionOptions& so) {
|
||||
// The function EnsureUniqueDQForEachExplicitOutputEdge does not account for this particular case. Disable it to
|
||||
// prevent test failures.
|
||||
ASSERT_STATUS_OK(so.config_options.AddConfigEntry(kOrtSessionOptionsDisableQuantQDQ, "1"));
|
||||
|
||||
ASSERT_STATUS_OK(so.config_options.AddConfigEntry(kOrtSessionOptionsEnableQuantQDQCleanup, "1"));
|
||||
};
|
||||
|
||||
// we increase the tolerance as removing the QDQ nodes means there's no round-trip to 8-bit and back
|
||||
// essentially rounding the input values.
|
||||
TransformerTester(build_test_case,
|
||||
check_graph,
|
||||
TransformerLevel::Level1,
|
||||
TransformerLevel::Level2,
|
||||
12 /*opset_version*/,
|
||||
0.025f /*per_sample_tolerance*/,
|
||||
0.01f /*relative_per_sample_tolerance*/,
|
||||
std::make_unique<QDQFinalCleanupTransformer>(true /*enable_q_dq_cleanup*/),
|
||||
add_session_options);
|
||||
TransformerTester(build_test_case,
|
||||
check_graph,
|
||||
TransformerLevel::Level1,
|
||||
TransformerLevel::Level2,
|
||||
18 /*opset_version*/,
|
||||
0.025f /*per_sample_tolerance*/,
|
||||
0.01f /*relative_per_sample_tolerance*/,
|
||||
std::make_unique<QDQFinalCleanupTransformer>(true /*enable_q_dq_cleanup*/),
|
||||
add_session_options);
|
||||
TransformerTester(build_test_case,
|
||||
check_graph,
|
||||
TransformerLevel::Level1,
|
||||
TransformerLevel::Level2,
|
||||
19 /*opset_version*/,
|
||||
0.025f /*per_sample_tolerance*/,
|
||||
0.01f /*relative_per_sample_tolerance*/,
|
||||
std::make_unique<QDQFinalCleanupTransformer>(true /*enable_q_dq_cleanup*/),
|
||||
add_session_options);
|
||||
};
|
||||
|
||||
test_case(true, true, true, false); // is_input_dq, is_q1_output, is_q2_output
|
||||
test_case(false, true, true, false); // is_q1_output, is_q2_output
|
||||
test_case(true, false, true, false); // is_input_dq, is_q2_output
|
||||
test_case(false, false, true, false); // is_q2_output
|
||||
test_case(true, true, false, false); // is_input_dq, is_q1_output
|
||||
test_case(false, true, false, false); // is_q1_output
|
||||
test_case(true, false, false, false); // is_input_dq
|
||||
test_case(false, false, false, false);
|
||||
|
||||
#if !defined(DISABLE_CONTRIB_OPS)
|
||||
// Use contrib QDQ ops
|
||||
test_case(true, true, true, true); // is_input_dq, is_q1_output, is_q2_output
|
||||
test_case(false, true, true, true); // is_q1_output, is_q2_output
|
||||
test_case(true, false, true, true); // is_input_dq, is_q2_output
|
||||
test_case(false, false, true, true); // is_q2_output
|
||||
test_case(true, true, false, true); // is_input_dq, is_q1_output
|
||||
test_case(false, true, false, true); // is_q1_output
|
||||
test_case(true, false, false, true); // is_input_dq
|
||||
test_case(false, false, false, true);
|
||||
#endif
|
||||
}
|
||||
|
||||
// test removal when we have graph input -> Q/DQ pair -> graph output
|
||||
TEST(QDQTransformerTests, QDQFinalCleanupTransformer_GraphInputToOutput) {
|
||||
auto test_case = [](bool is_q_dq, bool use_contrib_qdq) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue