Skip to content

Commit 0603d37

Browse files
committed
Arm backend: Lower MXFP Linear to TOSA
Signed-off-by: Martin Lindström <Martin.Lindstroem@arm.com> Co-authored-by: Sebastian Larsson <sebastian.larsson@arm.com> Change-Id: Iab2e1cf2ed21047bbc2a7a51604b9230fe2f2819
1 parent 9e7e1d0 commit 0603d37

14 files changed

Lines changed: 971 additions & 29 deletions

backends/arm/_passes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
from .rewrite_le_lt_to_ge_gt_pass import RewriteLeLtToGeGtPass # noqa
166166
from .rewrite_matmul import RewriteMatmulPass # noqa
167167
from .rewrite_max_pool2d_pass import RewriteMaxPool2dPass # noqa
168+
from .rewrite_mxfp_linear import RewriteMXFPLinearPass # noqa
168169
from .rewrite_pad import RewritePadPass # noqa
169170
from .rewrite_slice import RewriteSlicePass # noqa
170171
from .rewrite_upsample import RewriteUpsamplePass # noqa

backends/arm/_passes/arm_pass_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
RewriteLeLtToGeGtPass,
142142
RewriteMatmulPass,
143143
RewriteMaxPool2dPass,
144+
RewriteMXFPLinearPass,
144145
RewritePadPass,
145146
RewriteSlicePass,
146147
RewriteUpsamplePass,
@@ -524,6 +525,7 @@ def _tosa_pipeline(
524525
RewriteUpsamplePass(),
525526
RewriteMaxPool2dPass(),
526527
RewriteConvPass(exported_program),
528+
RewriteMXFPLinearPass(exported_program),
527529
RewriteMatmulPass(),
528530
RewritePadPass(),
529531
FuseViewCopyTransformPass(),
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
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 functools import reduce
8+
from typing import Any, cast, Sequence, Set, Type
9+
10+
import torch
11+
from executorch.backends.arm._passes import ArmPass
12+
from executorch.backends.arm._passes.arm_pass_utils import (
13+
create_node,
14+
get_first_fake_tensor,
15+
)
16+
from executorch.exir.dialects._ops import ops as exir_ops
17+
from executorch.exir.pass_base import ExportPass, PassResult
18+
19+
20+
class RewriteMXFPLinearPass(ArmPass):
21+
"""Rewrite ``tosa_mxfp.linear`` into explicit TOSA MXFP operators.
22+
23+
For each MXFP linear custom op, the pass:
24+
1. Reshapes activations and precomputed weight tensors to the rank expected
25+
by the block-scaled TOSA ops.
26+
2. Inserts ``tosa.CAST_TO_BLOCK_SCALED`` for the activation input.
27+
3. Inserts ``tosa.MATMUL_T_BLOCK_SCALED`` using the cast activations and the
28+
MXFP weight data/scale tensors.
29+
4. Restores the original output shape.
30+
5. Re-applies bias, reshaping it first to match the output rank when
31+
needed.
32+
33+
"""
34+
35+
_passes_required_after: Set[Type[ExportPass]] = set()
36+
37+
def __init__(self, exported_program: torch.export.ExportedProgram, *args, **kwargs):
38+
super().__init__(*args, **kwargs)
39+
self.exported_program = exported_program
40+
41+
def _get_linear_args(
42+
self, node: torch.fx.Node
43+
) -> tuple[torch.fx.Node, torch.fx.Node, torch.fx.Node, torch.fx.Node | None, int]:
44+
"""Extract the MXFP linear operands from a custom-op node."""
45+
input_node = cast(torch.fx.Node, node.args[0])
46+
weight_qdata_node = cast(torch.fx.Node, node.args[1])
47+
weight_scale_node = cast(torch.fx.Node, node.args[2])
48+
bias_node = cast(
49+
torch.fx.Node | None,
50+
node.args[3] if len(node.args) > 3 else node.kwargs.get("bias"),
51+
)
52+
block_size = cast(
53+
int,
54+
node.args[4] if len(node.args) > 4 else node.kwargs.get("block_size", 32),
55+
)
56+
return input_node, weight_qdata_node, weight_scale_node, bias_node, block_size
57+
58+
def _reshape_with_view(
59+
self,
60+
graph_module: torch.fx.GraphModule,
61+
input_node: torch.fx.Node,
62+
shape: Sequence[int | torch.SymInt],
63+
from_node: torch.fx.Node,
64+
) -> torch.fx.Node:
65+
"""Insert a ``view_copy`` node and update its fake-tensor metadata."""
66+
reshaped = create_node(
67+
graph=graph_module.graph,
68+
op_target=exir_ops.edge.aten.view_copy.default,
69+
args=(input_node, shape),
70+
kwargs={},
71+
from_node=from_node,
72+
)
73+
reshaped.meta["val"] = exir_ops.edge.aten.view_copy.default(
74+
get_first_fake_tensor(input_node),
75+
shape,
76+
)
77+
return reshaped
78+
79+
def _create_block_scaled_inputs(
80+
self,
81+
graph_module: torch.fx.GraphModule,
82+
mxfp_linear_node: torch.fx.Node,
83+
input_node: torch.fx.Node,
84+
weight_qdata_node: torch.fx.Node,
85+
weight_scale_node: torch.fx.Node,
86+
block_size: int,
87+
) -> tuple[torch.fx.Node, torch.fx.Node]:
88+
"""Create rank-3 inputs for the block-scaled cast and matmul ops."""
89+
graph = graph_module.graph
90+
input_fake = get_first_fake_tensor(input_node)
91+
weight_qdata_fake = get_first_fake_tensor(weight_qdata_node)
92+
weight_scale_fake = get_first_fake_tensor(weight_scale_node)
93+
94+
batches = reduce(operator.mul, input_fake.shape[:-1], 1)
95+
input_reshape_shape = [1, batches, input_fake.shape[-1]]
96+
97+
input_reshaped = self._reshape_with_view(
98+
graph_module,
99+
input_node,
100+
input_reshape_shape,
101+
mxfp_linear_node,
102+
)
103+
if weight_qdata_fake.ndim != 3 or weight_scale_fake.ndim != 3:
104+
raise RuntimeError(
105+
"Expected pre-reshaped rank-3 MXFP weight placeholders in rewrite pass"
106+
)
107+
108+
cast_node = create_node(
109+
graph=graph,
110+
op_target=exir_ops.backend.tosa.CAST_TO_BLOCK_SCALED.default,
111+
args=(input_reshaped, block_size),
112+
kwargs={"output_dtype": weight_qdata_fake.dtype},
113+
from_node=mxfp_linear_node,
114+
)
115+
cast_node.meta["val"] = exir_ops.backend.tosa.CAST_TO_BLOCK_SCALED.default(
116+
get_first_fake_tensor(input_reshaped),
117+
block_size,
118+
output_dtype=weight_qdata_fake.dtype,
119+
)
120+
121+
input_qdata_node = create_node(
122+
graph=graph,
123+
op_target=cast(Any, operator.getitem),
124+
args=(cast_node, 0),
125+
kwargs={},
126+
from_node=mxfp_linear_node,
127+
)
128+
input_qdata_node.meta["val"] = cast_node.meta["val"][0]
129+
130+
input_scale_node = create_node(
131+
graph=graph,
132+
op_target=cast(Any, operator.getitem),
133+
args=(cast_node, 1),
134+
kwargs={},
135+
from_node=mxfp_linear_node,
136+
)
137+
input_scale_node.meta["val"] = cast_node.meta["val"][1]
138+
139+
return (
140+
input_qdata_node,
141+
input_scale_node,
142+
)
143+
144+
def _create_matmul_node(
145+
self,
146+
graph_module: torch.fx.GraphModule,
147+
mxfp_linear_node: torch.fx.Node,
148+
input_qdata_node: torch.fx.Node,
149+
input_scale_node: torch.fx.Node,
150+
weight_qdata_node: torch.fx.Node,
151+
weight_scale_node: torch.fx.Node,
152+
block_size: int,
153+
) -> torch.fx.Node:
154+
"""Insert ``MATMUL_T_BLOCK_SCALED`` with updated fake metadata."""
155+
matmul_node = create_node(
156+
graph=graph_module.graph,
157+
op_target=exir_ops.backend.tosa.MATMUL_T_BLOCK_SCALED.default,
158+
args=(
159+
input_qdata_node,
160+
input_scale_node,
161+
weight_qdata_node,
162+
weight_scale_node,
163+
block_size,
164+
),
165+
kwargs={},
166+
from_node=mxfp_linear_node,
167+
)
168+
matmul_node.meta["val"] = exir_ops.backend.tosa.MATMUL_T_BLOCK_SCALED.default(
169+
get_first_fake_tensor(input_qdata_node),
170+
get_first_fake_tensor(input_scale_node),
171+
get_first_fake_tensor(weight_qdata_node),
172+
get_first_fake_tensor(weight_scale_node),
173+
block_size,
174+
)
175+
return matmul_node
176+
177+
def _create_output_view(
178+
self,
179+
graph_module: torch.fx.GraphModule,
180+
mxfp_linear_node: torch.fx.Node,
181+
matmul_node: torch.fx.Node,
182+
) -> torch.fx.Node:
183+
"""Restore the original linear output shape after block matmul."""
184+
output_fake = get_first_fake_tensor(mxfp_linear_node)
185+
output_node = create_node(
186+
graph=graph_module.graph,
187+
op_target=exir_ops.edge.aten.view_copy.default,
188+
args=(matmul_node, list(output_fake.shape)),
189+
kwargs={},
190+
from_node=mxfp_linear_node,
191+
)
192+
output_node.meta["val"] = exir_ops.edge.aten.view_copy.default(
193+
get_first_fake_tensor(matmul_node),
194+
list(output_fake.shape),
195+
)
196+
return output_node
197+
198+
def _create_bias_add(
199+
self,
200+
graph_module: torch.fx.GraphModule,
201+
mxfp_linear_node: torch.fx.Node,
202+
output_node: torch.fx.Node,
203+
bias_node: torch.fx.Node,
204+
) -> torch.fx.Node:
205+
"""Reshape bias to match output rank and append the final add node."""
206+
output_fake = get_first_fake_tensor(mxfp_linear_node)
207+
bias_fake = get_first_fake_tensor(bias_node)
208+
bias_shape = [1] * (output_fake.dim() - 1) + [output_fake.shape[-1]]
209+
bias_arg = bias_node
210+
211+
if tuple(bias_fake.shape) != tuple(bias_shape):
212+
# Match ranks by prepending singleton dimensions.
213+
with graph_module.graph.inserting_after(output_node):
214+
bias_arg = self._reshape_with_view(
215+
graph_module,
216+
bias_node,
217+
bias_shape,
218+
mxfp_linear_node,
219+
)
220+
with graph_module.graph.inserting_after(bias_arg):
221+
add_node = create_node(
222+
graph=graph_module.graph,
223+
op_target=exir_ops.edge.aten.add.Tensor,
224+
args=(output_node, bias_arg),
225+
kwargs={},
226+
from_node=mxfp_linear_node,
227+
)
228+
else:
229+
# Bias already has the right shape, so add it directly.
230+
with graph_module.graph.inserting_after(output_node):
231+
add_node = create_node(
232+
graph=graph_module.graph,
233+
op_target=exir_ops.edge.aten.add.Tensor,
234+
args=(output_node, bias_arg),
235+
kwargs={},
236+
from_node=mxfp_linear_node,
237+
)
238+
add_node.meta["val"] = exir_ops.edge.aten.add.Tensor(
239+
get_first_fake_tensor(output_node),
240+
get_first_fake_tensor(bias_arg),
241+
)
242+
243+
return add_node
244+
245+
def _rewrite_mxfp_linear_node(
246+
self,
247+
graph_module: torch.fx.GraphModule,
248+
mxfp_linear_node: torch.fx.Node,
249+
) -> torch.fx.Node:
250+
"""Rewrite one MXFP linear node to explicit TOSA MXFP ops."""
251+
graph = graph_module.graph
252+
(
253+
input_node,
254+
weight_qdata_node,
255+
weight_scale_node,
256+
bias_node,
257+
block_size,
258+
) = self._get_linear_args(mxfp_linear_node)
259+
260+
with graph.inserting_before(mxfp_linear_node):
261+
(
262+
input_qdata_node,
263+
input_scale_node,
264+
) = self._create_block_scaled_inputs(
265+
graph_module,
266+
mxfp_linear_node,
267+
input_node,
268+
weight_qdata_node,
269+
weight_scale_node,
270+
block_size,
271+
)
272+
matmul_node = self._create_matmul_node(
273+
graph_module,
274+
mxfp_linear_node,
275+
input_qdata_node,
276+
input_scale_node,
277+
weight_qdata_node,
278+
weight_scale_node,
279+
block_size,
280+
)
281+
282+
with graph.inserting_after(matmul_node):
283+
output_node = self._create_output_view(
284+
graph_module, mxfp_linear_node, matmul_node
285+
)
286+
287+
if bias_node is None:
288+
return output_node
289+
290+
return self._create_bias_add(
291+
graph_module,
292+
mxfp_linear_node,
293+
output_node,
294+
bias_node,
295+
)
296+
297+
def call(self, graph_module: torch.fx.GraphModule):
298+
modified = False
299+
graph = graph_module.graph
300+
301+
for node in list(graph.nodes):
302+
if node.op != "call_function" or node.target not in (
303+
torch.ops.tosa_mxfp.linear.default,
304+
exir_ops.edge.tosa_mxfp.linear.default,
305+
):
306+
continue
307+
308+
modified = True
309+
replacement = self._rewrite_mxfp_linear_node(graph_module, node)
310+
node.replace_all_uses_with(replacement)
311+
graph.erase_node(node)
312+
313+
if modified:
314+
graph.eliminate_dead_code()
315+
graph_module.recompile()
316+
graph_module = super().call(graph_module).graph_module
317+
318+
return PassResult(graph_module, modified)

backends/arm/operator_support/tosa_supported_operators.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,17 @@ def get_registered_tosa_support_checks(
236236
return checks
237237

238238

239+
class MXOpsSupportList(OperatorSupportBase):
240+
"""Accept Arm MX custom ops when the active spec enables MX support."""
241+
242+
targets = (exir_ops.edge.tosa_mxfp.linear.default,)
243+
244+
def is_node_supported(
245+
self, submodules: typing.Mapping[str, torch.nn.Module], node: fx.Node
246+
) -> bool:
247+
return node.op == "call_function" and node.target in self.targets
248+
249+
239250
def tosa_support_factory(
240251
tosa_spec: TosaSpecification,
241252
exported_program: ExportedProgram,
@@ -270,6 +281,8 @@ def tosa_support_factory(
270281
positive_checks.append(TOSAProINTSupportList())
271282
elif tosa_spec.support_float():
272283
positive_checks.append(TOSAProFPSupportList())
284+
if tosa_spec.support_extension("mxfp"):
285+
positive_checks.append(MXOpsSupportList())
273286
# TODO: Refactor to use TOSAProSupportLists + negtive checks
274287
positive_checks += [
275288
check(tosa_spec, reporter)
@@ -749,6 +762,9 @@ def is_node_supported(
749762
):
750763
return True
751764

765+
if node.target in MXOpsSupportList.targets:
766+
return True
767+
752768
floating_dtypes = set()
753769
for input_node in (
754770
input_node

backends/arm/operators/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
op_tosa_gather,
5656
op_tosa_identity,
5757
op_tosa_matmul,
58+
op_tosa_matmul_t_block_scaled,
5859
op_tosa_max_pool2d,
5960
op_tosa_pad,
6061
op_tosa_rescale,

0 commit comments

Comments
 (0)