Skip to content

Commit 7f1cf77

Browse files
authored
Qualcomm AI Engine Direct - Adding QNN backend support for diagonal core ATen op (#20596)
### Summary Added support for the core ATen op `diagonal` using a decomposition pass and the `permute, view, arange, index_select` ops by: 1. Permute input so `dim1` and `dim2` are the last two dimensions. 2. Reshape (`view`) to flatten the last two dims: `[..., M*N]`. 3. Compute flat diagonal indices via `arange(start, end, stride)`. 4. Use `index_select` on the last dim with the computed indices. Also made a small update to the `new_op_development` skill. ### Test plan ``` python backends/qualcomm/tests/test_qnn_delegate.py -k TestQNNQuantizedOperator.test_qnn_backend_diagonal --soc_model SM8750 --host aisw-vm15-labsd --device 545ee4aa --build_folder build-android python backends/qualcomm/tests/test_qnn_delegate.py -k TestQNNFloatingPointOperator.test_qnn_backend_diagonal --soc_model SM8750 --host aisw-vm15-labsd --device 545ee4aa --build_folder build-android ```
1 parent 2f02f75 commit 7f1cf77

7 files changed

Lines changed: 323 additions & 2 deletions

File tree

.claude/skills/qualcomm/new_op_development.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class MyOpVisitor(NodeVisitor):
6666
- Static params: `weight = get_parameter(self.get_node(node.args[1]), self.edge_program)`
6767
- Scalar params → `AddScalarParam`; Array params → `AddTensorParam`
6868
- Data types: axis/dims=`UINT_32`, epsilon=`FLOAT_32`, booleans=`BOOL_8`
69-
- Int64 index tensors: use `.to(torch.int32)` in builder + add op to `I64_IN_OPS` in `_passes/i64_to_i32.py` for CPU fallback safety (see `op_gather.py` pattern)
69+
- Int64 index tensors: If the op **requires** int64 for PyTorch tracing validation (like `gather`, `scatter`), add to `I64_IN_OPS` in `i64_to_i32.py` + `.to(torch.int32)` in builder. If the op **accepts** int32 (like `index_select`), produce int32 directly via `dtype=torch.int32` — no `I64_IN_OPS` entry needed.
7070

7171
## Step 5: Register Builder (`builders/__init__.py`)
7272

@@ -237,7 +237,7 @@ class DecomposeMyOp(ExportPass):
237237
- **Multi-output ops**: Use `wrapper_idx=i` + `getitem` skip op
238238
- **Negative dims**: QNN needs positive → `dim = dim % len(shape)`
239239
- **QCOM_AXIS_ORDER**: `LayoutTransform` permutes NCHW→NHWC; remap axis with `.index(dim)`. `get_tensor()` auto-permutes data.
240-
- **Int64 indices**: Add to `I64_IN_OPS` in `i64_to_i32.py` + `.to(torch.int32)` in builder (see `op_gather.py`)
240+
- **Int64 indices**: Only add to `I64_IN_OPS` if the op **requires** int64 at tracing time (e.g., `gather`, `scatter`). If the op accepts int32 (e.g., `index_select`), produce int32 directly in the decomposition pass. Check PyTorch docs for actual dtype requirements.
241241
- **Recompose passes**: Detect primitive sequences and replace with single native op. Ref: `recompose_pixel_unshuffle.py`
242242
- **`partition/common_defs.py`**: Remove op from `to_be_implemented_operator` when adding support
243243
- **HTP doc bugs**: If runtime fails but docs say supported → test on-device always.

backends/qualcomm/_passes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .decompose_binary_alpha import DecomposeBinaryAlpha
2121
from .decompose_cdist import DecomposeCDist
2222
from .decompose_col_im import DecomposeColIm
23+
from .decompose_diagonal import DecomposeDiagonal
2324
from .decompose_div_mode import DecomposeDivMode
2425
from .decompose_einsum import DecomposeEinsum
2526
from .decompose_expm1 import DecomposeExpM1
@@ -84,6 +85,7 @@
8485
DecomposeBinaryAlpha,
8586
DecomposeCDist,
8687
DecomposeColIm,
88+
DecomposeDiagonal,
8789
DecomposeDivMode,
8890
DecomposeEinsum,
8991
DecomposeExpM1,
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
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+
12+
from .utils import copy_meta
13+
14+
15+
class DecomposeDiagonal(ExportPass):
16+
"""
17+
Decompose diagonal operation into permute + view + arange + index_select.
18+
19+
torch.diagonal(input, offset=0, dim1=0, dim2=1) extracts diagonal elements from the 2D submatrix defined by dim1 and dim2.
20+
21+
Decomposition strategy:
22+
1. Permute input so dim1 and dim2 are the last two dimensions.
23+
2. Reshape (view) to flatten the last two dims: [..., M*N]
24+
3. Compute flat diagonal indices via arange(start, end, stride).
25+
4. Use index_select on the last dim with the computed indices.
26+
"""
27+
28+
def __init__(self) -> None:
29+
super().__init__()
30+
self._edge_targets = {
31+
exir_ops.edge.aten.diagonal_copy.default,
32+
}
33+
self._aten_targets = {
34+
torch.ops.aten.diagonal.default,
35+
}
36+
37+
def _get_ops(self, is_edge):
38+
if is_edge:
39+
return {
40+
"permute": exir_ops.edge.aten.permute_copy.default,
41+
"view": exir_ops.edge.aten.view_copy.default,
42+
"arange": exir_ops.edge.aten.arange.start_step,
43+
"index_select": exir_ops.edge.aten.index_select.default,
44+
"full": exir_ops.edge.aten.full.default,
45+
}
46+
return {
47+
"permute": torch.ops.aten.permute.default,
48+
"view": torch.ops.aten.view.default,
49+
"arange": torch.ops.aten.arange.start_step,
50+
"index_select": torch.ops.aten.index_select.default,
51+
"full": torch.ops.aten.full.default,
52+
}
53+
54+
def _compute_diag_params(self, M, N, offset):
55+
"""Compute diagonal size, start offset, and stride from matrix dims and offset."""
56+
if offset >= 0:
57+
diag_size = min(M, N - offset)
58+
start_offset = offset
59+
else:
60+
diag_size = min(M + offset, N)
61+
start_offset = (-offset) * N
62+
stride = N + 1
63+
return diag_size, start_offset, stride
64+
65+
def _compute_layout(self, input_shape, dim1, dim2):
66+
"""Compute permutation order, remaining dims, and flattened shape."""
67+
ndim = len(input_shape)
68+
M, N = input_shape[dim1], input_shape[dim2]
69+
remaining_dims = [i for i in range(ndim) if i != dim1 and i != dim2]
70+
perm = remaining_dims + [dim1, dim2]
71+
remaining_shapes = [input_shape[i] for i in remaining_dims]
72+
flattened_shape = remaining_shapes + [M * N]
73+
need_permute = perm != list(range(ndim))
74+
return remaining_dims, perm, flattened_shape, need_permute
75+
76+
def _build_diagonal_graph(
77+
self,
78+
node,
79+
graph,
80+
ops,
81+
input_node,
82+
input_val,
83+
perm,
84+
flattened_shape,
85+
need_permute,
86+
start_offset,
87+
diag_size,
88+
stride,
89+
):
90+
"""Build the decomposed graph: permute → view → arange → index_select."""
91+
meta = node.meta
92+
fake_mode = meta["val"].fake_mode
93+
94+
with graph.inserting_before(node):
95+
# Step 1: Permute dim1, dim2 to last positions (if needed)
96+
if need_permute:
97+
permute_node = graph.create_node(
98+
"call_function", ops["permute"], (input_node, perm)
99+
)
100+
permute_node.meta = copy_meta(
101+
meta, lambda m: {**m, "val": input_val.permute(perm)}
102+
)
103+
reshape_input = permute_node
104+
else:
105+
reshape_input = input_node
106+
107+
# Step 2: Reshape [..., M, N] -> [..., M*N]
108+
view_node = graph.create_node(
109+
"call_function", ops["view"], (reshape_input, flattened_shape)
110+
)
111+
if need_permute:
112+
view_val = input_val.permute(perm).contiguous().view(flattened_shape)
113+
else:
114+
view_val = input_val.contiguous().view(flattened_shape)
115+
view_node.meta = copy_meta(meta, lambda m: {**m, "val": view_val})
116+
117+
# Step 3: Compute flat diagonal indices
118+
arange_end = start_offset + diag_size * stride
119+
arange_node = graph.create_node(
120+
"call_function",
121+
ops["arange"],
122+
(start_offset, arange_end, stride),
123+
{
124+
"dtype": torch.int32,
125+
"layout": torch.strided,
126+
"device": torch.device("cpu"),
127+
"pin_memory": False,
128+
},
129+
)
130+
arange_node.meta = copy_meta(
131+
meta,
132+
lambda m: {
133+
**m,
134+
"val": fake_mode.from_tensor(
135+
torch.arange(
136+
start_offset, arange_end, stride, dtype=torch.int32
137+
)
138+
),
139+
},
140+
)
141+
142+
# Step 4: index_select on last dim
143+
last_dim = len(flattened_shape) - 1
144+
index_select_node = graph.create_node(
145+
"call_function",
146+
ops["index_select"],
147+
(view_node, last_dim, arange_node),
148+
)
149+
index_select_node.meta = copy_meta(meta)
150+
151+
return index_select_node
152+
153+
def _decompose_diagonal(self, node, graph):
154+
input_node = node.args[0]
155+
is_edge = isinstance(node.target, EdgeOpOverload)
156+
ops = self._get_ops(is_edge)
157+
158+
# Parse diagonal args: diagonal(input, offset=0, dim1=0, dim2=1)
159+
offset = node.args[1] if len(node.args) > 1 else 0
160+
dim1 = node.args[2] if len(node.args) > 2 else 0
161+
dim2 = node.args[3] if len(node.args) > 3 else 1
162+
163+
# Get input shape from meta
164+
input_val = input_node.meta["val"]
165+
input_shape = list(input_val.shape)
166+
ndim = len(input_shape)
167+
168+
# Normalize negative dims
169+
if dim1 < 0:
170+
dim1 += ndim
171+
if dim2 < 0:
172+
dim2 += ndim
173+
174+
M, N = input_shape[dim1], input_shape[dim2]
175+
176+
# Compute diagonal parameters
177+
diag_size, start_offset, stride = self._compute_diag_params(M, N, offset)
178+
179+
if diag_size <= 0:
180+
# Match PyTorch behavior: return empty tensor when offset exceeds dims
181+
remaining_dims = [i for i in range(ndim) if i != dim1 and i != dim2]
182+
empty_shape = [input_shape[i] for i in remaining_dims] + [0]
183+
with graph.inserting_before(node):
184+
empty_node = graph.create_node(
185+
"call_function", ops["full"], (empty_shape, 0.0)
186+
)
187+
empty_node.meta = copy_meta(node.meta)
188+
for user in node.users.copy():
189+
user.replace_input_with(node, empty_node)
190+
return
191+
192+
# Compute layout
193+
remaining_dims, perm, flattened_shape, need_permute = self._compute_layout(
194+
input_shape, dim1, dim2
195+
)
196+
197+
# Build decomposed graph
198+
result_node = self._build_diagonal_graph(
199+
node,
200+
graph,
201+
ops,
202+
input_node,
203+
input_val,
204+
perm,
205+
flattened_shape,
206+
need_permute,
207+
start_offset,
208+
diag_size,
209+
stride,
210+
)
211+
212+
# Replace original node
213+
for user in node.users.copy():
214+
user.replace_input_with(node, result_node)
215+
216+
def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
217+
graph = graph_module.graph
218+
219+
all_targets = self._edge_targets | self._aten_targets
220+
nodes_to_decompose = [
221+
n
222+
for n in graph.nodes
223+
if n.op == "call_function" and n.target in all_targets
224+
]
225+
226+
if not nodes_to_decompose:
227+
return PassResult(graph_module, False)
228+
229+
for node in nodes_to_decompose:
230+
self._decompose_diagonal(node, graph)
231+
232+
graph.eliminate_dead_code()
233+
graph_module.recompile()
234+
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
@@ -26,6 +26,7 @@
2626
DecomposeBinaryAlpha,
2727
DecomposeCDist,
2828
DecomposeColIm,
29+
DecomposeDiagonal,
2930
DecomposeDivMode,
3031
DecomposeEinsum,
3132
DecomposeExpM1,
@@ -129,6 +130,7 @@ def get_default_pass_activations(cls):
129130
(DecomposeAtan2, True),
130131
(DecomposeColIm, True),
131132
(DecomposeCDist, True),
133+
(DecomposeDiagonal, True),
132134
(DecomposeDivMode, True),
133135
(DecomposeFill, True),
134136
(DecomposeHyperbolicVariants, True),
@@ -168,6 +170,7 @@ def get_annotation_passes(cls):
168170
DecomposeAtan2,
169171
DecomposeBinaryAlpha,
170172
DecomposeCDist,
173+
DecomposeDiagonal,
171174
DecomposeDivMode,
172175
DecomposeMaxPool3d,
173176
DecomposePad,
@@ -286,6 +289,7 @@ def get_passes_dependency_for_capture_program(cls):
286289
DecomposeAtan2: [RemoveRedundancy],
287290
DecomposeColIm: [FoldQDQ],
288291
DecomposeCDist: [RemoveRedundancy],
292+
DecomposeDiagonal: [RemoveRedundancy],
289293
DecomposeDivMode: [RemoveRedundancy],
290294
DecomposeFill: [RemoveRedundancy],
291295
DecomposeHyperbolicVariants: [RemoveRedundancy],

backends/qualcomm/builders/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ The following PyTorch operators are supported through decomposition or annotatio
508508
| `aten.add` (with alpha), `aten.sub` (with alpha) | `DecomposeBinaryAlpha` |
509509
| `aten.cdist`, `aten._cdist_forward` | `DecomposeCDist` |
510510
| `aten.cosh` | `DecomposeHyperbolicVariants` |
511+
| `aten.diagonal` | `DecomposeDiagonal` |
511512
| `aten.div.Tensor_mode` | `DecomposeDivMode` |
512513
| `aten.div.Scalar_mode` | `LiftConstantScalarOperands``DecomposeDivMode` |
513514
| `aten.im2col`, `aten.col2im` | `DecomposeColIm` |

backends/qualcomm/tests/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,17 @@ def forward(self, x):
10391039
return x.cumsum(dim=0)
10401040

10411041

1042+
class Diagonal(torch.nn.Module):
1043+
def __init__(self, offset=0, dim1=0, dim2=1):
1044+
super().__init__()
1045+
self.offset = offset
1046+
self.dim1 = dim1
1047+
self.dim2 = dim2
1048+
1049+
def forward(self, x):
1050+
return torch.diagonal(x, offset=self.offset, dim1=self.dim1, dim2=self.dim2)
1051+
1052+
10421053
class Div(torch.nn.Module):
10431054
def __init__(self):
10441055
super().__init__()

0 commit comments

Comments
 (0)