Skip to content

Commit 93d7f6f

Browse files
committed
Qualcomm AI Engine Direct - Adding QNN backend support for var core ATen ops
1 parent ff90ade commit 93d7f6f

7 files changed

Lines changed: 353 additions & 0 deletions

File tree

backends/qualcomm/_passes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from .decompose_threshold import DecomposeThreshold
3838
from .decompose_triu import DecomposeTriu
3939
from .decompose_trunc import DecomposeTrunc
40+
from .decompose_var import DecomposeVar
4041
from .decompose_wrap_with_autocast import DecomposeWrapWithAutocast
4142
from .expand_broadcast_tensor_shape import ExpandBroadcastTensorShape
4243
from .fixed_linear_keep_dim import FixedLinearKeepDim
@@ -96,6 +97,7 @@
9697
DecomposeThreshold,
9798
DecomposeTriu,
9899
DecomposeTrunc,
100+
DecomposeVar,
99101
DecomposeWrapWithAutocast,
100102
ExpandBroadcastTensorShape,
101103
FixedLinearKeepDim,
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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) * N / (N - correction)
20+
21+
For var.correction:
22+
correction is an optional Scalar (default=1, i.e. Bessel's correction)
23+
For var.dim:
24+
unbiased=True maps to correction=1, unbiased=False maps to correction=0
25+
"""
26+
27+
def __init__(self):
28+
super(DecomposeVar, self).__init__()
29+
self.var_targets = {
30+
torch.ops.aten.var.correction,
31+
torch.ops.aten.var.dim,
32+
exir_ops.edge.aten.var.correction,
33+
exir_ops.edge.aten.var.dim,
34+
}
35+
36+
def _get_correction(self, node):
37+
"""Extract the correction factor from node args based on op variant."""
38+
target = node.target
39+
if target in (
40+
torch.ops.aten.var.correction,
41+
exir_ops.edge.aten.var.correction,
42+
):
43+
# var.correction(Tensor self, int[1]? dim=None, *, Scalar? correction=None, bool keepdim=False)
44+
# correction is a kwarg, but in the graph it may appear in kwargs
45+
correction = node.kwargs.get("correction", None)
46+
if correction is None:
47+
correction = 1.0
48+
return float(correction)
49+
else:
50+
# var.dim(Tensor self, int[1]? dim=None, bool unbiased=True, bool keepdim=False)
51+
unbiased = node.args[2] if len(node.args) > 2 else True
52+
return 1.0 if unbiased else 0.0
53+
54+
def _get_dim_and_keepdim(self, node):
55+
"""Extract dim and keepdim from node args based on op variant."""
56+
target = node.target
57+
if target in (
58+
torch.ops.aten.var.correction,
59+
exir_ops.edge.aten.var.correction,
60+
):
61+
# var.correction(Tensor self, int[1]? dim=None, *, Scalar? correction=None, bool keepdim=False)
62+
dim = node.args[1] if len(node.args) > 1 else None
63+
keepdim = node.kwargs.get("keepdim", False)
64+
return dim, keepdim
65+
else:
66+
# var.dim(Tensor self, int[1]? dim=None, bool unbiased=True, bool keepdim=False)
67+
dim = node.args[1] if len(node.args) > 1 else None
68+
keepdim = node.args[3] if len(node.args) > 3 else False
69+
return dim, keepdim
70+
71+
def call(self, graph_module: torch.fx.GraphModule):
72+
graph = graph_module.graph
73+
const_cache = {}
74+
75+
for node in list(graph.nodes):
76+
if node.op == "call_function" and node.target in self.var_targets:
77+
x_node = node.args[0]
78+
is_edge = isinstance(node.target, EdgeOpOverload)
79+
meta = node.meta
80+
81+
correction = self._get_correction(node)
82+
dim, keepdim = self._get_dim_and_keepdim(node)
83+
84+
mean_op = (
85+
exir_ops.edge.aten.mean.dim if is_edge else torch.ops.aten.mean.dim
86+
)
87+
sub_op = (
88+
exir_ops.edge.aten.sub.Tensor
89+
if is_edge
90+
else torch.ops.aten.sub.Tensor
91+
)
92+
mul_op = (
93+
exir_ops.edge.aten.mul.Tensor
94+
if is_edge
95+
else torch.ops.aten.mul.Tensor
96+
)
97+
98+
# Handle dim=None: reduce over all dimensions
99+
input_shape = node.args[0].meta["val"].shape
100+
if dim is None:
101+
dim = list(range(len(input_shape)))
102+
103+
with graph.inserting_before(node):
104+
x_val = x_node.meta["val"]
105+
106+
# Step 1: mean_x = mean(x, dim, keepdim=True)
107+
mean_x_node = graph.create_node(
108+
"call_function", mean_op, (x_node, dim, True)
109+
)
110+
mean_x_node.meta = copy_meta(
111+
meta,
112+
callback=lambda m, _x=x_val, _d=dim: {
113+
**m,
114+
"val": _x.mean(_d, keepdim=True),
115+
},
116+
)
117+
118+
# Step 2: diff = x - mean_x
119+
diff_node = graph.create_node(
120+
"call_function", sub_op, (x_node, mean_x_node)
121+
)
122+
diff_node.meta = copy_meta(
123+
meta, callback=lambda m, _x=x_val: {**m, "val": _x}
124+
)
125+
126+
# Step 3: sq = diff * diff (more efficient than pow(diff, 2))
127+
sq_node = graph.create_node(
128+
"call_function", mul_op, (diff_node, diff_node)
129+
)
130+
sq_node.meta = copy_meta(
131+
meta, callback=lambda m, _x=x_val: {**m, "val": _x}
132+
)
133+
134+
# Step 4: var = mean(sq, dim, keepdim)
135+
var_node = graph.create_node(
136+
"call_function", mean_op, (sq_node, dim, keepdim)
137+
)
138+
var_node.meta = copy_meta(meta)
139+
140+
# Step 5: Apply correction factor if needed
141+
if correction != 0.0:
142+
# N = product of sizes along reduced dims
143+
n = 1
144+
for d in dim:
145+
n *= input_shape[d]
146+
147+
denom = float(n - correction)
148+
# Guard against division by zero (e.g. single-element dim with correction=1).
149+
# Using inf matches the native PyTorch behavior where 0 * inf → nan.
150+
scale = float("inf") if denom == 0 else float(n) / denom
151+
152+
if is_edge:
153+
cache_key = ("_var_scale_", scale)
154+
if cache_key not in const_cache:
155+
attr_name = get_new_attr_name_with_prefix(
156+
"_var_scale_const_"
157+
)(graph_module)
158+
const_cache[cache_key] = get_const_node(
159+
graph, graph_module, attr_name, scale, node
160+
)
161+
scale_node = const_cache[cache_key]
162+
else:
163+
scale_node = scale
164+
165+
result_node = graph.create_node(
166+
"call_function", mul_op, (var_node, scale_node)
167+
)
168+
result_node.meta = copy_meta(meta)
169+
else:
170+
result_node = var_node
171+
172+
for user in node.users.copy():
173+
user.replace_input_with(node, result_node)
174+
175+
graph.eliminate_dead_code()
176+
graph_module.recompile()
177+
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
@@ -42,6 +42,7 @@
4242
DecomposeThreshold,
4343
DecomposeTriu,
4444
DecomposeTrunc,
45+
DecomposeVar,
4546
DecomposeWrapWithAutocast,
4647
ExpandBroadcastTensorShape,
4748
FixedLinearKeepDim,
@@ -117,6 +118,7 @@ def get_capture_program_passes():
117118
(DecomposeRemainder, True),
118119
(DecomposeTan, True),
119120
(DecomposeTrunc, True),
121+
(DecomposeVar, True),
120122
(ExpandBroadcastTensorShape, True),
121123
(FixedLinearKeepDim, True),
122124
(FoldQDQ, True),
@@ -245,6 +247,7 @@ def transform_for_annotation_pipeline(self, graph_module: GraphModule):
245247
self.add_pass(DecomposeThreshold())
246248
self.add_pass(DecomposeTriu())
247249
self.add_pass(DecomposeTrunc())
250+
self.add_pass(DecomposeVar())
248251
self.add_pass(DecomposeWrapWithAutocast())
249252
self.add_pass(DecomposeEinsum())
250253
self.add_pass(DecomposeExpM1())
@@ -273,6 +276,7 @@ def transform_for_export_pipeline(
273276
self.add_pass(DecomposeSelectScatter())
274277
self.add_pass(DecomposeThreshold())
275278
self.add_pass(DecomposeTriu())
279+
self.add_pass(DecomposeVar())
276280
self.add_pass(DecomposeLinalgVectorNorm(quantization_capture=True))
277281
self.add_pass(DecomposeExpM1())
278282
# 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,
@@ -111,6 +112,7 @@ def get_passes_dependency_for_capture_program():
111112
DecomposeRemainder: [RemoveRedundancy],
112113
DecomposeTan: [RemoveRedundancy],
113114
DecomposeTrunc: [RemoveRedundancy],
115+
DecomposeVar: [RemoveRedundancy],
114116
ExpandBroadcastTensorShape: [FoldQDQ],
115117
FixedLinearKeepDim: [FoldQDQ],
116118
FoldQDQ: [AnnotateQuantAttrs, AnnotateStack, AnnotateUnbind],

backends/qualcomm/builders/README.md

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

527528
## Issues
528529
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
@@ -2599,6 +2599,30 @@ def forward(self, x):
25992599
return x.unsqueeze(0)
26002600

26012601

2602+
class VarCorrection(torch.nn.Module):
2603+
def __init__(self, dim=None, correction=1, keepdim=False):
2604+
super().__init__()
2605+
self.dim = dim
2606+
self.correction = correction
2607+
self.keepdim = keepdim
2608+
2609+
def forward(self, x):
2610+
return torch.var(
2611+
x, dim=self.dim, correction=self.correction, keepdim=self.keepdim
2612+
)
2613+
2614+
2615+
class VarDim(torch.nn.Module):
2616+
def __init__(self, dim=None, unbiased=True, keepdim=False):
2617+
super().__init__()
2618+
self.dim = dim
2619+
self.unbiased = unbiased
2620+
self.keepdim = keepdim
2621+
2622+
def forward(self, x):
2623+
return torch.var(x, dim=self.dim, unbiased=self.unbiased, keepdim=self.keepdim)
2624+
2625+
26022626
class View(torch.nn.Module):
26032627
def __init__(self):
26042628
super().__init__()

0 commit comments

Comments
 (0)