Skip to content

Commit 3008de5

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 2c560b1 commit 3008de5

6 files changed

Lines changed: 525 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
@@ -28,6 +28,7 @@
2828
tag_as_implicit_q_dq,
2929
)
3030
from executorch.backends.xnnpack.utils.utils import (
31+
COMPUTED_PARAM_TAG,
3132
get_input_node,
3233
is_depthwise_conv,
3334
is_getitem,
@@ -39,6 +40,7 @@
3940
)
4041
from executorch.exir.backend.utils import WhyNoPartition
4142
from executorch.exir.dialects._ops import ops as exir_ops
43+
from executorch.exir.dialects.edge._ops import EdgeOpOverload
4244
from torch.export import ExportedProgram
4345
from torch.fx.experimental.symbolic_shapes import free_symbols
4446
from torch.fx.passes.utils.source_matcher_utils import (
@@ -50,6 +52,77 @@
5052
why = WhyNoPartition(logger=logger)
5153

5254

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

161234
return (True, gemm_deps)
162235
else:
@@ -248,10 +321,10 @@ def _get_bias_deps(
248321
if len(node.all_input_nodes) > 2 and self.bias_idx is not None:
249322
bias_node = get_input_node(node, self.bias_idx)
250323
if bias_node:
251-
if not is_param_node(ep, bias_node):
252-
why(node, "Expected bias to be a static param")
324+
bias_deps = get_static_param_deps(ep, bias_node, node)
325+
if bias_deps is None:
253326
return (False, [])
254-
gemm_deps.append(bias_node)
327+
gemm_deps.extend(bias_deps)
255328

256329
return (True, gemm_deps)
257330

0 commit comments

Comments
 (0)