Skip to content

Commit d29750b

Browse files
authored
Qualcomm AI Engine Direct - Adding QNN backend support for the empty.memory_format and empty_strided core ATen ops (pytorch#22938)
### Summary Add support for `aten.empty.memory_format` and `aten.empty_strided` on the QNN backend via a decomposition pass that rewrites them into `aten.full.default`. Both ops allocate uninitialized memory, making neither the values nor the physical layout observable. The rewrite is valid always, including for non-contiguous memory formats and arbitrary strides. The pass handles dtype correctly: `full.default` infers its output dtype from the fill scalar's Python type, so when the target dtype differs an explicit `aten.to.dtype` cast is inserted. Both ATen IR and edge dialects are supported. ### Test plan ``` python backends/qualcomm/tests/test_qnn_delegate.py TestQNNQuantizedOperator.test_qnn_backend_empty --soc_model SM8750 --host aisw-vm15-labsd --device 545ee4aa --build_folder build-android python backends/qualcomm/tests/test_qnn_delegate.py TestQNNFloatingPointOperator.test_qnn_backend_empty --soc_model SM8750 --host aisw-vm15-labsd --device 545ee4aa --build_folder build-android python backends/qualcomm/tests/test_qnn_delegate.py TestQNNQuantizedOperator.test_qnn_backend_empty_strided --soc_model SM8750 --host aisw-vm15-labsd --device 545ee4aa --build_folder build-android python backends/qualcomm/tests/test_qnn_delegate.py TestQNNFloatingPointOperator.test_qnn_backend_empty_strided --soc_model SM8750 --host aisw-vm15-labsd --device 545ee4aa --build_folder build-android pytest --import-mode=importlib \ backends/qualcomm/tests/rework/{htp,lpai}/op/*/test.py \ backends/qualcomm/tests/rework/gpu/op/test.py \ -k "test_empty" -v ```
1 parent 42d0740 commit d29750b

15 files changed

Lines changed: 457 additions & 0 deletions

File tree

‎backends/qualcomm/_passes/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from .decompose_diagonal import DecomposeDiagonal
2727
from .decompose_div_mode import DecomposeDivMode
2828
from .decompose_einsum import DecomposeEinsum
29+
from .decompose_empty import DecomposeEmpty
2930
from .decompose_expm1 import DecomposeExpM1
3031
from .decompose_fill import DecomposeFill
3132
from .decompose_floor_divide import DecomposeFloorDivide
@@ -97,6 +98,7 @@
9798
DecomposeDiagonal,
9899
DecomposeDivMode,
99100
DecomposeEinsum,
101+
DecomposeEmpty,
100102
DecomposeExpM1,
101103
DecomposeFill,
102104
DecomposeFloorDivide,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 executorch.exir.passes import dead_code_elimination_pass
12+
13+
from .utils import copy_meta
14+
15+
16+
def _get_ops(is_edge: bool):
17+
return (
18+
{
19+
"full": exir_ops.edge.aten.full.default,
20+
"cast": exir_ops.edge.aten._to_copy.default,
21+
}
22+
if is_edge
23+
else {
24+
"full": torch.ops.aten.full.default,
25+
"cast": torch.ops.aten.to.dtype,
26+
}
27+
)
28+
29+
30+
def _fill_value(dtype: torch.dtype):
31+
"""Return a zero fill scalar and the dtype full.default infers from it.
32+
33+
full.default has no dtype argument here (see the note in DecomposeEmpty on
34+
why kwargs are not an option), so its output dtype is derived from the type
35+
of the python scalar: bool -> bool, int -> int64, float -> the default
36+
dtype. The caller compares the returned dtype against the dtype it actually
37+
needs and inserts a cast when they differ.
38+
"""
39+
if dtype == torch.bool:
40+
return False, torch.bool
41+
if dtype.is_floating_point or dtype.is_complex:
42+
return 0.0, torch.get_default_dtype()
43+
return 0, torch.int64
44+
45+
46+
class DecomposeEmpty(ExportPass):
47+
"""
48+
Decompose empty.memory_format / empty_strided into full.default.
49+
50+
The values are uninitialized, so filling with zero is free. The rewrite is
51+
applied to every allocation, including a non-contiguous memory_format (e.g.
52+
channels_last) or an arbitrary stride passed to empty_strided: QNN tensors
53+
are dense and carry no strides, so the layout cannot survive delegation
54+
anyway, and nothing in the buffer is readable. Restricting the pass to
55+
contiguous allocations would instead split the delegate and push the
56+
remaining empty nodes onto CPU.
57+
58+
Assumes a plain CPU, non-pinned allocation -- full.default is created
59+
without device/layout/pin_memory.
60+
"""
61+
62+
def __init__(self):
63+
super().__init__()
64+
self.targets = {
65+
torch.ops.aten.empty.memory_format,
66+
torch.ops.aten.empty_strided.default,
67+
exir_ops.edge.aten.empty.memory_format,
68+
exir_ops.edge.aten.empty_strided.default,
69+
}
70+
71+
def call(self, graph_module: torch.fx.GraphModule):
72+
graph = graph_module.graph
73+
for node in list(graph.nodes):
74+
if node.op == "call_function" and node.target in self.targets:
75+
empty_node = node
76+
val = empty_node.meta["val"]
77+
shape = list(val.shape)
78+
79+
is_edge = isinstance(empty_node.target, EdgeOpOverload)
80+
ops = _get_ops(is_edge)
81+
fill_value, fill_dtype = _fill_value(val.dtype)
82+
83+
# The replacement always produces a contiguous tensor, so the
84+
# propagated FakeTensor must not keep the original strides.
85+
def cast_meta(meta, dtype=None):
86+
out = meta["val"].contiguous()
87+
meta["val"] = out if dtype is None else out.to(dtype)
88+
return meta
89+
90+
with graph.inserting_before(empty_node):
91+
# Never pass kwargs (dtype/device) to an ATen op here: the
92+
# ATen IR requires them to be empty and prepare_pt2e
93+
# asserts on it, which this pass would hit since it runs
94+
# before the quantizer and full.default is annotated.
95+
full_node = graph.create_node(
96+
"call_function",
97+
ops["full"],
98+
(shape, fill_value),
99+
)
100+
full_node.meta = copy_meta(
101+
empty_node.meta,
102+
lambda meta, _dtype=fill_dtype: cast_meta(meta, _dtype),
103+
)
104+
out_node = full_node
105+
106+
if fill_dtype != val.dtype:
107+
# to.dtype takes its dtype positionally, keeping the
108+
# ATen node free of kwargs; the edge dialect uses
109+
# _to_copy, where kwargs are expected.
110+
cast_args, cast_kwargs = (
111+
((full_node,), {"dtype": val.dtype})
112+
if is_edge
113+
else ((full_node, val.dtype), {})
114+
)
115+
cast_node = graph.create_node(
116+
"call_function",
117+
ops["cast"],
118+
cast_args,
119+
cast_kwargs,
120+
)
121+
cast_node.meta = copy_meta(empty_node.meta, cast_meta)
122+
out_node = cast_node
123+
124+
for user in empty_node.users.copy():
125+
user.replace_input_with(empty_node, out_node)
126+
127+
dead_code_elimination_pass(graph_module)
128+
return PassResult(graph_module, True)

‎backends/qualcomm/_passes/qnn_pass_manager.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
DecomposeDiagonal,
3232
DecomposeDivMode,
3333
DecomposeEinsum,
34+
DecomposeEmpty,
3435
DecomposeExpM1,
3536
DecomposeFill,
3637
DecomposeFloorDivide,
@@ -192,6 +193,7 @@ def get_annotation_passes(cls, convert_linear_to_conv2d: bool = False):
192193
DecomposeVar,
193194
DecomposeWrapWithAutocast,
194195
DecomposeEinsum,
196+
DecomposeEmpty,
195197
DecomposeExpM1,
196198
DecomposeFill,
197199
DecomposeGlu,
@@ -224,6 +226,7 @@ def get_export_passes(cls):
224226
DecomposeThreshold,
225227
DecomposeTriu,
226228
DecomposeLinalgVectorNorm,
229+
DecomposeEmpty,
227230
DecomposeExpM1,
228231
DecomposeFill,
229232
DecomposeVar,

‎backends/qualcomm/builders/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ The following PyTorch operators are supported through decomposition or annotatio
515515
| `aten.div.Scalar_mode` | `LiftConstantScalarOperands` → `DecomposeDivMode` |
516516
| `aten.im2col`, `aten.col2im` | `DecomposeColIm` |
517517
| `aten.einsum` | `DecomposeEinsum` |
518+
| `aten.empty.memory_format`, `aten.empty_strided` | `DecomposeEmpty` |
518519
| `aten.special_expm1` | `DecomposeExpM1` |
519520
| `aten.fill.Scalar` | `DecomposeFill` |
520521
| `aten.floor_divide` | `DecomposeFloorDivide` |

‎backends/qualcomm/tests/models.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,40 @@ def forward(self, x):
12051205
return self.embedding(x)
12061206

12071207

1208+
class EmptyMemoryFormat(torch.nn.Module):
1209+
def __init__(self, shape=None, dtype=None, memory_format=torch.contiguous_format):
1210+
super().__init__()
1211+
self.shape = shape
1212+
self.dtype = dtype
1213+
self.memory_format = memory_format
1214+
1215+
def forward(self, x):
1216+
empty = torch.empty(
1217+
x.shape if self.shape is None else self.shape,
1218+
dtype=x.dtype if self.dtype is None else self.dtype,
1219+
memory_format=self.memory_format,
1220+
)
1221+
return torch.add(x, torch.zeros_like(empty).to(x.dtype))
1222+
1223+
1224+
class EmptyStrided(torch.nn.Module):
1225+
def __init__(self, shape, strides, dtype=None):
1226+
super().__init__()
1227+
self.shape = shape
1228+
self.strides = strides
1229+
self.dtype = dtype
1230+
1231+
def forward(self, x):
1232+
# empty_strided() returns uninitialized memory, so zeros_like is used to
1233+
# make the result deterministic and comparable against the eager reference
1234+
empty = torch.empty_strided(
1235+
self.shape,
1236+
self.strides,
1237+
dtype=x.dtype if self.dtype is None else self.dtype,
1238+
)
1239+
return torch.add(x, torch.zeros_like(empty).to(x.dtype))
1240+
1241+
12081242
class Equal(torch.nn.Module):
12091243
def __init__(self):
12101244
super().__init__()

‎backends/qualcomm/tests/rework/gpu/op/test.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,27 @@ def test_embedding(request, kwargs):
519519
Embedding.test(request, kwargs) # noqa: F405
520520

521521

522+
@enumerate_fp_dtype(Tolerance(rtol=1e-1))
523+
@with_gpu_context
524+
def test_empty(request, kwargs):
525+
Empty.test(request, kwargs) # noqa: F405
526+
527+
528+
@enumerate_fp_dtype(Tolerance(rtol=1e-1))
529+
@with_gpu_context
530+
def test_empty_strided(request, kwargs):
531+
EmptyStrided.test(request, kwargs) # noqa: F405
532+
533+
534+
# empty_strided() returns uninitialized memory, so the requested stride is
535+
# unobservable and DecomposeEmpty rewrites even a non-contiguous one into
536+
# full.default, keeping the graph fully delegated.
537+
@enumerate_fp_dtype(Tolerance(rtol=1e-1))
538+
@with_gpu_context
539+
def test_empty_strided_non_contiguous(request, kwargs):
540+
EmptyStrided.test_non_contiguous(request, kwargs) # noqa: F405
541+
542+
522543
@enumerate_fp_dtype(Tolerance(rtol=1e-1))
523544
@with_gpu_context
524545
def test_equal(request, kwargs):

‎backends/qualcomm/tests/rework/htp/op/v68/test.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,27 @@ def test_embedding(request, kwargs):
568568
Embedding.test(request, kwargs) # noqa: F405
569569

570570

571+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
572+
@with_htp_context
573+
def test_empty(request, kwargs):
574+
Empty.test(request, kwargs) # noqa: F405
575+
576+
577+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
578+
@with_htp_context
579+
def test_empty_strided(request, kwargs):
580+
EmptyStrided.test(request, kwargs) # noqa: F405
581+
582+
583+
# empty_strided() returns uninitialized memory, so the requested stride is
584+
# unobservable and DecomposeEmpty rewrites even a non-contiguous one into
585+
# full.default, keeping the graph fully delegated.
586+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
587+
@with_htp_context
588+
def test_empty_strided_non_contiguous(request, kwargs):
589+
EmptyStrided.test_non_contiguous(request, kwargs) # noqa: F405
590+
591+
571592
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
572593
@with_htp_context
573594
def test_equal(request, kwargs):

‎backends/qualcomm/tests/rework/htp/op/v69/test.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,27 @@ def test_embedding(request, kwargs):
564564
Embedding.test(request, kwargs) # noqa: F405
565565

566566

567+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
568+
@with_htp_context
569+
def test_empty(request, kwargs):
570+
Empty.test(request, kwargs) # noqa: F405
571+
572+
573+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
574+
@with_htp_context
575+
def test_empty_strided(request, kwargs):
576+
EmptyStrided.test(request, kwargs) # noqa: F405
577+
578+
579+
# empty_strided() returns uninitialized memory, so the requested stride is
580+
# unobservable and DecomposeEmpty rewrites even a non-contiguous one into
581+
# full.default, keeping the graph fully delegated.
582+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
583+
@with_htp_context
584+
def test_empty_strided_non_contiguous(request, kwargs):
585+
EmptyStrided.test_non_contiguous(request, kwargs) # noqa: F405
586+
587+
567588
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
568589
@with_htp_context
569590
def test_equal(request, kwargs):

‎backends/qualcomm/tests/rework/htp/op/v73/test.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,27 @@ def test_embedding(request, kwargs):
546546
Embedding.test(request, kwargs) # noqa: F405
547547

548548

549+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
550+
@with_htp_context
551+
def test_empty(request, kwargs):
552+
Empty.test(request, kwargs) # noqa: F405
553+
554+
555+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
556+
@with_htp_context
557+
def test_empty_strided(request, kwargs):
558+
EmptyStrided.test(request, kwargs) # noqa: F405
559+
560+
561+
# empty_strided() returns uninitialized memory, so the requested stride is
562+
# unobservable and DecomposeEmpty rewrites even a non-contiguous one into
563+
# full.default, keeping the graph fully delegated.
564+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
565+
@with_htp_context
566+
def test_empty_strided_non_contiguous(request, kwargs):
567+
EmptyStrided.test_non_contiguous(request, kwargs) # noqa: F405
568+
569+
549570
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
550571
@with_htp_context
551572
def test_equal(request, kwargs):

‎backends/qualcomm/tests/rework/htp/op/v75/test.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,27 @@ def test_embedding(request, kwargs):
546546
Embedding.test(request, kwargs) # noqa: F405
547547

548548

549+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
550+
@with_htp_context
551+
def test_empty(request, kwargs):
552+
Empty.test(request, kwargs) # noqa: F405
553+
554+
555+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
556+
@with_htp_context
557+
def test_empty_strided(request, kwargs):
558+
EmptyStrided.test(request, kwargs) # noqa: F405
559+
560+
561+
# empty_strided() returns uninitialized memory, so the requested stride is
562+
# unobservable and DecomposeEmpty rewrites even a non-contiguous one into
563+
# full.default, keeping the graph fully delegated.
564+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
565+
@with_htp_context
566+
def test_empty_strided_non_contiguous(request, kwargs):
567+
EmptyStrided.test_non_contiguous(request, kwargs) # noqa: F405
568+
569+
549570
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
550571
@with_htp_context
551572
def test_equal(request, kwargs):

0 commit comments

Comments
 (0)