Skip to content

Commit c1da27c

Browse files
[QNN EP] Add Case-2 LPBQ pattern support for Gemm and Matmul nodes (microsoft#25865)
### Description - Case-2 LPBQ pattern omits QuantizeLinear node in LPBQ packing pattern - Modify LPBQ fusion logic in QNN EP implemented for Gemma and MatMul nodes to gracefully handle the optional QuantizeLinear node in LPBQ packing pattern. - Add unit tests to verify Case-2 LPBQ pattern fusion for Gemm and MatMul nodes. ### Motivation and Context - QuantizeLinear node in LowPowerBlockQuantization encoding packing pattern can be optional as it helps to keep the weights in INT datatype and further helps to reduce the size of model. --------- Co-authored-by: tirupath-qti <tirupath@qti.qualcomm.com>
1 parent e6110d0 commit c1da27c

8 files changed

Lines changed: 614 additions & 147 deletions

File tree

onnxruntime/core/providers/qnn/builder/qnn_node_group/lpbqgemm_fusion.cc

Lines changed: 102 additions & 61 deletions
Large diffs are not rendered by default.

onnxruntime/core/providers/qnn/builder/qnn_node_group/lpbqgemm_fusion.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class QnnModelWrapper;
2424
class LowPowerBlockQuantizedGemmFusion : public IQnnNodeGroup {
2525
public:
2626
LowPowerBlockQuantizedGemmFusion(const NodeUnit& Scale_DQL_node_unit,
27-
const NodeUnit& W_QL_node_unit,
27+
const NodeUnit* W_QL_node_unit,
2828
const NodeUnit& W_DQL_node_unit,
2929
const NodeUnit& Act_DQL_node_unit,
3030
const NodeUnit& Gemm_node_unit,
@@ -46,7 +46,8 @@ class LowPowerBlockQuantizedGemmFusion : public IQnnNodeGroup {
4646

4747
private:
4848
std::array<const NodeUnit*, 6> node_units_;
49+
std::vector<const NodeUnit*> filtered_node_units_;
4950
};
5051

5152
} // namespace qnn
52-
} // namespace onnxruntime
53+
} // namespace onnxruntime

onnxruntime/core/providers/qnn/builder/qnn_node_group/lpbqmatmul_fusion.cc

Lines changed: 174 additions & 82 deletions
Large diffs are not rendered by default.

onnxruntime/core/providers/qnn/builder/qnn_node_group/lpbqmatmul_fusion.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class QnnModelWrapper;
2424
class LowPowerBlockQuantizedMatMulFusion : public IQnnNodeGroup {
2525
public:
2626
LowPowerBlockQuantizedMatMulFusion(const NodeUnit& Scale_DQL_node_unit,
27-
const NodeUnit& W_QL_node_unit,
27+
const NodeUnit* W_QL_node_unit,
2828
const NodeUnit& MatMul_node_unit);
2929
ORT_DISALLOW_COPY_AND_ASSIGNMENT(LowPowerBlockQuantizedMatMulFusion);
3030

@@ -43,7 +43,8 @@ class LowPowerBlockQuantizedMatMulFusion : public IQnnNodeGroup {
4343

4444
private:
4545
std::array<const NodeUnit*, 3> node_units_;
46+
std::vector<const NodeUnit*> filtered_node_units_;
4647
};
4748

4849
} // namespace qnn
49-
} // namespace onnxruntime
50+
} // namespace onnxruntime

onnxruntime/core/providers/qnn/builder/qnn_node_group/utils.cc

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,5 +314,62 @@ const NodeUnit* GetOnlyChildOfOutput(const GraphViewer& graph_viewer,
314314
return (child_count == 1) ? p_child_node_unit : nullptr;
315315
}
316316

317+
const NodeUnit* GetParentOfInputByName(const GraphViewer& graph_viewer,
318+
const NodeUnit& node_unit,
319+
const std::string& input_name,
320+
const std::unordered_map<const Node*, const NodeUnit*>& node_unit_map,
321+
const std::unordered_map<const NodeUnit*, const IQnnNodeGroup*>& qnn_node_group_map) {
322+
// Iterate through all nodes in the group
323+
for (auto node : node_unit.GetAllNodesInGroup()) {
324+
// Check if this node has the input we're looking for
325+
bool has_input = std::any_of(node->InputDefs().begin(), node->InputDefs().end(),
326+
[&](const NodeArg* node_input) {
327+
return node_input->Name() == input_name;
328+
});
329+
if (!has_input) {
330+
continue;
331+
}
332+
333+
const Node& child_node = *node;
334+
335+
for (auto edge = child_node.InputEdgesBegin(); edge != child_node.InputEdgesEnd(); ++edge) {
336+
const Node& parent_node = edge->GetNode();
337+
if (parent_node.OutputDefs()[0]->Name() != input_name) {
338+
continue;
339+
}
340+
341+
if (graph_viewer.GetNode(parent_node.Index()) == nullptr) {
342+
// Node is not in this GraphViewer
343+
return nullptr;
344+
}
345+
346+
if (graph_viewer.NodeProducesGraphOutput(parent_node)) {
347+
// Node is producing a graph output
348+
return nullptr;
349+
}
350+
351+
const auto parent_node_unit_it = node_unit_map.find(&parent_node);
352+
if (parent_node_unit_it == node_unit_map.end()) {
353+
return nullptr;
354+
}
355+
const NodeUnit* p_parent_node_unit = parent_node_unit_it->second;
356+
357+
// Check if parent node has already been handled. Should not be the case if the calling
358+
// fusion function has been called in topological order, but check to be safe.
359+
if (qnn_node_group_map.count(p_parent_node_unit) != 0) {
360+
return nullptr;
361+
}
362+
363+
// parent must not already be part of a QDQ NodeUnit (i.e., be standalone).
364+
if (p_parent_node_unit->UnitType() != NodeUnit::Type::SingleNode) {
365+
return nullptr;
366+
}
367+
368+
return p_parent_node_unit;
369+
}
370+
}
371+
return nullptr;
372+
}
373+
317374
} // namespace qnn
318375
} // namespace onnxruntime

onnxruntime/core/providers/qnn/builder/qnn_node_group/utils.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,11 @@ const NodeUnit* GetOnlyChildOfOutput(const GraphViewer& graph_viewer,
5757
const std::unordered_map<const Node*, const NodeUnit*>& node_unit_map,
5858
const std::unordered_map<const NodeUnit*, const IQnnNodeGroup*>& qnn_node_group_map);
5959

60+
const NodeUnit* GetParentOfInputByName(const GraphViewer& graph_viewer,
61+
const NodeUnit& node_unit,
62+
const std::string& input_name,
63+
const std::unordered_map<const Node*, const NodeUnit*>& node_unit_map,
64+
const std::unordered_map<const NodeUnit*, const IQnnNodeGroup*>& qnn_node_group_map);
65+
6066
} // namespace qnn
6167
} // namespace onnxruntime
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
#if !defined(ORT_MINIMAL_BUILD)
5+
6+
#include <string>
7+
#include <vector>
8+
#include <cmath>
9+
#include <optional>
10+
#include <utility>
11+
#include <array>
12+
#include <memory>
13+
#include <unordered_map>
14+
15+
#include "core/graph/graph.h"
16+
#include "core/graph/node_attr_utils.h"
17+
#include "test/unittest_util/qdq_test_utils.h"
18+
#include "test/providers/qnn/qnn_test_utils.h"
19+
#include "gtest/gtest.h"
20+
21+
namespace onnxruntime {
22+
namespace test {
23+
24+
#if defined(__aarch64__) || defined(_M_ARM64)
25+
26+
namespace {
27+
28+
GetQDQTestCaseFn BuildLPBQGemmWithoutQLTestCase() {
29+
return [](ModelTestBuilder& builder) -> void {
30+
// Define the test case for LPBQGemm fusion without QuantizeLinear node
31+
const int64_t input_channels = 16;
32+
const int64_t output_channels = 16;
33+
const int64_t blocks_per_axis = 4;
34+
const std::vector<int64_t> input_shape{1, input_channels};
35+
auto input_def = TestInputDef<float>(input_shape, false, -0.5f, 0.5f);
36+
NodeArg* input = MakeTestInput<float>(builder, input_def);
37+
38+
// QuantizeLinear for Activation
39+
NodeArg* act_ql_output = builder.MakeIntermediate();
40+
NodeArg* act_ql_scale = builder.MakeScalarInitializer<float>(0.00005509183756657876f);
41+
NodeArg* act_ql_zero_point = builder.MakeScalarInitializer<uint16_t>(23715);
42+
builder.AddNode("QuantizeLinear", {input, act_ql_scale, act_ql_zero_point}, {act_ql_output});
43+
44+
// DequantizeLinear for Activation
45+
NodeArg* act_dql_output = builder.MakeIntermediate();
46+
NodeArg* act_dql_scale = builder.MakeScalarInitializer<float>(0.00005509183756657876f);
47+
NodeArg* act_dql_zero_point = builder.MakeScalarInitializer<uint16_t>(23715);
48+
builder.AddNode("DequantizeLinear", {act_ql_output, act_dql_scale, act_dql_zero_point}, {act_dql_output});
49+
50+
// DequantizeLinear for Scale
51+
NodeArg* scale_dql_input = builder.MakeInitializer<uint8_t>({blocks_per_axis, output_channels}, 1, 15);
52+
NodeArg* scale_dql_scale = builder.MakeInitializer<float>({output_channels}, 0.01f, 0.02f);
53+
std::vector<uint8_t> dql_zero_points_data = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
54+
NodeArg* scale_dql_zero_point = builder.Make1DInitializer<uint8_t>(dql_zero_points_data);
55+
NodeArg* scale_dql_output = builder.MakeIntermediate();
56+
Node& scale_dql = builder.AddNode("DequantizeLinear", {scale_dql_input, scale_dql_scale, scale_dql_zero_point}, {scale_dql_output});
57+
scale_dql.AddAttribute("axis", static_cast<int64_t>(1));
58+
59+
// Create quantized weight directly (skip QuantizeLinear)
60+
// We're creating a pre-quantized weight tensor that would normally be the output of QuantizeLinear
61+
std::vector<Int4x2> quantized_weight_data;
62+
size_t num_storage_elems = input_channels * output_channels;
63+
quantized_weight_data.resize(Int4x2::CalcNumInt4Pairs(num_storage_elems));
64+
for (size_t i = 0; i < num_storage_elems / 2; ++i) {
65+
// Set some pattern in the quantized weights
66+
quantized_weight_data[i].SetElem(0, i % 16);
67+
quantized_weight_data[i].SetElem(1, (i + 8) % 16);
68+
}
69+
NodeArg* w_quantized = builder.MakeInitializer<Int4x2>({input_channels, output_channels}, quantized_weight_data);
70+
71+
// DequantizeLinear for Weight (directly using the quantized weight)
72+
std::vector<Int4x2> zero_points_data;
73+
size_t num_zp_elems = blocks_per_axis * output_channels;
74+
zero_points_data.resize(Int4x2::CalcNumInt4Pairs(num_zp_elems));
75+
for (size_t i = 0; i < num_zp_elems; ++i) {
76+
size_t r = i >> 1;
77+
size_t c = i & 0x1;
78+
zero_points_data[r].SetElem(c, 0);
79+
}
80+
NodeArg* w_dql_zero_point = builder.MakeInitializer<Int4x2>({blocks_per_axis, output_channels}, zero_points_data);
81+
NodeArg* w_dql_output = builder.MakeIntermediate();
82+
Node& w_dql = builder.AddNode("DequantizeLinear", {w_quantized, scale_dql_output, w_dql_zero_point}, {w_dql_output});
83+
w_dql.AddAttribute("axis", static_cast<int64_t>(0));
84+
w_dql.AddAttribute("block_size", static_cast<int64_t>(4));
85+
86+
// Gemm
87+
NodeArg* gemm_bias = builder.MakeInitializer<float>({output_channels}, -1.0f, 1.0f);
88+
NodeArg* gemm_output = builder.MakeIntermediate();
89+
builder.AddNode("Gemm", {act_dql_output, w_dql_output, gemm_bias}, {gemm_output});
90+
91+
// QuantizeLinear for Output
92+
NodeArg* output_ql_scale = builder.MakeScalarInitializer<float>(0.00019595865160226822f);
93+
NodeArg* output_ql_zero_point = builder.MakeScalarInitializer<uint16_t>(31693);
94+
NodeArg* output_ql_output = builder.MakeIntermediate();
95+
builder.AddNode("QuantizeLinear", {gemm_output, output_ql_scale, output_ql_zero_point}, {output_ql_output});
96+
97+
// DequantizeLinear for Output
98+
NodeArg* output_dql_scale = builder.MakeScalarInitializer<float>(0.00019595865160226822f);
99+
NodeArg* output_dql_zero_point = builder.MakeScalarInitializer<uint16_t>(31693);
100+
NodeArg* output_dql_output = builder.MakeOutput();
101+
builder.AddNode("DequantizeLinear", {output_ql_output, output_dql_scale, output_dql_zero_point}, {output_dql_output});
102+
};
103+
}
104+
105+
ProviderOptions GetProviderOptions() {
106+
ProviderOptions provider_options;
107+
provider_options["backend_type"] = "htp";
108+
provider_options["offload_graph_io_quantization"] = "0";
109+
return provider_options;
110+
}
111+
112+
} // namespace
113+
114+
#if defined(_WIN32)
115+
// Graph fails to compose on ARM64 Windows since QNN 2.37.0
116+
TEST_F(QnnHTPBackendTests, DISABLED_LPBQGemmFusionWithoutQL) {
117+
#else
118+
TEST_F(QnnHTPBackendTests, LPBQGemmFusionWithoutQL) {
119+
#endif
120+
ProviderOptions provider_options = GetProviderOptions();
121+
RunQnnModelTest(BuildLPBQGemmWithoutQLTestCase(),
122+
provider_options,
123+
/*opset_version=*/21,
124+
/*expected_ep_assignment=*/ExpectedEPNodeAssignment::Some,
125+
/*fp32_abs_err=*/1e-2f,
126+
/*log_severity =*/logging::Severity::kERROR,
127+
/*verify_outputs=*/false);
128+
}
129+
130+
#endif // defined(__aarch64__) || defined(_M_ARM64)
131+
132+
} // namespace test
133+
} // namespace onnxruntime
134+
135+
#endif // !defined(ORT_MINIMAL_BUILD)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
#if !defined(ORT_MINIMAL_BUILD)
5+
6+
#include <string>
7+
#include <vector>
8+
#include <cmath>
9+
#include <optional>
10+
#include <utility>
11+
#include <array>
12+
#include <memory>
13+
#include <unordered_map>
14+
15+
#include "core/graph/graph.h"
16+
#include "core/graph/node_attr_utils.h"
17+
#include "test/unittest_util/qdq_test_utils.h"
18+
#include "test/providers/qnn/qnn_test_utils.h"
19+
#include "gtest/gtest.h"
20+
21+
namespace onnxruntime {
22+
namespace test {
23+
24+
#if defined(__aarch64__) || defined(_M_ARM64)
25+
26+
namespace {
27+
28+
GetQDQTestCaseFn BuildLPBQMatMulWithoutQLTestCase() {
29+
return [](ModelTestBuilder& builder) -> void {
30+
// Define the test case for LPBQMatMul fusion without QuantizeLinear node
31+
const int64_t input_channels = 16;
32+
const int64_t output_channels = 16;
33+
const int64_t blocks_per_axis = 4;
34+
const std::vector<int64_t> input_shape{1, input_channels};
35+
auto input_def = TestInputDef<float>(input_shape, false, -0.5f, 0.5f);
36+
NodeArg* input = MakeTestInput<float>(builder, input_def);
37+
38+
// QuantizeLinear for Activation
39+
NodeArg* act_ql_output = builder.MakeIntermediate();
40+
NodeArg* act_ql_scale = builder.MakeScalarInitializer<float>(0.00005509183756657876f);
41+
NodeArg* act_ql_zero_point = builder.MakeScalarInitializer<uint16_t>(23715);
42+
builder.AddNode("QuantizeLinear", {input, act_ql_scale, act_ql_zero_point}, {act_ql_output});
43+
44+
// DequantizeLinear for Activation
45+
NodeArg* act_dql_output = builder.MakeIntermediate();
46+
NodeArg* act_dql_scale = builder.MakeScalarInitializer<float>(0.00005509183756657876f);
47+
NodeArg* act_dql_zero_point = builder.MakeScalarInitializer<uint16_t>(23715);
48+
builder.AddNode("DequantizeLinear", {act_ql_output, act_dql_scale, act_dql_zero_point}, {act_dql_output});
49+
50+
// DequantizeLinear for Scale
51+
NodeArg* scale_dql_input = builder.MakeInitializer<uint8_t>({blocks_per_axis, output_channels}, 1, 15);
52+
NodeArg* scale_dql_scale = builder.MakeInitializer<float>({output_channels}, 0.01f, 0.02f);
53+
std::vector<uint8_t> dql_zero_points_data = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
54+
NodeArg* scale_dql_zero_point = builder.Make1DInitializer<uint8_t>(dql_zero_points_data);
55+
NodeArg* scale_dql_output = builder.MakeIntermediate();
56+
Node& scale_dql = builder.AddNode("DequantizeLinear", {scale_dql_input, scale_dql_scale, scale_dql_zero_point}, {scale_dql_output});
57+
scale_dql.AddAttribute("axis", static_cast<int64_t>(1));
58+
59+
// Create quantized weight directly (skip QuantizeLinear)
60+
// We're creating a pre-quantized weight tensor that would normally be the output of QuantizeLinear
61+
std::vector<Int4x2> quantized_weight_data;
62+
size_t num_storage_elems = input_channels * output_channels;
63+
quantized_weight_data.resize(Int4x2::CalcNumInt4Pairs(num_storage_elems));
64+
for (size_t i = 0; i < num_storage_elems / 2; ++i) {
65+
// Set some pattern in the quantized weights
66+
quantized_weight_data[i].SetElem(0, i % 16);
67+
quantized_weight_data[i].SetElem(1, (i + 8) % 16);
68+
}
69+
NodeArg* w_quantized = builder.MakeInitializer<Int4x2>({input_channels, output_channels}, quantized_weight_data);
70+
71+
// DequantizeLinear for Weight
72+
std::vector<Int4x2> zero_points_data;
73+
size_t num_zp_elems = blocks_per_axis * output_channels;
74+
zero_points_data.resize(Int4x2::CalcNumInt4Pairs(num_zp_elems));
75+
for (size_t i = 0; i < num_zp_elems; ++i) {
76+
size_t r = i >> 1;
77+
size_t c = i & 0x1;
78+
zero_points_data[r].SetElem(c, 0);
79+
}
80+
NodeArg* w_dql_zero_point = builder.MakeInitializer<Int4x2>({blocks_per_axis, output_channels}, zero_points_data);
81+
NodeArg* w_dql_output = builder.MakeIntermediate();
82+
Node& w_dql = builder.AddNode("DequantizeLinear", {w_quantized, scale_dql_output, w_dql_zero_point}, {w_dql_output});
83+
w_dql.AddAttribute("axis", static_cast<int64_t>(0));
84+
w_dql.AddAttribute("block_size", static_cast<int64_t>(4));
85+
86+
// MatMul
87+
NodeArg* matmul_output = builder.MakeIntermediate();
88+
builder.AddNode("MatMul", {act_dql_output, w_dql_output}, {matmul_output});
89+
90+
// QuantizeLinear for Output
91+
NodeArg* output_ql_scale = builder.MakeScalarInitializer<float>(0.00019595865160226822f);
92+
NodeArg* output_ql_zero_point = builder.MakeScalarInitializer<uint16_t>(31693);
93+
NodeArg* output_ql_output = builder.MakeIntermediate();
94+
builder.AddNode("QuantizeLinear", {matmul_output, output_ql_scale, output_ql_zero_point}, {output_ql_output});
95+
96+
// DequantizeLinear for Output
97+
NodeArg* output_dql_scale = builder.MakeScalarInitializer<float>(0.00019595865160226822f);
98+
NodeArg* output_dql_zero_point = builder.MakeScalarInitializer<uint16_t>(31693);
99+
NodeArg* output_dql_output = builder.MakeOutput();
100+
builder.AddNode("DequantizeLinear", {output_ql_output, output_dql_scale, output_dql_zero_point}, {output_dql_output});
101+
};
102+
}
103+
104+
ProviderOptions GetProviderOptions() {
105+
ProviderOptions provider_options;
106+
provider_options["backend_type"] = "htp";
107+
provider_options["offload_graph_io_quantization"] = "0";
108+
return provider_options;
109+
}
110+
111+
} // namespace
112+
113+
#if defined(_WIN32)
114+
// Graph fails to compose on ARM64 Windows since QNN 2.37.0
115+
TEST_F(QnnHTPBackendTests, DISABLED_LPBQMatMulFusionWithoutQL) {
116+
#else
117+
TEST_F(QnnHTPBackendTests, LPBQMatMulFusionWithoutQL) {
118+
#endif
119+
ProviderOptions provider_options = GetProviderOptions();
120+
RunQnnModelTest(BuildLPBQMatMulWithoutQLTestCase(),
121+
provider_options,
122+
/*opset_version=*/21,
123+
/*expected_ep_assignment=*/ExpectedEPNodeAssignment::Some,
124+
/*fp32_abs_err=*/1e-2f,
125+
/*log_severity =*/logging::Severity::kERROR,
126+
/*verify_outputs=*/false);
127+
}
128+
129+
#endif // defined(__aarch64__) || defined(_M_ARM64)
130+
131+
} // namespace test
132+
} // namespace onnxruntime
133+
134+
#endif // !defined(ORT_MINIMAL_BUILD)

0 commit comments

Comments
 (0)