Skip to content

Commit 68f6f07

Browse files
pytorchbotJCNTH
andcommitted
[ExecuTorch][Vulkan] Preserve persistent buffer mutations (#21662)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #21597 by @JCNTH ^ Please use this as the source of truth for the PR details, comments, and reviews ghstack PR base: https://github.com/pytorch/executorch/tree/gh/JCNTH/204/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/JCNTH/204/head Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/JCNTH/203/orig Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/JCNTH/204/orig Differential Revision: [D114936148](https://our.internmc.facebook.com/intern/diff/D114936148/) @diff-train-skip-merge cc @SS-JIA @manuelcandales @digantdesai @cbilgin --------- Co-authored-by: Julian Ng-Thow-Hing <juliannth@meta.com> Co-authored-by: Julian Ng-Thow-Hing <107437036+JCNTH@users.noreply.github.com>
1 parent 58c046f commit 68f6f07

35 files changed

Lines changed: 1720 additions & 200 deletions

backends/vulkan/serialization/vulkan_graph_builder.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import logging
1010
import operator
1111
from types import NoneType
12-
from typing import cast, List, Optional, Union
12+
from typing import cast, Dict, List, Optional, Union
1313

1414
import executorch.backends.vulkan.serialization.vulkan_graph_schema as vk_graph_schema
1515
import torch
@@ -28,6 +28,7 @@
2828
)
2929
from executorch.exir._serialize._named_data_store import NamedDataStore
3030
from executorch.exir.backend.utils import DelegateMappingBuilder
31+
from executorch.exir.dialects._ops import ops as exir_ops
3132
from executorch.exir.tensor import TensorSpec
3233
from torch._export.utils import get_buffer, get_param, is_buffer, is_param
3334
from torch.export import ExportedProgram
@@ -49,11 +50,41 @@ def __init__(
4950
delegate_mapping_builder: DelegateMappingBuilder,
5051
downcast_64_bit: bool = True,
5152
force_fp16: bool = False,
53+
alias_buffer_mutations: bool = False,
5254
) -> None:
5355
self.program = program
5456
self.delegate_mapping_builder = delegate_mapping_builder
5557
self.downcast_64_bit = downcast_64_bit
5658
self.force_fp16 = force_fp16
59+
self.buffer_mutation_inputs: Dict[str, Node] = {}
60+
self.buffer_mutation_user_outputs: set[str] = set()
61+
if alias_buffer_mutations:
62+
nodes_by_name = {
63+
node.name: node for node in program.graph_module.graph.nodes
64+
}
65+
buffer_inputs_by_target: Dict[str, Node] = {}
66+
for name, target in program.graph_signature.inputs_to_buffers.items():
67+
if name not in nodes_by_name:
68+
continue
69+
buffer_input = nodes_by_name[name]
70+
prepack = next(
71+
(
72+
user
73+
for user in buffer_input.users
74+
if user.op == "call_function"
75+
and user.target == exir_ops.edge.et_vk.prepack.default
76+
),
77+
None,
78+
)
79+
buffer_inputs_by_target[target] = prepack or buffer_input
80+
self.buffer_mutation_inputs = {
81+
output_name: buffer_inputs_by_target[target]
82+
for output_name, target in program.graph_signature.buffers_to_mutate.items()
83+
if target in buffer_inputs_by_target
84+
}
85+
self.buffer_mutation_user_outputs = set(
86+
program.graph_signature.user_outputs
87+
)
5788
self.chain = []
5889
self.values = []
5990
self.input_ids = []
@@ -160,6 +191,16 @@ def maybe_add_constant_tensor(self, node: Node) -> int:
160191
return constant_id
161192

162193
def create_node_value(self, node: Node) -> int:
194+
if node.name in self.buffer_mutation_inputs:
195+
input_node = self.buffer_mutation_inputs[node.name]
196+
if input_node not in self.node_to_value_ids:
197+
raise AssertionError(
198+
"Cannot alias a buffer mutation before its input is serialized"
199+
)
200+
value_id = self.node_to_value_ids[input_node]
201+
self.node_to_value_ids[node] = value_id
202+
return value_id
203+
163204
# If the node has been marked as a scalar tensor, create a SymInt instead of a tensor
164205
if is_symint_node(node) or node.meta.get("etvk_is_scalar_tensor", False):
165206
new_id = self.create_symint_value()
@@ -448,7 +489,10 @@ def process_output_node(self, node: Node) -> None:
448489
)
449490
# Mutable buffers outputs are not included as an output to the
450491
# delegate call. Skip marking them as an output.
451-
if is_mutable_buffer_node(out_node, self.program):
492+
if out_node.name in self.buffer_mutation_inputs:
493+
if out_node.name not in self.buffer_mutation_user_outputs:
494+
continue
495+
elif is_mutable_buffer_node(out_node, self.program):
452496
continue
453497

454498
self.output_ids.append(self.node_to_value_ids[out_node])

backends/vulkan/test/test_serialization.py

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
import ctypes
1010
import random
1111
import unittest
12-
from typing import List
12+
from types import SimpleNamespace
13+
from typing import List, Tuple
1314

15+
import executorch.backends.vulkan.custom_ops_lib # noqa: F401
1416
import torch
17+
from executorch.backends.vulkan.serialization import (
18+
vulkan_graph_builder as graph_builder_module,
19+
)
1520

1621
from executorch.backends.vulkan.serialization.vulkan_graph_schema import (
1722
IntList,
@@ -30,6 +35,137 @@
3035

3136

3237
class TestSerialization(unittest.TestCase):
38+
def _build_mutation_program(
39+
self, prepack: bool, shared_user_output: bool = False
40+
) -> Tuple[SimpleNamespace, torch.fx.Node, torch.fx.Node, torch.fx.Node]:
41+
graph = torch.fx.Graph()
42+
state = graph.placeholder("state")
43+
user_input = graph.placeholder("user_input")
44+
state.meta["spec"] = graph_builder_module.TensorSpec.from_tensor(torch.zeros(4))
45+
user_input.meta["spec"] = graph_builder_module.TensorSpec.from_tensor(
46+
torch.ones(4)
47+
)
48+
49+
state_value = state
50+
if prepack:
51+
state_value = graph.call_function(
52+
graph_builder_module.exir_ops.edge.et_vk.prepack.default,
53+
(state,),
54+
)
55+
state_value.meta["spec"] = graph_builder_module.TensorSpec.from_tensor(
56+
torch.zeros(4)
57+
)
58+
59+
mutation = graph.call_function(
60+
torch.ops.aten.add.Tensor, (state_value, user_input)
61+
)
62+
mutation.meta["spec"] = graph_builder_module.TensorSpec.from_tensor(
63+
torch.ones(4)
64+
)
65+
user_output = mutation
66+
if not shared_user_output:
67+
user_output = graph.call_function(
68+
torch.ops.aten.mul.Tensor, (user_input, 2.0)
69+
)
70+
user_output.meta["spec"] = graph_builder_module.TensorSpec.from_tensor(
71+
torch.ones(4)
72+
)
73+
graph.output((mutation, user_output))
74+
75+
graph_module = torch.fx.GraphModule({}, graph)
76+
signature = SimpleNamespace(
77+
buffers_to_mutate={mutation.name: "state"},
78+
inputs_to_buffers={state.name: "state"},
79+
inputs_to_lifted_tensor_constants={},
80+
inputs_to_parameters={},
81+
non_persistent_buffers=set(),
82+
user_outputs=(user_output.name,),
83+
)
84+
program = SimpleNamespace(
85+
constants={},
86+
graph_module=graph_module,
87+
graph_signature=signature,
88+
state_dict={"state": torch.zeros(4)},
89+
)
90+
return program, state_value, mutation, user_output
91+
92+
def test_alias_buffer_mutations_is_opt_in(self) -> None:
93+
for prepack in (False, True):
94+
with self.subTest(prepack=prepack):
95+
program, state_value, mutation, user_output = (
96+
self._build_mutation_program(prepack)
97+
)
98+
99+
default_builder = graph_builder_module.VkGraphBuilder(
100+
program,
101+
graph_builder_module.DelegateMappingBuilder(
102+
generated_identifiers=True
103+
),
104+
)
105+
default_graph = default_builder.build_graph()
106+
self.assertNotEqual(
107+
default_builder.node_to_value_ids[mutation],
108+
default_builder.node_to_value_ids[state_value],
109+
)
110+
self.assertEqual(
111+
default_graph.output_ids,
112+
[
113+
default_builder.node_to_value_ids[mutation],
114+
default_builder.node_to_value_ids[user_output],
115+
],
116+
)
117+
118+
explicit_false_builder = graph_builder_module.VkGraphBuilder(
119+
program,
120+
graph_builder_module.DelegateMappingBuilder(
121+
generated_identifiers=True
122+
),
123+
alias_buffer_mutations=False,
124+
)
125+
self.assertEqual(default_graph, explicit_false_builder.build_graph())
126+
127+
aliasing_builder = graph_builder_module.VkGraphBuilder(
128+
program,
129+
graph_builder_module.DelegateMappingBuilder(
130+
generated_identifiers=True
131+
),
132+
alias_buffer_mutations=True,
133+
)
134+
aliasing_graph = aliasing_builder.build_graph()
135+
self.assertEqual(
136+
aliasing_builder.node_to_value_ids[mutation],
137+
aliasing_builder.node_to_value_ids[state_value],
138+
)
139+
self.assertEqual(
140+
aliasing_graph.output_ids,
141+
[aliasing_builder.node_to_value_ids[user_output]],
142+
)
143+
144+
def test_alias_buffer_mutations_preserves_shared_user_output(self) -> None:
145+
for prepack in (False, True):
146+
with self.subTest(prepack=prepack):
147+
program, state_value, mutation, _ = self._build_mutation_program(
148+
prepack, shared_user_output=True
149+
)
150+
builder = graph_builder_module.VkGraphBuilder(
151+
program,
152+
graph_builder_module.DelegateMappingBuilder(
153+
generated_identifiers=True
154+
),
155+
alias_buffer_mutations=True,
156+
)
157+
158+
graph = builder.build_graph()
159+
160+
self.assertEqual(
161+
builder.node_to_value_ids[mutation],
162+
builder.node_to_value_ids[state_value],
163+
)
164+
self.assertEqual(
165+
graph.output_ids,
166+
[builder.node_to_value_ids[mutation]],
167+
)
168+
33169
def _generate_random_const_tensors(self, num_tensors: int) -> List[torch.Tensor]:
34170
"""
35171
Helper function to generate `num_tensor` buffers of random sizes and random contents,

backends/vulkan/test/test_vulkan_compile_options.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ def test_skip_memory_planning_round_trips(self) -> None:
4141
round_tripped = self._round_trip({"skip_memory_planning": True})
4242
self.assertTrue(round_tripped.get("skip_memory_planning"))
4343

44+
def test_alias_buffer_mutations_round_trips(self) -> None:
45+
round_tripped = self._round_trip({"alias_buffer_mutations": True})
46+
self.assertTrue(round_tripped.get("alias_buffer_mutations"))
47+
4448
def test_force_fp16_round_trips(self) -> None:
4549
round_tripped = self._round_trip({"force_fp16": True})
4650
self.assertTrue(round_tripped.get("force_fp16"))
@@ -105,23 +109,23 @@ def build_graph():
105109
), patch(
106110
"executorch.backends.vulkan.vulkan_preprocess.VkGraphBuilder",
107111
return_value=graph_builder,
108-
), patch(
112+
) as graph_builder_factory, patch(
109113
"executorch.backends.vulkan.vulkan_preprocess.serialize_vulkan_graph",
110114
return_value=b"vk_graph",
111115
):
112116
result = VulkanBackend.preprocess(program, parse_compile_options(options))
113-
return result.data_store_output, externalize_pte_data
117+
return result.data_store_output, externalize_pte_data, graph_builder_factory
114118

115119
def test_external_constants_default_keeps_constants_inline(self) -> None:
116-
output, externalize_pte_data = self._preprocess_named_data({})
120+
output, externalize_pte_data, _ = self._preprocess_named_data({})
117121

118122
self.assertEqual(output.buffers, [b"constant"])
119123
self.assertEqual(output.pte_data, {"constant": DataEntry(0, 16, None)})
120124
self.assertEqual(output.external_data, {})
121125
externalize_pte_data.assert_not_called()
122126

123127
def test_external_constants_option_externalizes_constants(self) -> None:
124-
output, externalize_pte_data = self._preprocess_named_data(
128+
output, externalize_pte_data, _ = self._preprocess_named_data(
125129
{"external_constants_max_data_bytes": 16}
126130
)
127131

@@ -131,8 +135,21 @@ def test_external_constants_option_externalizes_constants(self) -> None:
131135
self.assertEqual(list(next(iter(output.external_data.values()))), ["constant"])
132136
externalize_pte_data.assert_called_once_with(16, "vulkan_constants")
133137

138+
def test_alias_buffer_mutations_reaches_graph_builder(self) -> None:
139+
for options, expected in (
140+
({}, False),
141+
({"alias_buffer_mutations": True}, True),
142+
):
143+
with self.subTest(options=options):
144+
_, _, graph_builder_factory = self._preprocess_named_data(options)
145+
self.assertIs(
146+
graph_builder_factory.call_args.kwargs["alias_buffer_mutations"],
147+
expected,
148+
)
149+
134150
def test_unset_options_are_absent(self) -> None:
135151
round_tripped = self._round_trip({})
152+
self.assertNotIn("alias_buffer_mutations", round_tripped)
136153
self.assertNotIn("small_texture_limits", round_tripped)
137154
self.assertNotIn("skip_memory_planning", round_tripped)
138155
self.assertNotIn("external_constants_max_data_bytes", round_tripped)

backends/vulkan/vulkan_preprocess.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ def parse_compile_spec(compile_specs: List[CompileSpec]) -> Dict[str, Any]:
130130
if spec.key == "skip_memory_planning":
131131
options[spec.key] = bool.from_bytes(spec.value, byteorder="little")
132132

133+
if spec.key == "alias_buffer_mutations":
134+
options[spec.key] = bool.from_bytes(spec.value, byteorder="little")
135+
133136
if spec.key == "external_constants_max_data_bytes":
134137
options[spec.key] = _parse_external_constants_max_data_bytes(spec.value)
135138

@@ -172,6 +175,7 @@ def preprocess( # noqa: C901
172175
)
173176
downcast_64_bit = compile_options.get("downcast_64_bit", True)
174177
force_fp16 = compile_options.get("force_fp16", False)
178+
alias_buffer_mutations = compile_options.get("alias_buffer_mutations", False)
175179

176180
program = unsafe_remove_auto_functionalized_pass(program)
177181

@@ -258,6 +262,7 @@ def preprocess( # noqa: C901
258262
DelegateMappingBuilder(generated_identifiers=True),
259263
downcast_64_bit=downcast_64_bit,
260264
force_fp16=force_fp16,
265+
alias_buffer_mutations=alias_buffer_mutations,
261266
)
262267
vk_graph = graph_builder.build_graph()
263268
external_constants_max_data_bytes = compile_options.get(

backends/webgpu/runtime/WebGPUDispatchMath.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ namespace executorch::backends::webgpu::utils {
2424
// Ceiling division for non-negative integers (mirrors Vulkan's utils::div_up).
2525
template <typename T>
2626
inline T div_up(T a, T b) {
27-
return (a + b - 1) / b;
27+
return a / b + (a % b != 0);
2828
}
2929

3030
// Product of a tensor's dims; the same accumulation was duplicated per-op.

0 commit comments

Comments
 (0)