Skip to content

Commit 398a89f

Browse files
Andrew Pullinmeta-codesync[bot]
authored andcommitted
Add DecomposeGruPass for ARM backend
Summary: Adds a decomposition pass that transforms aten.gru.input into elementary ops supported by TOSA (matmul, sigmoid, tanh, mul, add, slice, cat). GRU cell equations per timestep: r_t = sigmoid(x_t @ W_ir.T + b_ir + h_{t-1} @ W_hr.T + b_hr) z_t = sigmoid(x_t @ W_iz.T + b_iz + h_{t-1} @ W_hz.T + b_hz) n_t = tanh(x_t @ W_in.T + b_in + r_t * (h_{t-1} @ W_hn.T + b_hn)) h_t = n_t + z_t * (h_{t-1} - n_t) Features: - Multi-layer GRU support - Bidirectional GRU support - With/without bias - batch_first support - Batched gate computation (2 mm ops per timestep instead of 6) Differential Revision: D92058313
1 parent 462a4af commit 398a89f

4 files changed

Lines changed: 489 additions & 0 deletions

File tree

backends/arm/_passes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from .decompose_glu_pass import DecomposeGluPass # noqa
5454
from .decompose_grouped_conv_pass import DecomposeGroupedConvPass # noqa
5555
from .decompose_groupnorm_pass import DecomposeGroupNormPass # noqa
56+
from .decompose_gru_pass import DecomposeGruPass # noqa
5657
from .decompose_index_copy_pass import DecomposeIndexCopyPass # noqa
5758
from .decompose_index_select_to_gather_pass import ( # noqa
5859
DecomposeIndexSelectToGatherPass,

backends/arm/_passes/arm_pass_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
DecomposeGluPass,
6262
DecomposeGroupedConvPass,
6363
DecomposeGroupNormPass,
64+
DecomposeGruPass,
6465
DecomposeIndexCopyPass,
6566
DecomposeIndexSelectToGatherPass,
6667
DecomposeIndexTensorToGatherPass,
@@ -365,6 +366,7 @@ def _tosa_pipeline(
365366
ConvertToClampPass(),
366367
DecomposeTOSAUnsupportedClampPass(),
367368
DecomposeGroupNormPass(),
369+
DecomposeGruPass(),
368370
DecomposeLayerNormPass(),
369371
DecomposeVarPass(),
370372
DecomposeMeanDimPass(exported_program.graph_module, self.tosa_spec),
@@ -585,6 +587,7 @@ def transform_for_annotation_pipeline(self, graph_module: GraphModule):
585587
self.add_passes(
586588
[
587589
NormalizeWhileInitialArgsPass(use_exir_clone=False, tfa_pass=True),
590+
DecomposeGruPass(tfa_pass=True),
588591
DecomposeNotEqualPass(tfa_pass=True),
589592
DecomposeCosineSimilarityPass(tfa_pass=True),
590593
DecomposeGluPass(tfa_pass=True),
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
import operator
7+
from typing import List, Set, Tuple, Type
8+
9+
import torch
10+
from executorch.backends.arm._passes.arm_pass import ArmPass
11+
from executorch.backends.arm._passes.arm_pass_utils import create_node
12+
from executorch.backends.arm._passes.insert_table_ops import InsertTableOpsPass
13+
from executorch.exir.pass_base import ExportPass, PassResult
14+
15+
16+
class DecomposeGruPass(ArmPass):
17+
"""Decomposes aten.gru.input into elementary ops supported by TOSA.
18+
19+
GRU cell equations per timestep:
20+
r_t = sigmoid(x_t @ W_ir.T + b_ir + h_{t-1} @ W_hr.T + b_hr)
21+
z_t = sigmoid(x_t @ W_iz.T + b_iz + h_{t-1} @ W_hz.T + b_hz)
22+
n_t = tanh(x_t @ W_in.T + b_in + r_t * (h_{t-1} @ W_hn.T + b_hn))
23+
h_t = n_t + z_t * (h_{t-1} - n_t)
24+
25+
The weights are batched: one mm computes all three gates at once, then the
26+
result is sliced into r/z/n components. This yields 2 mm ops per timestep
27+
instead of 6.
28+
29+
Supports multi-layer, bidirectional, with/without bias, and batch_first.
30+
31+
"""
32+
33+
_passes_required_after: Set[Type[ExportPass]] = {InsertTableOpsPass}
34+
35+
_TARGET = torch.ops.aten.gru.input
36+
37+
_mm = torch.ops.aten.mm.default
38+
_t = torch.ops.aten.t.default
39+
_add = torch.ops.aten.add.Tensor
40+
_sub = torch.ops.aten.sub.Tensor
41+
_mul = torch.ops.aten.mul.Tensor
42+
_sigmoid = torch.ops.aten.sigmoid.default
43+
_tanh = torch.ops.aten.tanh.default
44+
_slice = torch.ops.aten.slice_copy.Tensor
45+
_unsqueeze = torch.ops.aten.unsqueeze.default
46+
_cat = torch.ops.aten.cat.default
47+
_select = torch.ops.aten.select_copy.int
48+
49+
def _build_direction(
50+
self,
51+
graph: torch.fx.Graph,
52+
node: torch.fx.Node,
53+
current_input: torch.fx.Node,
54+
h_prev: torch.fx.Node,
55+
weight_ih: torch.fx.Node,
56+
weight_hh: torch.fx.Node,
57+
bias_ih,
58+
bias_hh,
59+
hidden_size: int,
60+
seq_len: int,
61+
time_dim: int,
62+
reverse: bool,
63+
) -> Tuple[List[torch.fx.Node], torch.fx.Node]:
64+
"""Build GRU cell computation for one direction.
65+
66+
Returns (timestep_outputs, h_final) where timestep_outputs are
67+
unsqueezed hidden states in forward time order.
68+
69+
"""
70+
w_ih_t = create_node(graph, self._t, args=(weight_ih,), from_node=node)
71+
w_hh_t = create_node(graph, self._t, args=(weight_hh,), from_node=node)
72+
73+
time_indices = range(seq_len - 1, -1, -1) if reverse else range(seq_len)
74+
timestep_outputs = []
75+
76+
for t_idx in time_indices:
77+
x_t = create_node(
78+
graph,
79+
self._select,
80+
args=(current_input, time_dim, t_idx),
81+
from_node=node,
82+
)
83+
84+
gates_x = create_node(graph, self._mm, args=(x_t, w_ih_t), from_node=node)
85+
gates_h = create_node(
86+
graph, self._mm, args=(h_prev, w_hh_t), from_node=node
87+
)
88+
89+
if bias_ih is not None:
90+
gates_x = create_node(
91+
graph, self._add, args=(gates_x, bias_ih), from_node=node
92+
)
93+
if bias_hh is not None:
94+
gates_h = create_node(
95+
graph, self._add, args=(gates_h, bias_hh), from_node=node
96+
)
97+
98+
H = hidden_size
99+
r_x = create_node(
100+
graph, self._slice, args=(gates_x, 1, 0, H), from_node=node
101+
)
102+
z_x = create_node(
103+
graph, self._slice, args=(gates_x, 1, H, 2 * H), from_node=node
104+
)
105+
n_x = create_node(
106+
graph,
107+
self._slice,
108+
args=(gates_x, 1, 2 * H, 3 * H),
109+
from_node=node,
110+
)
111+
r_h = create_node(
112+
graph, self._slice, args=(gates_h, 1, 0, H), from_node=node
113+
)
114+
z_h = create_node(
115+
graph, self._slice, args=(gates_h, 1, H, 2 * H), from_node=node
116+
)
117+
n_h = create_node(
118+
graph,
119+
self._slice,
120+
args=(gates_h, 1, 2 * H, 3 * H),
121+
from_node=node,
122+
)
123+
124+
r_pre = create_node(graph, self._add, args=(r_x, r_h), from_node=node)
125+
r_t = create_node(graph, self._sigmoid, args=(r_pre,), from_node=node)
126+
127+
z_pre = create_node(graph, self._add, args=(z_x, z_h), from_node=node)
128+
z_t = create_node(graph, self._sigmoid, args=(z_pre,), from_node=node)
129+
130+
r_n_h = create_node(graph, self._mul, args=(r_t, n_h), from_node=node)
131+
n_pre = create_node(graph, self._add, args=(n_x, r_n_h), from_node=node)
132+
n_t = create_node(graph, self._tanh, args=(n_pre,), from_node=node)
133+
134+
diff = create_node(graph, self._sub, args=(h_prev, n_t), from_node=node)
135+
z_diff = create_node(graph, self._mul, args=(z_t, diff), from_node=node)
136+
h_t = create_node(graph, self._add, args=(n_t, z_diff), from_node=node)
137+
h_prev = h_t
138+
139+
h_t_expanded = create_node(
140+
graph, self._unsqueeze, args=(h_t, time_dim), from_node=node
141+
)
142+
timestep_outputs.append(h_t_expanded)
143+
144+
# Backward outputs were appended in reverse time order; flip to
145+
# forward order so they align with the forward direction for concat.
146+
if reverse:
147+
timestep_outputs.reverse()
148+
149+
return timestep_outputs, h_prev
150+
151+
def call(self, graph_module: torch.fx.GraphModule): # noqa: C901
152+
graph = graph_module.graph
153+
made_changes = False
154+
155+
for node in list(graph.nodes):
156+
if (
157+
node.op != "call_function"
158+
or node.target != self._TARGET
159+
or not self.allowed_to_transform(node.meta)
160+
):
161+
continue
162+
163+
args = node.args
164+
input_node = args[0]
165+
hx_node = args[1]
166+
params = args[2]
167+
has_biases = args[3]
168+
num_layers = args[4]
169+
# dropout (args[5]) and train (args[6]) are unused at inference
170+
bidirectional = args[7]
171+
batch_first = args[8]
172+
173+
input_val = input_node.meta["val"]
174+
hx_val = hx_node.meta["val"]
175+
176+
if batch_first:
177+
seq_len = input_val.shape[1]
178+
time_dim = 1
179+
else:
180+
seq_len = input_val.shape[0]
181+
time_dim = 0
182+
183+
hidden_size = hx_val.shape[-1]
184+
num_directions = 2 if bidirectional else 1
185+
# Params per layer: (w_ih, w_hh[, b_ih, b_hh]) * num_directions
186+
dir_step = 4 if has_biases else 2
187+
layer_step = dir_step * num_directions
188+
189+
with graph.inserting_before(node):
190+
current_input = input_node
191+
layer_final_hiddens = []
192+
193+
for layer_idx in range(num_layers):
194+
layer_offset = layer_idx * layer_step
195+
196+
# Forward direction
197+
fw_off = layer_offset
198+
fw_w_ih = params[fw_off]
199+
fw_w_hh = params[fw_off + 1]
200+
fw_b_ih = params[fw_off + 2] if has_biases else None
201+
fw_b_hh = params[fw_off + 3] if has_biases else None
202+
203+
fw_h0 = create_node(
204+
graph,
205+
self._select,
206+
args=(hx_node, 0, num_directions * layer_idx),
207+
from_node=node,
208+
)
209+
210+
fw_outputs, fw_h_final = self._build_direction(
211+
graph,
212+
node,
213+
current_input,
214+
fw_h0,
215+
fw_w_ih,
216+
fw_w_hh,
217+
fw_b_ih,
218+
fw_b_hh,
219+
hidden_size,
220+
seq_len,
221+
time_dim,
222+
reverse=False,
223+
)
224+
225+
if bidirectional:
226+
bw_off = layer_offset + dir_step
227+
bw_w_ih = params[bw_off]
228+
bw_w_hh = params[bw_off + 1]
229+
bw_b_ih = params[bw_off + 2] if has_biases else None
230+
bw_b_hh = params[bw_off + 3] if has_biases else None
231+
232+
bw_h0 = create_node(
233+
graph,
234+
self._select,
235+
args=(hx_node, 0, 2 * layer_idx + 1),
236+
from_node=node,
237+
)
238+
239+
bw_outputs, bw_h_final = self._build_direction(
240+
graph,
241+
node,
242+
current_input,
243+
bw_h0,
244+
bw_w_ih,
245+
bw_w_hh,
246+
bw_b_ih,
247+
bw_b_hh,
248+
hidden_size,
249+
seq_len,
250+
time_dim,
251+
reverse=True,
252+
)
253+
254+
# Concatenate fw + bw at each timestep along feature dim
255+
merged = []
256+
for fw_out, bw_out in zip(fw_outputs, bw_outputs):
257+
merged.append(
258+
create_node(
259+
graph,
260+
self._cat,
261+
args=([fw_out, bw_out], -1),
262+
from_node=node,
263+
)
264+
)
265+
266+
layer_output = create_node(
267+
graph,
268+
self._cat,
269+
args=(merged, time_dim),
270+
from_node=node,
271+
)
272+
273+
layer_final_hiddens.append(
274+
create_node(
275+
graph,
276+
self._unsqueeze,
277+
args=(fw_h_final, 0),
278+
from_node=node,
279+
)
280+
)
281+
layer_final_hiddens.append(
282+
create_node(
283+
graph,
284+
self._unsqueeze,
285+
args=(bw_h_final, 0),
286+
from_node=node,
287+
)
288+
)
289+
else:
290+
layer_output = create_node(
291+
graph,
292+
self._cat,
293+
args=(fw_outputs, time_dim),
294+
from_node=node,
295+
)
296+
297+
layer_final_hiddens.append(
298+
create_node(
299+
graph,
300+
self._unsqueeze,
301+
args=(fw_h_final, 0),
302+
from_node=node,
303+
)
304+
)
305+
306+
current_input = layer_output
307+
308+
# Build h_n
309+
if len(layer_final_hiddens) == 1:
310+
h_n = layer_final_hiddens[0]
311+
else:
312+
h_n = create_node(
313+
graph,
314+
self._cat,
315+
args=(layer_final_hiddens, 0),
316+
from_node=node,
317+
)
318+
319+
output_node = current_input
320+
321+
# Replace getitem users: GRU returns (output, h_n)
322+
getitem_nodes = []
323+
for user in list(node.users.keys()):
324+
if user.target == operator.getitem:
325+
idx = user.args[1]
326+
if idx == 0:
327+
user.replace_all_uses_with(output_node)
328+
elif idx == 1:
329+
user.replace_all_uses_with(h_n)
330+
getitem_nodes.append(user)
331+
332+
# Erase getitem nodes then the GRU node explicitly;
333+
# eliminate_dead_code does not remove GRU because aten
334+
# considers it impure (may have dropout side-effects).
335+
for gi in getitem_nodes:
336+
graph.erase_node(gi)
337+
graph.erase_node(node)
338+
made_changes = True
339+
340+
if not made_changes:
341+
return PassResult(graph_module, False)
342+
343+
graph_module.recompile()
344+
graph_module = super().call(graph_module).graph_module
345+
return PassResult(graph_module, True)

0 commit comments

Comments
 (0)