Skip to content

Commit 1a735c5

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 ATen 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 graph is not modified. Fixes #22078 This change was authored with Claude Code.
1 parent 455aa9c commit 1a735c5

6 files changed

Lines changed: 543 additions & 7 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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 (
11+
COMPUTED_PARAM_TAG,
12+
get_param_tensor,
13+
is_param_node,
14+
)
15+
from executorch.exir.pass_base import PassResult
16+
from torch.export.graph_signature import InputKind
17+
18+
19+
class FoldComputedParamsPass(XNNPACKPass):
20+
"""
21+
Folds the nodes that the partitioner tagged with COMPUTED_PARAM_TAG, which
22+
compute a GEMM weight or bias from params, into a param holding the result.
23+
"""
24+
25+
def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
26+
graph = graph_module.graph
27+
names = {node.name for node in graph.nodes}
28+
values = {}
29+
for node in graph.nodes:
30+
# A GEMM rejected after its check, possibly by an earlier partitioner,
31+
# leaves tags behind, which delegate outputs and inputs inherit. So
32+
# fold a tagged node only next to its GEMM, and only from params.
33+
if (
34+
node.op != "call_function"
35+
or not names & node.meta.get(COMPUTED_PARAM_TAG, set())
36+
or not all(
37+
n in values or is_param_node(self.exported_program, n)
38+
for n in node.all_input_nodes
39+
)
40+
):
41+
continue
42+
args, kwargs = torch.fx.node.map_arg(
43+
(node.args, node.kwargs),
44+
lambda n: (
45+
values[n]
46+
if n in values
47+
else get_param_tensor(self.exported_program, n)
48+
),
49+
)
50+
with torch.no_grad():
51+
values[node] = node.target(*args, **kwargs)
52+
53+
if not values:
54+
return PassResult(graph_module, False)
55+
56+
taken = (
57+
names
58+
| self.exported_program.state_dict.keys()
59+
| self.exported_program.constants.keys()
60+
)
61+
for node, value in values.items():
62+
if all(user in values for user in node.users):
63+
continue
64+
name = f"{node.name}_folded"
65+
while name in taken:
66+
name += "_"
67+
taken.add(name)
68+
with graph.inserting_before(next(iter(graph.nodes))):
69+
param = create_constant_placeholder(
70+
exp_program=self.exported_program,
71+
graph=graph,
72+
kind=InputKind.PARAMETER,
73+
name=name,
74+
data=value,
75+
)
76+
node.replace_all_uses_with(param)
77+
78+
for node in reversed(values):
79+
graph.erase_node(node)
80+
81+
graph_module.recompile()
82+
return PassResult(graph_module, True)

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

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
is_quant,
2828
)
2929
from executorch.backends.xnnpack.utils.utils import (
30+
COMPUTED_PARAM_TAG,
3031
get_input_node,
3132
is_depthwise_conv,
3233
is_getitem,
@@ -38,6 +39,7 @@
3839
)
3940
from executorch.exir.backend.utils import WhyNoPartition
4041
from executorch.exir.dialects._ops import ops as exir_ops
42+
from executorch.exir.dialects.edge._ops import EdgeOpOverload
4143
from torch.export import ExportedProgram
4244
from torch.fx.experimental.symbolic_shapes import free_symbols, statically_known_true
4345
from torch.fx.passes.utils.source_matcher_utils import (
@@ -49,6 +51,77 @@
4951
why = WhyNoPartition(logger=logger)
5052

5153

54+
def get_static_param_deps( # noqa: C901
55+
ep: ExportedProgram, node: torch.fx.Node, user: torch.fx.Node
56+
) -> Optional[List[torch.fx.Node]]:
57+
"""
58+
Returns the nodes to partition with user for node to be a static param, or
59+
None. That is node itself if it is a param. Otherwise it is node and the
60+
unmutated params it is computed from with pure ATen ops, such as the weight
61+
of a conv under weight_norm. Preprocess folds node into a param that
62+
replaces them without growing the program, so only node and the params may
63+
have users outside of the computation, and only user, and node may not be
64+
larger than the params it frees. The computing nodes add user's name to
65+
their COMPUTED_PARAM_TAG set.
66+
"""
67+
if is_param_node(ep, node):
68+
return [node]
69+
70+
signature = ep.graph_signature
71+
mutated_buffers = set(signature.buffers_to_mutate.values())
72+
deps = {}
73+
pending = [node]
74+
while pending:
75+
dep = pending.pop()
76+
if dep in deps:
77+
continue
78+
deps[dep] = None
79+
if dep.op == "placeholder" and is_param_node(ep, dep):
80+
if signature.inputs_to_buffers.get(dep.name) in mutated_buffers:
81+
why(user, f"{dep} is a mutated buffer")
82+
return None
83+
# Folding would move data tagged for an external file into the PTE.
84+
if dep.meta.get("custom", {}).get("delegate_constant_tag") is not None:
85+
why(user, f"{dep} is tagged for an external data file")
86+
return None
87+
continue
88+
# Preprocess runs these ops on CPU, and a custom op may only have a fake kernel.
89+
if (
90+
dep.op != "call_function"
91+
or dep.is_impure()
92+
or is_quant(dep)
93+
or is_dequant(dep)
94+
or is_qparam(dep)
95+
or not (
96+
is_getitem(dep)
97+
or (
98+
isinstance(dep.target, (torch._ops.OpOverload, EdgeOpOverload))
99+
and dep.target.namespace in ("aten", "dim_order_ops")
100+
)
101+
)
102+
):
103+
why(user, f"{dep} is neither a param nor an op that can be folded into one")
104+
return None
105+
pending.extend(dep.all_input_nodes)
106+
107+
for dep in deps:
108+
for u in dep.users:
109+
if u not in deps and not (
110+
u is user and (dep is node or dep.op == "placeholder")
111+
):
112+
why(user, f"{dep} is also used by {u}")
113+
return None
114+
removed = [dep for dep in deps if dep.op == "placeholder" and user not in dep.users]
115+
if node.meta["val"].nbytes > sum(param.meta["val"].nbytes for param in removed):
116+
why(user, f"folding {node} would grow the program")
117+
return None
118+
119+
for dep in deps:
120+
if dep.op == "call_function":
121+
dep.meta.setdefault(COMPUTED_PARAM_TAG, set()).add(user.name)
122+
return list(deps)
123+
124+
52125
class GEMMConfig(XNNPartitionerConfig):
53126
"""
54127
GEMM-like ops like Convolution, Addmm, Linear, mostly behave in the same way, in which we
@@ -147,10 +220,10 @@ def _get_weight_deps(
147220
if precision == ConfigPrecisionType.FP32:
148221
# First find the weight
149222
weight_node = get_input_node(node, self.weight_idx)
150-
if not is_param_node(ep, weight_node):
151-
why(node, "Expected weight to be a static param")
223+
weight_deps = get_static_param_deps(ep, weight_node, node)
224+
if weight_deps is None:
152225
return (False, [])
153-
gemm_deps.append(weight_node)
226+
gemm_deps.extend(weight_deps)
154227

155228
return (True, gemm_deps)
156229
else:
@@ -245,10 +318,10 @@ def _get_bias_deps(
245318
if len(node.all_input_nodes) > 2 and self.bias_idx is not None:
246319
bias_node = get_input_node(node, self.bias_idx)
247320
if bias_node:
248-
if not is_param_node(ep, bias_node):
249-
why(node, "Expected bias to be a static param")
321+
bias_deps = get_static_param_deps(ep, bias_node, node)
322+
if bias_deps is None:
250323
return (False, [])
251-
gemm_deps.append(bias_node)
324+
gemm_deps.extend(bias_deps)
252325

253326
return (True, gemm_deps)
254327

0 commit comments

Comments
 (0)