Skip to content

Commit 0dec2b6

Browse files
rascaniclaude
andauthored
Cortex-M: quantize aten.matmul by rewriting it to bmm before annotation (#21028)
### Summary The cortex_m quantizer annotates only aten.bmm.default, but torch.matmul / the @ operator is captured as aten.matmul.default and does not decompose to bmm until to_edge -- after quantization runs -- so matmuls (for example every attention score and context product) never received qparams and stayed in fp32 on portable kernels. Add a pre-annotation MatmulToBmmPass that rewrites aten.matmul to aten.bmm (rank-3 directly; higher ranks by folding the leading batch dims to 3D and reshaping back; rank-2 and broadcasting matmuls left unchanged), so the existing CortexMBmmCheck annotation and quantized_batch_matmul lowering handle it. No new kernel is needed -- the batch-matmul kernel already exists; this is purely an annotation-timing fix. ### Test plan Verified test-first (RED->GREEN) with rank-3 and rank-4 dialect tests and no regression on the existing bmm suite; confirmed it flips all 15 SAM mask-decoder attention matmuls from fp32 to quantizable. Authored with Claude Code. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c995192 commit 0dec2b6

3 files changed

Lines changed: 151 additions & 0 deletions

File tree

backends/cortex_m/passes/cortex_m_pass_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from .clamp_hardswish_pass import ClampHardswishPass
2828
from .decompose_hardswish_pass import DecomposeHardswishPass
2929
from .decompose_mean_pass import DecomposeMeanPass
30+
from .matmul_to_bmm_pass import MatmulToBmmPass
3031
from .quantized_clamp_activation_pass import QuantizedClampActivationPass
3132
from .replace_quant_nodes_pass import ReplaceQuantNodesPass
3233

@@ -51,6 +52,7 @@ class CortexMPassManager(PassManager):
5152
ReplaceScalarWithTensorArgPass,
5253
ClampHardswishPass,
5354
DecomposeMeanPass,
55+
MatmulToBmmPass,
5456
DeduplicateGetAttrPass,
5557
]
5658

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
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+
from typing import cast, Dict
8+
9+
import torch
10+
from executorch.exir.pass_base import ExportPass, NodeMetadata, ProxyValue
11+
12+
from torch._ops import OpOverload
13+
from torch.fx.node import Argument
14+
15+
16+
class MatmulToBmmPass(ExportPass):
17+
"""
18+
Rewrites ``aten.matmul.default`` into ``aten.bmm.default`` so the cortex_m
19+
annotator and ``quantized_batch_matmul`` lowering pick it up.
20+
21+
``a @ b`` / ``torch.matmul`` is captured as ``aten.matmul.default`` at
22+
annotation time and only decomposes to ``bmm`` at ``to_edge`` -- after
23+
quantization -- so it would otherwise never receive qparams. This pass runs
24+
before annotation and normalizes matmul to bmm:
25+
26+
- rank-3 @ rank-3 with matching batch dims: replaced directly with
27+
``aten.bmm.default``.
28+
- rank>3 (e.g. attention [B,H,N,d]@[B,H,d,N]) with matching leading batch
29+
dims: the leading batch dims are folded into a single batch dim (reshape to
30+
3D), ``aten.bmm.default`` is applied, and the result is reshaped back to the
31+
original leading dims. This is required because the cortex_m bmm checker
32+
rejects non-rank-3.
33+
- everything else is left unchanged, including rank-2 (mm / linear territory)
34+
and broadcasting matmuls whose batch dims differ (``aten.bmm`` requires
35+
equal batch dims, so rewriting those would crash).
36+
"""
37+
38+
def call_operator(
39+
self,
40+
op: OpOverload,
41+
args: tuple[Argument, ...],
42+
kwargs: Dict[str, Argument],
43+
meta: NodeMetadata,
44+
) -> ProxyValue:
45+
if op != torch.ops.aten.matmul.default:
46+
return super().call_operator(op, args, kwargs, meta)
47+
48+
lhs = cast(ProxyValue, args[0])
49+
rhs = cast(ProxyValue, args[1])
50+
lhs_shape = lhs.to_tensor().shape
51+
rhs_shape = rhs.to_tensor().shape
52+
lhs_rank = len(lhs_shape)
53+
rhs_rank = len(rhs_shape)
54+
55+
if lhs_rank == 3 and rhs_rank == 3 and lhs_shape[:-2] == rhs_shape[:-2]:
56+
return super().call_operator(
57+
torch.ops.aten.bmm.default, (lhs, rhs), {}, meta
58+
)
59+
60+
if lhs_rank == rhs_rank and lhs_rank > 3 and lhs_shape[:-2] == rhs_shape[:-2]:
61+
batch_dims = list(lhs_shape[:-2])
62+
m, k = lhs_shape[-2], lhs_shape[-1]
63+
n = rhs_shape[-1]
64+
65+
lhs_3d = super().call_operator(
66+
torch.ops.aten.reshape.default, (lhs, [-1, m, k]), {}, meta
67+
)
68+
rhs_3d = super().call_operator(
69+
torch.ops.aten.reshape.default, (rhs, [-1, k, n]), {}, meta
70+
)
71+
bmm_out = super().call_operator(
72+
torch.ops.aten.bmm.default, (lhs_3d, rhs_3d), {}, meta
73+
)
74+
return super().call_operator(
75+
torch.ops.aten.reshape.default,
76+
(bmm_out, batch_dims + [m, n]),
77+
{},
78+
meta,
79+
)
80+
81+
return super().call_operator(op, args, kwargs, meta)

backends/cortex_m/test/ops/test_batch_matmul.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,46 @@ def forward(self, lhs):
4848
return torch.bmm(lhs, self.rhs)
4949

5050

51+
class CortexMMatmul(torch.nn.Module):
52+
"""``torch.matmul`` is captured as ``aten.matmul.default`` at annotation time
53+
and only decomposes to ``bmm`` at ``to_edge`` -- after quantization -- so it
54+
would never receive qparams. The pre-annotation matmul->bmm rewrite makes it
55+
lower to ``cortex_m.quantized_batch_matmul`` for both rank-3 @ rank-3 and
56+
rank>3 (leading batch dims folded to 3D and reshaped back)."""
57+
58+
ops_before_transforms = {
59+
"executorch_exir_dialects_edge__ops_aten_bmm_default": 1,
60+
}
61+
62+
ops_after_transforms = {
63+
"executorch_exir_dialects_edge__ops_cortex_m_quantized_batch_matmul_default": 1,
64+
"executorch_exir_dialects_edge__ops_aten_bmm_default": 0,
65+
"executorch_exir_dialects_edge__ops_aten_matmul_default": 0,
66+
}
67+
68+
def forward(self, lhs, rhs):
69+
return torch.matmul(lhs, rhs)
70+
71+
72+
class CortexMMatmulBroadcast(torch.nn.Module):
73+
"""Broadcasting rank-3 matmul whose batch dims differ ([1,4,8] @ [2,8,4] ->
74+
[2,4,4]). ``aten.bmm`` requires equal batch dims, so this must NOT be
75+
rewritten to bmm; it falls through, stays ``aten.matmul`` -> fp32, and is not
76+
lowered (no cortex_m_quantized_batch_matmul)."""
77+
78+
ops_before_transforms = {
79+
"executorch_exir_dialects_edge__ops_aten_bmm_default": 1,
80+
}
81+
82+
ops_after_transforms = {
83+
"executorch_exir_dialects_edge__ops_cortex_m_quantized_batch_matmul_default": 0,
84+
"executorch_exir_dialects_edge__ops_aten_bmm_default": 1,
85+
}
86+
87+
def forward(self, lhs, rhs):
88+
return torch.matmul(lhs, rhs)
89+
90+
5191
test_cases = {
5292
"bmm_small": McuTestCase(
5393
CortexMBmm(),
@@ -84,6 +124,22 @@ def forward(self, lhs):
84124
}
85125

86126

127+
matmul_test_cases = {
128+
"matmul_rank3": McuTestCase(
129+
CortexMMatmul(),
130+
(torch.randn(2, 4, 8), torch.randn(2, 8, 4)),
131+
),
132+
"matmul_rank4": McuTestCase(
133+
CortexMMatmul(),
134+
(torch.randn(1, 4, 16, 8), torch.randn(1, 4, 8, 16)),
135+
),
136+
"matmul_broadcast_rank3": McuTestCase(
137+
CortexMMatmulBroadcast(),
138+
(torch.randn(1, 4, 8), torch.randn(2, 8, 4)),
139+
),
140+
}
141+
142+
87143
@parametrize("test_case", test_cases)
88144
def test_dialect_batch_matmul(test_case, cortex_m_target):
89145
tester = CortexMTester(
@@ -108,6 +164,18 @@ def test_dialect_batch_matmul_const_rhs(test_case, cortex_m_target):
108164
)
109165

110166

167+
@parametrize("test_case", matmul_test_cases)
168+
def test_dialect_matmul(test_case, cortex_m_target):
169+
tester = CortexMTester(
170+
test_case.model, test_case.example_inputs, target_config=cortex_m_target
171+
)
172+
tester.test_dialect(
173+
test_case.model.ops_before_transforms,
174+
test_case.model.ops_after_transforms,
175+
qtol=1,
176+
)
177+
178+
111179
@parametrize("test_case", test_cases)
112180
def test_implementation_batch_matmul(test_case, cortex_m_target):
113181
tester = CortexMTester(

0 commit comments

Comments
 (0)