Skip to content

Commit 698f672

Browse files
committed
Qualcomm AI Engine Direct - Adding QNN backend support for var core ATen ops
1 parent 8fbfed9 commit 698f672

7 files changed

Lines changed: 275 additions & 0 deletions

File tree

backends/qualcomm/_passes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from .decompose_threshold import DecomposeThreshold
3737
from .decompose_triu import DecomposeTriu
3838
from .decompose_trunc import DecomposeTrunc
39+
from .decompose_var import DecomposeVar
3940
from .decompose_wrap_with_autocast import DecomposeWrapWithAutocast
4041
from .expand_broadcast_tensor_shape import ExpandBroadcastTensorShape
4142
from .fixed_linear_keep_dim import FixedLinearKeepDim
@@ -93,6 +94,7 @@
9394
DecomposeThreshold,
9495
DecomposeTriu,
9596
DecomposeTrunc,
97+
DecomposeVar,
9698
DecomposeWrapWithAutocast,
9799
ExpandBroadcastTensorShape,
98100
FixedLinearKeepDim,
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Copyright (c) Qualcomm Innovation Center, Inc.
2+
# All rights reserved
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import torch
8+
from executorch.exir.dialects._ops import ops as exir_ops
9+
from executorch.exir.dialects.edge._ops import EdgeOpOverload
10+
from executorch.exir.pass_base import ExportPass, PassResult
11+
from torchao.quantization.pt2e.utils import get_new_attr_name_with_prefix
12+
13+
from .utils import copy_meta, get_const_node
14+
15+
16+
class DecomposeVar(ExportPass):
17+
"""
18+
Decompose aten.var.correction and aten.var.dim into supported primitives:
19+
var(x, dim) = mean((x - mean(x, dim, keepdim=True))^2, dim, keepdim)
20+
* N / (N - correction)
21+
22+
For var.correction:
23+
correction is an optional Scalar (default=1, i.e. Bessel's correction)
24+
For var.dim:
25+
unbiased=True maps to correction=1, unbiased=False maps to correction=0
26+
"""
27+
28+
def __init__(self):
29+
super(DecomposeVar, self).__init__()
30+
self.var_targets = {
31+
torch.ops.aten.var.correction,
32+
torch.ops.aten.var.dim,
33+
exir_ops.edge.aten.var.correction,
34+
exir_ops.edge.aten.var.dim,
35+
}
36+
37+
def _get_correction(self, node):
38+
"""Extract the correction factor from node args based on op variant."""
39+
target = node.target
40+
if target in (
41+
torch.ops.aten.var.correction,
42+
exir_ops.edge.aten.var.correction,
43+
):
44+
# var.correction(Tensor self, int[1]? dim=None, *, Scalar? correction=None, bool keepdim=False)
45+
# correction is a kwarg, but in the graph it may appear in kwargs
46+
correction = node.kwargs.get("correction", None)
47+
if correction is None:
48+
correction = 1.0
49+
return float(correction)
50+
else:
51+
# var.dim(Tensor self, int[1]? dim=None, bool unbiased=True, bool keepdim=False)
52+
unbiased = node.args[2] if len(node.args) > 2 else True
53+
return 1.0 if unbiased else 0.0
54+
55+
def _get_dim_and_keepdim(self, node):
56+
"""Extract dim and keepdim from node args based on op variant."""
57+
target = node.target
58+
if target in (
59+
torch.ops.aten.var.correction,
60+
exir_ops.edge.aten.var.correction,
61+
):
62+
# var.correction(Tensor self, int[1]? dim=None, *, Scalar? correction=None, bool keepdim=False)
63+
dim = node.args[1] if len(node.args) > 1 else None
64+
keepdim = node.kwargs.get("keepdim", False)
65+
return dim, keepdim
66+
else:
67+
# var.dim(Tensor self, int[1]? dim=None, bool unbiased=True, bool keepdim=False)
68+
dim = node.args[1] if len(node.args) > 1 else None
69+
keepdim = node.args[3] if len(node.args) > 3 else False
70+
return dim, keepdim
71+
72+
def call(self, graph_module: torch.fx.GraphModule):
73+
graph = graph_module.graph
74+
const_cache = {}
75+
76+
for node in list(graph.nodes):
77+
if node.op == "call_function" and node.target in self.var_targets:
78+
x_node = node.args[0]
79+
is_edge = isinstance(node.target, EdgeOpOverload)
80+
meta = node.meta
81+
82+
correction = self._get_correction(node)
83+
dim, keepdim = self._get_dim_and_keepdim(node)
84+
85+
mean_op = (
86+
exir_ops.edge.aten.mean.dim if is_edge else torch.ops.aten.mean.dim
87+
)
88+
sub_op = (
89+
exir_ops.edge.aten.sub.Tensor
90+
if is_edge
91+
else torch.ops.aten.sub.Tensor
92+
)
93+
mul_op = (
94+
exir_ops.edge.aten.mul.Tensor
95+
if is_edge
96+
else torch.ops.aten.mul.Tensor
97+
)
98+
99+
# Handle dim=None: reduce over all dimensions
100+
input_shape = node.args[0].meta["val"].shape
101+
if dim is None:
102+
dim = list(range(len(input_shape)))
103+
104+
with graph.inserting_before(node):
105+
x_val = x_node.meta["val"]
106+
107+
# Step 1: mean_x = mean(x, dim, keepdim=True)
108+
mean_x_node = graph.create_node(
109+
"call_function", mean_op, (x_node, dim, True)
110+
)
111+
mean_x_node.meta = copy_meta(
112+
meta,
113+
callback=lambda m, _x=x_val, _d=dim: {
114+
**m,
115+
"val": _x.mean(_d, keepdim=True),
116+
},
117+
)
118+
119+
# Step 2: diff = x - mean_x
120+
diff_node = graph.create_node(
121+
"call_function", sub_op, (x_node, mean_x_node)
122+
)
123+
diff_node.meta = copy_meta(
124+
meta, callback=lambda m, _x=x_val: {**m, "val": _x}
125+
)
126+
127+
# Step 3: sq = diff * diff (more efficient than pow(diff, 2))
128+
sq_node = graph.create_node(
129+
"call_function", mul_op, (diff_node, diff_node)
130+
)
131+
sq_node.meta = copy_meta(
132+
meta, callback=lambda m, _x=x_val: {**m, "val": _x}
133+
)
134+
135+
# Step 4: var = mean(sq, dim, keepdim)
136+
var_node = graph.create_node(
137+
"call_function", mean_op, (sq_node, dim, keepdim)
138+
)
139+
var_node.meta = copy_meta(meta)
140+
141+
# Step 5: Apply correction factor if needed
142+
if correction != 0.0:
143+
# N = product of sizes along reduced dims
144+
n = 1
145+
for d in dim:
146+
n *= input_shape[d]
147+
148+
scale = float(n) / float(n - correction)
149+
150+
if is_edge:
151+
cache_key = ("_var_scale_", scale)
152+
if cache_key not in const_cache:
153+
attr_name = get_new_attr_name_with_prefix(
154+
"_var_scale_const_"
155+
)(graph_module)
156+
const_cache[cache_key] = get_const_node(
157+
graph, graph_module, attr_name, scale, node
158+
)
159+
scale_node = const_cache[cache_key]
160+
else:
161+
scale_node = scale
162+
163+
result_node = graph.create_node(
164+
"call_function", mul_op, (var_node, scale_node)
165+
)
166+
result_node.meta = copy_meta(meta)
167+
else:
168+
result_node = var_node
169+
170+
for user in node.users.copy():
171+
user.replace_input_with(node, result_node)
172+
173+
graph.eliminate_dead_code()
174+
graph_module.recompile()
175+
return PassResult(graph_module, True)

backends/qualcomm/_passes/qnn_pass_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
DecomposeThreshold,
4242
DecomposeTriu,
4343
DecomposeTrunc,
44+
DecomposeVar,
4445
DecomposeWrapWithAutocast,
4546
ExpandBroadcastTensorShape,
4647
FixedLinearKeepDim,
@@ -115,6 +116,7 @@ def get_capture_program_passes():
115116
(DecomposeRemainder, True),
116117
(DecomposeTan, True),
117118
(DecomposeTrunc, True),
119+
(DecomposeVar, True),
118120
(ExpandBroadcastTensorShape, True),
119121
(FixedLinearKeepDim, True),
120122
(FoldQDQ, True),
@@ -242,6 +244,7 @@ def transform_for_annotation_pipeline(self, graph_module: GraphModule):
242244
self.add_pass(DecomposeThreshold())
243245
self.add_pass(DecomposeTriu())
244246
self.add_pass(DecomposeTrunc())
247+
self.add_pass(DecomposeVar())
245248
self.add_pass(DecomposeWrapWithAutocast())
246249
self.add_pass(DecomposeEinsum())
247250
self.add_pass(DecomposeExpM1())
@@ -268,6 +271,7 @@ def transform_for_export_pipeline(
268271
self.add_pass(DecomposeRoll())
269272
self.add_pass(DecomposeThreshold())
270273
self.add_pass(DecomposeTriu())
274+
self.add_pass(DecomposeVar())
271275
self.add_pass(DecomposeLinalgVectorNorm(quantization_capture=True))
272276
self.add_pass(DecomposeExpM1())
273277
# DecomposeFloorDivide does not apply to the annotation pipeline,

backends/qualcomm/_passes/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def get_passes_dependency_for_capture_program():
7676
DecomposeRemainder,
7777
DecomposeTan,
7878
DecomposeTrunc,
79+
DecomposeVar,
7980
ExpandBroadcastTensorShape,
8081
FixedLinearKeepDim,
8182
FoldQDQ,
@@ -110,6 +111,7 @@ def get_passes_dependency_for_capture_program():
110111
DecomposeRemainder: [RemoveRedundancy],
111112
DecomposeTan: [RemoveRedundancy],
112113
DecomposeTrunc: [RemoveRedundancy],
114+
DecomposeVar: [RemoveRedundancy],
113115
ExpandBroadcastTensorShape: [FoldQDQ],
114116
FixedLinearKeepDim: [FoldQDQ],
115117
FoldQDQ: [AnnotateQuantAttrs, AnnotateStack, AnnotateUnbind],

backends/qualcomm/builders/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ The following PyTorch operators are supported through decomposition or annotatio
522522
| `aten.threshold` | `DecomposeThreshold` |
523523
| `aten.triu` | `DecomposeTriu` |
524524
| `aten.trunc` | `DecomposeTrunc` |
525+
| `aten.var.correction`, `aten.var.dim` | `DecomposeVar` |
525526

526527
## Issues
527528
Please refer to the [issue section](../README.md#issues) for more information.

backends/qualcomm/tests/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2581,6 +2581,30 @@ def forward(self, x):
25812581
return x.unsqueeze(0)
25822582

25832583

2584+
class VarCorrection(torch.nn.Module):
2585+
def __init__(self, dim=None, correction=1, keepdim=False):
2586+
super().__init__()
2587+
self.dim = dim
2588+
self.correction = correction
2589+
self.keepdim = keepdim
2590+
2591+
def forward(self, x):
2592+
return torch.var(
2593+
x, dim=self.dim, correction=self.correction, keepdim=self.keepdim
2594+
)
2595+
2596+
2597+
class VarDim(torch.nn.Module):
2598+
def __init__(self, dim=None, unbiased=True, keepdim=False):
2599+
super().__init__()
2600+
self.dim = dim
2601+
self.unbiased = unbiased
2602+
self.keepdim = keepdim
2603+
2604+
def forward(self, x):
2605+
return torch.var(x, dim=self.dim, unbiased=self.unbiased, keepdim=self.keepdim)
2606+
2607+
25842608
class View(torch.nn.Module):
25852609
def __init__(self):
25862610
super().__init__()

backends/qualcomm/tests/test_qnn_delegate.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2175,6 +2175,39 @@ def test_qnn_backend_unsqueeze(self):
21752175
sample_input = (torch.randn([1, 3, 3]),)
21762176
self.lower_module_and_test_output(module, sample_input)
21772177

2178+
def test_qnn_backend_var(self):
2179+
test_comb = [
2180+
{
2181+
QCOM_MODULE: [
2182+
VarCorrection(dim=[1], correction=1, keepdim=False), # noqa: F405
2183+
VarCorrection(dim=[1], correction=0, keepdim=True), # noqa: F405
2184+
VarCorrection( # noqa: F405
2185+
dim=[0, 2], correction=1, keepdim=False
2186+
),
2187+
],
2188+
QCOM_SAMPLE_INPUTS: [
2189+
(torch.randn(3, 4, 5),),
2190+
],
2191+
},
2192+
{
2193+
QCOM_MODULE: [
2194+
VarDim(dim=[1], unbiased=True, keepdim=False), # noqa: F405
2195+
VarDim(dim=[1], unbiased=False, keepdim=True), # noqa: F405
2196+
],
2197+
QCOM_SAMPLE_INPUTS: [
2198+
(torch.randn(3, 4, 5),),
2199+
],
2200+
},
2201+
]
2202+
2203+
index = 0
2204+
for comb in test_comb:
2205+
for module in comb[QCOM_MODULE]:
2206+
for sample_input in comb[QCOM_SAMPLE_INPUTS]:
2207+
with self.subTest(i=index):
2208+
index += 1
2209+
self.lower_module_and_test_output(module, sample_input)
2210+
21782211
def test_qnn_backend_view(self):
21792212
module = View() # noqa: F405
21802213
sample_input = (torch.randn([1, 8, 512]), torch.randn([1, 2, 8, 256]))
@@ -5030,6 +5063,40 @@ def test_qnn_backend_unsqueeze(self):
50305063
module = self.get_qdq_module(module, sample_input)
50315064
self.lower_module_and_test_output(module, sample_input)
50325065

5066+
def test_qnn_backend_var(self):
5067+
test_comb = [
5068+
{
5069+
QCOM_MODULE: [
5070+
VarCorrection(dim=[1], correction=1, keepdim=False), # noqa: F405
5071+
VarCorrection(dim=[1], correction=0, keepdim=True), # noqa: F405
5072+
VarCorrection( # noqa: F405
5073+
dim=[0, 2], correction=1, keepdim=False
5074+
),
5075+
],
5076+
QCOM_SAMPLE_INPUTS: [
5077+
(torch.randn(3, 4, 5),),
5078+
],
5079+
},
5080+
{
5081+
QCOM_MODULE: [
5082+
VarDim(dim=[1], unbiased=True, keepdim=False), # noqa: F405
5083+
VarDim(dim=[1], unbiased=False, keepdim=True), # noqa: F405
5084+
],
5085+
QCOM_SAMPLE_INPUTS: [
5086+
(torch.randn(3, 4, 5),),
5087+
],
5088+
},
5089+
]
5090+
5091+
index = 0
5092+
for comb in test_comb:
5093+
for module in comb[QCOM_MODULE]:
5094+
for sample_input in comb[QCOM_SAMPLE_INPUTS]:
5095+
with self.subTest(i=index):
5096+
index += 1
5097+
qdq_module = self.get_qdq_module(module, sample_input)
5098+
self.lower_module_and_test_output(qdq_module, sample_input)
5099+
50335100
def test_qnn_backend_view(self):
50345101
module = View() # noqa: F405
50355102
sample_input = (torch.randn([1, 8, 512]), torch.randn([1, 2, 8, 256]))

0 commit comments

Comments
 (0)