Skip to content

Commit 96e5183

Browse files
committed
[XNNPACK] Delegate GEMMs whose weight is computed from parameters
A convolution, linear, addmm or mm whose weight or bias is computed from parameters, such as a convolution under weight_norm or spectral_norm, was declined by the XNNPACK partitioner and ran on the portable kernels together with the computation of its weight. The GEMM configs now accept such a weight or bias when it is computed with pure ops from parameters, unmutated buffers or constants, the computation feeds only that GEMM, and the folded tensor is no larger than the parameters it replaces. The computation is partitioned with the GEMM, and FoldComputedParamsPass folds it into a constant at the start of XNNPACK preprocess, inside the delegate, so the top-level program is not modified. AddmmConfig now also rejects addmm with non-default alpha or beta, which the XNNPACK lowering ignored. Fixes #22078 This change was authored with Claude Code.
1 parent 4628b2b commit 96e5183

5 files changed

Lines changed: 388 additions & 4 deletions

File tree

‎backends/xnnpack/_passes/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
)
2727
from executorch.backends.xnnpack._passes.decompose_batch_norm import DecomposeBatchNorm
2828
from executorch.backends.xnnpack._passes.decompose_cat import DecomposeConcatenate
29+
from executorch.backends.xnnpack._passes.fold_computed_params import (
30+
FoldComputedParamsPass,
31+
)
2932
from executorch.backends.xnnpack._passes.fuse_activation_pass import FuseActivationPass
3033
from executorch.backends.xnnpack._passes.fuse_batch_norm import FuseBatchNormPass
3134
from executorch.backends.xnnpack._passes.fuse_pad_into_conv_pass import (
@@ -74,6 +77,7 @@ def __init__(
7477
if not passes:
7578
# All the XNNPACK passes
7679
self.passes = [
80+
FoldComputedParamsPass,
7781
RewriteFp16SiluPass,
7882
XNNPACKRemoveCloneOpsTransform,
7983
# TODO - remove this pass once we have a better support for dim_order ops lowering
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
import torch
8+
from executorch.backends.transforms.utils import create_constant_placeholder
9+
from executorch.backends.xnnpack._passes.xnnpack_pass import XNNPACKPass
10+
from executorch.backends.xnnpack.utils.utils import COMPUTED_PARAM_TAG, get_param_tensor
11+
from executorch.exir.pass_base import PassResult
12+
from torch.export.graph_signature import InputKind
13+
14+
15+
class FoldComputedParamsPass(XNNPACKPass):
16+
"""
17+
Folds the nodes that the partitioner tagged with COMPUTED_PARAM_TAG, which
18+
compute a GEMM weight or bias from params, into a param holding the result.
19+
"""
20+
21+
def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
22+
graph = graph_module.graph
23+
names = {node.name for node in graph.nodes}
24+
values = {}
25+
for node in graph.nodes:
26+
# Checking a GEMM tags the nodes computing its weight or bias before
27+
# the GEMM is accepted, so they are folded only next to that GEMM.
28+
if not names & node.meta.get(COMPUTED_PARAM_TAG, set()):
29+
continue
30+
args, kwargs = torch.fx.node.map_arg(
31+
(node.args, node.kwargs),
32+
lambda n: (
33+
values[n]
34+
if n in values
35+
else get_param_tensor(self.exported_program, n)
36+
),
37+
)
38+
with torch.no_grad():
39+
values[node] = node.target(*args, **kwargs)
40+
41+
if not values:
42+
return PassResult(graph_module, False)
43+
44+
taken = (
45+
names
46+
| self.exported_program.state_dict.keys()
47+
| self.exported_program.constants.keys()
48+
)
49+
for node, value in values.items():
50+
if all(user in values for user in node.users):
51+
continue
52+
name = f"{node.name}_folded"
53+
while name in taken:
54+
name += "_"
55+
taken.add(name)
56+
with graph.inserting_before(next(iter(graph.nodes))):
57+
param = create_constant_placeholder(
58+
exp_program=self.exported_program,
59+
graph=graph,
60+
kind=InputKind.PARAMETER,
61+
name=name,
62+
data=value,
63+
)
64+
node.replace_all_uses_with(param)
65+
66+
for node in reversed(values):
67+
graph.erase_node(node)
68+
69+
graph_module.recompile()
70+
return PassResult(graph_module, True)

‎backends/xnnpack/partition/config/gemm_configs.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
)
3030
from executorch.backends.xnnpack.utils.utils import (
3131
get_input_node,
32+
get_static_param_deps,
3233
is_depthwise_conv,
3334
is_getitem,
3435
is_node,
@@ -153,10 +154,11 @@ def _get_weight_deps(
153154
if precision == ConfigPrecisionType.FP32:
154155
# First find the weight
155156
weight_node = get_input_node(node, self.weight_idx)
156-
if not is_param_node(ep, weight_node):
157+
weight_deps = get_static_param_deps(ep, weight_node, node)
158+
if weight_deps is None:
157159
why(node, "Expected weight to be a static param")
158160
return (False, [])
159-
gemm_deps.append(weight_node)
161+
gemm_deps.extend(weight_deps)
160162

161163
return (True, gemm_deps)
162164
else:
@@ -248,10 +250,11 @@ def _get_bias_deps(
248250
if len(node.all_input_nodes) > 2 and self.bias_idx is not None:
249251
bias_node = get_input_node(node, self.bias_idx)
250252
if bias_node:
251-
if not is_param_node(ep, bias_node):
253+
bias_deps = get_static_param_deps(ep, bias_node, node)
254+
if bias_deps is None:
252255
why(node, "Expected bias to be a static param")
253256
return (False, [])
254-
gemm_deps.append(bias_node)
257+
gemm_deps.extend(bias_deps)
255258

256259
return (True, gemm_deps)
257260

@@ -479,6 +482,12 @@ def __init__(self, **kwargs):
479482
self.src_partitions = None
480483
self.linear_modules = [torch.nn.functional.linear, torch.nn.Linear]
481484

485+
def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
486+
if node.kwargs.get("alpha", 1) != 1 or node.kwargs.get("beta", 1) != 1:
487+
why(node, "XNNPACK does not support addmm with alpha or beta")
488+
return False
489+
return super().check_constraints(node, ep)
490+
482491
def _get_weight_deps(
483492
self, node: torch.fx.Node, ep: ExportedProgram, precision: ConfigPrecisionType
484493
) -> Tuple[bool, List[torch.fx.Node]]:
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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+
import unittest
8+
9+
import torch
10+
import torch.nn.functional as F
11+
from executorch.backends.xnnpack.partition.config.generic_node_configs import MulConfig
12+
from executorch.backends.xnnpack.partition.xnnpack_partitioner import XnnpackPartitioner
13+
from executorch.backends.xnnpack.test.tester import Tester, ToEdgeTransformAndLower
14+
from torch.nn.utils.parametrizations import weight_norm
15+
16+
17+
class TestFoldComputedParams(unittest.TestCase):
18+
def setUp(self):
19+
torch._dynamo.reset()
20+
21+
class WeightNormConv1d(torch.nn.Module):
22+
def __init__(self):
23+
super().__init__()
24+
self.conv = weight_norm(
25+
torch.nn.Conv1d(8, 8, 5, padding=2, groups=2), dim=2
26+
)
27+
28+
def forward(self, x):
29+
return self.conv(x)
30+
31+
class ComputedLinear(torch.nn.Module):
32+
def __init__(self):
33+
super().__init__()
34+
self.weight = torch.nn.Parameter(torch.randn(4, 8))
35+
self.bias = torch.nn.Parameter(torch.randn(4))
36+
37+
def forward(self, x):
38+
return F.linear(x, self.weight * 2, self.bias + 1)
39+
40+
class FoldedNameCollision(torch.nn.Module):
41+
def __init__(self):
42+
super().__init__()
43+
self.weight = torch.nn.Parameter(torch.randn(4, 8))
44+
# The name the folded weight would get by default.
45+
self.aten_mul_tensor_folded = torch.nn.Parameter(torch.randn(4))
46+
47+
def forward(self, x):
48+
return F.linear(x, self.weight * 2, self.aten_mul_tensor_folded)
49+
50+
class MatmulWeightConv1d(torch.nn.Module):
51+
def __init__(self):
52+
super().__init__()
53+
self.weight = torch.nn.Parameter(torch.randn(3, 3))
54+
55+
def forward(self, x):
56+
return F.conv1d(x, (self.weight @ self.weight).reshape(3, 3, 1))
57+
58+
class NestedGemmWeightConv1d(torch.nn.Module):
59+
def __init__(self):
60+
super().__init__()
61+
self.a = torch.nn.Parameter(torch.randn(3, 3))
62+
self.b = torch.nn.Parameter(torch.randn(3, 3))
63+
64+
def forward(self, x):
65+
return F.conv1d(x, (self.a @ self.b.flip([1])).reshape(3, 3, 1))
66+
67+
class ScaledConv1d(torch.nn.Module):
68+
def __init__(self):
69+
super().__init__()
70+
self.weight = torch.nn.Parameter(torch.randn(4, 4, 3))
71+
72+
def forward(self, x):
73+
return F.conv1d(x, self.weight * 2)
74+
75+
class ScaledAddmm(torch.nn.Module):
76+
def __init__(self, computed):
77+
super().__init__()
78+
self.computed = computed
79+
self.weight = torch.nn.Parameter(torch.randn(8, 4))
80+
self.bias = torch.nn.Parameter(torch.randn(4))
81+
82+
def forward(self, x):
83+
weight, bias = self.weight, self.bias
84+
if self.computed:
85+
weight, bias = weight * 2, bias + 1
86+
return torch.addmm(bias, x, weight, alpha=2, beta=3)
87+
88+
class SourceUsedElsewhere(torch.nn.Module):
89+
def __init__(self):
90+
super().__init__()
91+
self.weight = torch.nn.Parameter(torch.randn(4, 8))
92+
93+
def forward(self, x):
94+
return F.linear(x, self.weight * 2), self.weight.sum()
95+
96+
class SourceRetainedByGemm(torch.nn.Module):
97+
def __init__(self):
98+
super().__init__()
99+
self.weight = torch.nn.Parameter(torch.randn(16))
100+
101+
def forward(self, x):
102+
return F.linear(x, (self.weight * 2).unsqueeze(1), self.weight)
103+
104+
class WeightComputationFeedsActivation(torch.nn.Module):
105+
def __init__(self):
106+
super().__init__()
107+
self.weight = torch.nn.Parameter(torch.randn(16, 16))
108+
109+
def forward(self, x):
110+
w = self.weight * 2
111+
return w @ (w * 2) + x
112+
113+
class LowRankWeight(torch.nn.Module):
114+
def __init__(self):
115+
super().__init__()
116+
self.a = torch.nn.Parameter(torch.randn(16, 2))
117+
self.b = torch.nn.Parameter(torch.randn(2, 16))
118+
119+
def forward(self, x):
120+
return F.linear(x, self.a @ self.b)
121+
122+
class MutatedBufferSource(torch.nn.Module):
123+
def __init__(self):
124+
super().__init__()
125+
self.register_buffer("weight", torch.randn(4, 4, 3))
126+
127+
def forward(self, x):
128+
y = F.conv1d(x, self.weight * 2)
129+
self.weight.copy_(x.mean() * torch.ones(4, 4, 3))
130+
return y
131+
132+
def _test_fully_delegated(self, module, inputs):
133+
(
134+
Tester(module.eval(), inputs)
135+
.export()
136+
.to_edge_transform_and_lower()
137+
.check_count({"torch.ops.higher_order.executorch_call_delegate": 1})
138+
.check_not(["executorch_exir_dialects_edge__ops_"])
139+
.to_executorch()
140+
.serialize()
141+
.run_method_and_compare_outputs()
142+
)
143+
144+
def test_fp32_weight_norm_conv1d(self):
145+
self._test_fully_delegated(self.WeightNormConv1d(), (torch.randn(1, 8, 16),))
146+
147+
def test_fp32_computed_linear_weight_and_bias(self):
148+
self._test_fully_delegated(self.ComputedLinear(), (torch.randn(2, 8),))
149+
150+
def test_fp32_folded_name_does_not_replace_param(self):
151+
self._test_fully_delegated(self.FoldedNameCollision(), (torch.randn(2, 8),))
152+
153+
def test_fp32_computed_weight_of_rejected_gemm_not_folded(self):
154+
# Conv1d with two dynamic dims is rejected after its weight was checked,
155+
# while the matmul computing the weight is still delegated on its own.
156+
dims = {
157+
0: torch.export.Dim("batch", min=1, max=4),
158+
2: torch.export.Dim("length", min=4, max=16),
159+
}
160+
for module in (self.MatmulWeightConv1d(), self.NestedGemmWeightConv1d()):
161+
with self.subTest(module=type(module).__name__):
162+
(
163+
Tester(
164+
module.eval(), (torch.randn(2, 3, 8),), dynamic_shapes=(dims,)
165+
)
166+
.export()
167+
.to_edge_transform_and_lower()
168+
.check(
169+
["executorch_exir_dialects_edge__ops_aten_convolution_default"]
170+
)
171+
.to_executorch()
172+
.serialize()
173+
.run_method_and_compare_outputs()
174+
)
175+
176+
def test_fp32_scaled_addmm_not_delegated(self):
177+
for computed in (False, True):
178+
with self.subTest(computed=computed):
179+
(
180+
Tester(self.ScaledAddmm(computed).eval(), (torch.randn(2, 8),))
181+
.export()
182+
.to_edge_transform_and_lower()
183+
.check(["executorch_exir_dialects_edge__ops_aten_addmm_default"])
184+
.to_executorch()
185+
.serialize()
186+
.run_method_and_compare_outputs()
187+
)
188+
189+
def test_fp32_weight_computed_by_prior_delegate_not_folded(self):
190+
(
191+
Tester(self.ScaledConv1d().eval(), (torch.randn(1, 4, 8),))
192+
.export()
193+
.to_edge_transform_and_lower(
194+
ToEdgeTransformAndLower(
195+
[XnnpackPartitioner(configs=[MulConfig]), XnnpackPartitioner()]
196+
)
197+
)
198+
.check(["executorch_exir_dialects_edge__ops_aten_convolution_default"])
199+
.to_executorch()
200+
.serialize()
201+
.run_method_and_compare_outputs()
202+
)
203+
204+
def test_fp32_computed_weight_not_folded(self):
205+
for module, inputs, gemm in (
206+
(self.SourceUsedElsewhere(), (torch.randn(2, 8),), "aten_mm_default"),
207+
(self.LowRankWeight(), (torch.randn(2, 16),), "aten_mm_default"),
208+
(self.SourceRetainedByGemm(), (torch.randn(2, 1),), "aten_addmm_default"),
209+
(
210+
self.WeightComputationFeedsActivation(),
211+
(torch.randn(16, 16),),
212+
"aten_mm_default",
213+
),
214+
(
215+
self.MutatedBufferSource(),
216+
(torch.randn(1, 4, 8),),
217+
"aten_convolution_default",
218+
),
219+
):
220+
with self.subTest(module=type(module).__name__):
221+
(
222+
Tester(module.eval(), inputs)
223+
.export()
224+
.to_edge_transform_and_lower()
225+
.check([f"executorch_exir_dialects_edge__ops_{gemm}"])
226+
)
227+
228+
def test_fp32_externally_stored_weight_not_folded(self):
229+
tester = Tester(self.WeightNormConv1d().eval(), (torch.randn(1, 8, 16),))
230+
for node in tester.export().get_artifact().graph.nodes:
231+
if node.op == "placeholder" and node.name.startswith("p_"):
232+
node.meta["custom"] = {"delegate_constant_tag": "weights.ptd"}
233+
tester.to_edge_transform_and_lower().check(
234+
["executorch_exir_dialects_edge__ops_aten_convolution_default"]
235+
)

0 commit comments

Comments
 (0)