Skip to content

Commit 6755ea3

Browse files
authored
Arm Backend: Add Silero VAD Ethos-U example and recurrent-state lowering (pytorch#22172)
### Summary Silero VAD is a compact streaming model that demonstrates a realistic recurrent-state workload on Ethos-U85 rather than another stateless classification example. This PR adds an in-tree Silero VAD Ethos-U example covering model preparation, PT2E quantization, Arm lowering, bare-metal execution on Corstone-320, and host-side output validation. It also adds the reusable Arm backend support needed to lower the LSTMCell gate/update pattern while preserving model-owned mutable hidden and cell state between audio frames. The runtime accepts one 576-sample audio input per frame, keeps recurrent state inside the model, prints speech probabilities and detected segments over serial, and validates the FVP results against export-time reference probabilities. Tracking: MLETORCH-2112 This PR was developed with OpenAI Codex assistance. ### Test plan - `python -m pytest -q backends/arm/test/passes/test_mutable_buffers.py backends/arm/test/misc/test_silero_vad_example.py` — 14 passed. - `backends/arm/test/test_arm_ootb.sh run_silero_vad_e2e_ethos_u` — exports the PTE, builds the runtime, runs Corstone-320 / Ethos-U85-256 FVP, and compares 79 output frames. - FVP validation: maximum absolute error `0.156800`, mean absolute error `0.018459`, and zero threshold-decision mismatches at `0.55`. - Repository pre-push lint, license, commit-message, and Arm documentation checks passed. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani --------- Signed-off-by: Usamah Zaheer <usamah.zaheer@arm.com>
1 parent 60ae401 commit 6755ea3

22 files changed

Lines changed: 2620 additions & 74 deletions

‎backends/arm/_passes/arm_pass.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
from typing import Any, List, Optional, Set, Type
1212

1313
import torch
14+
from executorch.backends.arm._passes.mutable_buffer_utils import (
15+
collect_mutable_buffer_infos,
16+
restore_mutable_buffer_targets,
17+
)
1418
from executorch.backends.arm.constants import DISALLOW_TFA_META_KEY
1519
from executorch.backends.arm.tosa.mapping import TosaSpecialDtype
1620
from executorch.exir.dialects._ops import ops as exir_ops
@@ -217,8 +221,14 @@ def __call__(self, graph_module: GraphModule) -> PassResult | None:
217221
if not self.should_run_pass(graph_module):
218222
self.ensures(graph_module)
219223
return PassResult(graph_module, False)
224+
mutable_buffers = collect_mutable_buffer_infos(graph_module)
220225
res = self.call(graph_module)
221226
self.ensures(graph_module)
227+
if res is not None:
228+
res = PassResult(
229+
restore_mutable_buffer_targets(res.graph_module, mutable_buffers),
230+
res.modified,
231+
)
222232
return res
223233

224234

‎backends/arm/_passes/arm_pass_manager.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@
182182
TosaLoweringContext,
183183
TosaSpecification,
184184
)
185-
186185
from executorch.exir import ExportedProgram
187186
from executorch.exir._program_utils import _get_updated_graph_signature
188187
from executorch.exir.pass_base import (
@@ -822,4 +821,4 @@ def transform_for_annotation_pipeline(self, graph_module: GraphModule):
822821
]
823822
)
824823

825-
return self._transform_graph_module(graph_module)
824+
return GraphModulePassManager(self.passes)(graph_module).graph_module

‎backends/arm/_passes/constant_folding_pass.py‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import torch
99
from executorch.backends.arm._passes import ArmPass
1010
from executorch.exir.pass_base import ExportPass, PassResult
11-
from torch._export.passes.constant_folding import constant_fold
11+
from torchao.quantization.pt2e.constant_fold import constant_fold
1212

1313

1414
class ConstantFoldingPass(ArmPass):
@@ -52,19 +52,8 @@ def _ensure_param_attr(
5252

5353
def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
5454
before_graph = str(graph_module.graph)
55-
56-
# The ConstantFolder checks for this attribute which does not always exist
57-
faked_dequantize_affine = False
58-
if not hasattr(torch.ops.pt2e_quant, "dequantize_affine"):
59-
torch.ops.pt2e_quant.dequantize_affine = None # type: ignore[attr-defined]
60-
faked_dequantize_affine = True
61-
6255
constant_fold(graph_module)
6356

64-
# Remove attribute again if added to not affect global scope
65-
if faked_dequantize_affine:
66-
del torch.ops.pt2e_quant.dequantize_affine # type: ignore[attr-defined]
67-
6857
modified = before_graph != str(graph_module.graph)
6958

7059
# Constant folding may have replaced some parameters with tensors, so we need to ensure they are still parameters for the backend to work correctly.

‎backends/arm/_passes/decompose_lstm_pass.py‎

Lines changed: 135 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# LICENSE file in the root directory of this source tree.
77

88
import operator
9-
from typing import List, Set, Tuple, Type
9+
from typing import cast, List, Optional, Set, Tuple, Type
1010

1111
import torch
1212
from executorch.backends.arm._passes.arm_pass import ArmPass
@@ -18,7 +18,8 @@
1818

1919

2020
class DecomposeLstmPass(ArmPass):
21-
"""Decomposes aten.lstm.input into elementary ops supported by TOSA.
21+
"""Decomposes aten.lstm.input and aten.lstm_cell.default into elementary ops
22+
supported by TOSA.
2223
2324
LSTM cell equations per timestep:
2425
i_t = sigmoid(x_t @ W_ii.T + b_ii + h_{t-1} @ W_hi.T + b_hi)
@@ -32,13 +33,17 @@ class DecomposeLstmPass(ArmPass):
3233
the result is sliced into i/f/g/o components. This yields 2 mm ops per
3334
timestep instead of 8.
3435
35-
Supports multi-layer, bidirectional, with/without bias, and batch_first.
36+
``lstm_cell`` applies the equations once. ``lstm.input`` unrolls the
37+
sequence and reuses the same cell-step decomposition at each timestep.
38+
Full LSTM supports multi-layer, bidirectional, with/without bias, and
39+
batch_first.
3640
3741
"""
3842

3943
_passes_required_after: Set[Type[ExportPass]] = set()
4044

4145
_TARGET = torch.ops.aten.lstm.input
46+
_TARGET_CELL = torch.ops.aten.lstm_cell.default
4247

4348
_linear = torch.ops.aten.linear.default
4449
_add = torch.ops.aten.add.Tensor
@@ -59,8 +64,8 @@ def _build_direction(
5964
c_prev: torch.fx.Node,
6065
weight_ih: torch.fx.Node,
6166
weight_hh: torch.fx.Node,
62-
bias_ih,
63-
bias_hh,
67+
bias_ih: Optional[torch.fx.Node],
68+
bias_hh: Optional[torch.fx.Node],
6469
hidden_size: int,
6570
seq_len: int,
6671
time_dim: int,
@@ -82,50 +87,21 @@ def _build_direction(
8287
from_node=node,
8388
)
8489

85-
gates_x = create_node(
86-
graph, self._linear, args=(x_t, weight_ih, bias_ih), from_node=node
87-
)
88-
gates_h = create_node(
89-
graph, self._linear, args=(h_prev, weight_hh, bias_hh), from_node=node
90-
)
91-
92-
gates = create_node(
93-
graph, self._add, args=(gates_x, gates_h), from_node=node
94-
)
95-
96-
H = hidden_size
97-
i_pre = create_node(
98-
graph, self._slice, args=(gates, 1, 0, H), from_node=node
99-
)
100-
f_pre = create_node(
101-
graph, self._slice, args=(gates, 1, H, 2 * H), from_node=node
102-
)
103-
g_pre = create_node(
104-
graph, self._slice, args=(gates, 1, 2 * H, 3 * H), from_node=node
105-
)
106-
o_pre = create_node(
107-
graph, self._slice, args=(gates, 1, 3 * H, 4 * H), from_node=node
90+
h_prev, c_prev = self._build_cell_step(
91+
graph,
92+
node,
93+
x_t,
94+
h_prev,
95+
c_prev,
96+
weight_ih,
97+
weight_hh,
98+
bias_ih,
99+
bias_hh,
100+
hidden_size,
108101
)
109102

110-
i_t = create_node(graph, self._sigmoid, args=(i_pre,), from_node=node)
111-
f_t = create_node(graph, self._sigmoid, args=(f_pre,), from_node=node)
112-
g_t = create_node(graph, self._tanh, args=(g_pre,), from_node=node)
113-
o_t = create_node(graph, self._sigmoid, args=(o_pre,), from_node=node)
114-
115-
# c_t = f_t * c_{t-1} + i_t * g_t
116-
f_c = create_node(graph, self._mul, args=(f_t, c_prev), from_node=node)
117-
i_g = create_node(graph, self._mul, args=(i_t, g_t), from_node=node)
118-
c_t = create_node(graph, self._add, args=(f_c, i_g), from_node=node)
119-
120-
# h_t = o_t * tanh(c_t)
121-
tanh_c = create_node(graph, self._tanh, args=(c_t,), from_node=node)
122-
h_t = create_node(graph, self._mul, args=(o_t, tanh_c), from_node=node)
123-
124-
h_prev = h_t
125-
c_prev = c_t
126-
127103
h_t_expanded = create_node(
128-
graph, self._unsqueeze, args=(h_t, time_dim), from_node=node
104+
graph, self._unsqueeze, args=(h_prev, time_dim), from_node=node
129105
)
130106
timestep_outputs.append(h_t_expanded)
131107

@@ -134,17 +110,129 @@ def _build_direction(
134110

135111
return timestep_outputs, h_prev, c_prev
136112

113+
def _build_cell_step(
114+
self,
115+
graph: torch.fx.Graph,
116+
node: torch.fx.Node,
117+
input_node: torch.fx.Node,
118+
h_prev: torch.fx.Node,
119+
c_prev: torch.fx.Node,
120+
weight_ih: torch.fx.Node,
121+
weight_hh: torch.fx.Node,
122+
bias_ih: Optional[torch.fx.Node],
123+
bias_hh: Optional[torch.fx.Node],
124+
hidden_size: int,
125+
) -> Tuple[torch.fx.Node, torch.fx.Node]:
126+
gates_x = create_node(
127+
graph, self._linear, args=(input_node, weight_ih, bias_ih), from_node=node
128+
)
129+
gates_h = create_node(
130+
graph, self._linear, args=(h_prev, weight_hh, bias_hh), from_node=node
131+
)
132+
gates = create_node(graph, self._add, args=(gates_x, gates_h), from_node=node)
133+
gate_dim = -1
134+
135+
i_pre = create_node(
136+
graph,
137+
self._slice,
138+
args=(gates, gate_dim, 0, hidden_size),
139+
from_node=node,
140+
)
141+
f_pre = create_node(
142+
graph,
143+
self._slice,
144+
args=(gates, gate_dim, hidden_size, 2 * hidden_size),
145+
from_node=node,
146+
)
147+
g_pre = create_node(
148+
graph,
149+
self._slice,
150+
args=(gates, gate_dim, 2 * hidden_size, 3 * hidden_size),
151+
from_node=node,
152+
)
153+
o_pre = create_node(
154+
graph,
155+
self._slice,
156+
args=(gates, gate_dim, 3 * hidden_size, 4 * hidden_size),
157+
from_node=node,
158+
)
159+
160+
i_t = create_node(graph, self._sigmoid, args=(i_pre,), from_node=node)
161+
f_t = create_node(graph, self._sigmoid, args=(f_pre,), from_node=node)
162+
g_t = create_node(graph, self._tanh, args=(g_pre,), from_node=node)
163+
o_t = create_node(graph, self._sigmoid, args=(o_pre,), from_node=node)
164+
165+
f_c = create_node(graph, self._mul, args=(f_t, c_prev), from_node=node)
166+
i_g = create_node(graph, self._mul, args=(i_t, g_t), from_node=node)
167+
c_t = create_node(graph, self._add, args=(f_c, i_g), from_node=node)
168+
tanh_c = create_node(graph, self._tanh, args=(c_t,), from_node=node)
169+
h_t = create_node(graph, self._mul, args=(o_t, tanh_c), from_node=node)
170+
return h_t, c_t
171+
172+
def _decompose_cell(
173+
self,
174+
graph: torch.fx.Graph,
175+
node: torch.fx.Node,
176+
) -> Tuple[torch.fx.Node, torch.fx.Node]:
177+
args = node.args
178+
input_node = cast(torch.fx.Node, args[0])
179+
h_prev, c_prev = cast(Tuple[torch.fx.Node, torch.fx.Node], args[1])
180+
weight_ih = cast(torch.fx.Node, args[2])
181+
weight_hh = cast(torch.fx.Node, args[3])
182+
bias_ih = cast(Optional[torch.fx.Node], args[4]) if len(args) > 4 else None
183+
bias_hh = cast(Optional[torch.fx.Node], args[5]) if len(args) > 5 else None
184+
185+
hidden_size = h_prev.meta["val"].shape[-1]
186+
187+
return self._build_cell_step(
188+
graph,
189+
node,
190+
input_node,
191+
h_prev,
192+
c_prev,
193+
weight_ih,
194+
weight_hh,
195+
bias_ih,
196+
bias_hh,
197+
hidden_size,
198+
)
199+
137200
def call(self, graph_module: torch.fx.GraphModule): # noqa: C901
138201
graph = graph_module.graph
139202
modified = False
140203

141204
for node in list(graph.nodes):
142205
if (
143206
node.op != "call_function"
144-
or node.target != self._TARGET
207+
or node.target not in (self._TARGET, self._TARGET_CELL)
145208
or not self.allowed_to_transform(node.meta)
146209
):
147210
continue
211+
212+
if node.target == self._TARGET_CELL:
213+
users = list(node.users)
214+
if any(
215+
user.target != operator.getitem
216+
or user.args[1] not in (-2, -1, 0, 1)
217+
for user in users
218+
):
219+
continue
220+
221+
with graph.inserting_before(node):
222+
h_t, c_t = self._decompose_cell(graph, node)
223+
224+
getitem_nodes = []
225+
for user in users:
226+
idx = cast(int, user.args[1])
227+
user.replace_all_uses_with(h_t if idx in (-2, 0) else c_t)
228+
getitem_nodes.append(user)
229+
230+
for gi in getitem_nodes:
231+
graph.erase_node(gi)
232+
graph.erase_node(node)
233+
modified = True
234+
continue
235+
148236
getitem_users = get_getitem_users(node, 3)
149237

150238
args = node.args

0 commit comments

Comments
 (0)