Skip to content

Commit 9ca4ed0

Browse files
Jack-Khuufacebook-github-bot
authored andcommitted
Introduce DelegateMappingBuilder (#184)
Summary: Pull Request resolved: #184 For creating the debug_handle_map generated in the design doc, this diff introduces a helper class for manage the state of the handle map `get_delegate_mapping`: Is used to return the debug handle map out of preprocess `upsert_delegate_mapping_entry`: Handles the creation and manipulation of debug handle entries --- Reviewed By: tarun292 Differential Revision: D48569900 fbshipit-source-id: 2a6e462d9306380465a46ae20b2aeb0cb1c4dc67
1 parent 511b441 commit 9ca4ed0

File tree

3 files changed

+282
-1
lines changed

3 files changed

+282
-1
lines changed

exir/backend/test/TARGETS

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,18 @@ python_unittest(
211211
],
212212
)
213213

214+
python_unittest(
215+
name = "test_delegate_map_builder",
216+
srcs = [
217+
"test_delegate_map_builder.py",
218+
],
219+
deps = [
220+
"//caffe2:torch",
221+
"//executorch/exir:lib",
222+
"//executorch/exir/backend:utils",
223+
],
224+
)
225+
214226
python_unittest(
215227
name = "test_utils",
216228
srcs = [
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
from typing import Iterator, Union
9+
10+
import torch
11+
from executorch import exir
12+
13+
from executorch.exir.backend.utils import DelegateMappingBuilder
14+
15+
16+
class TestDelegateMapBuilder(unittest.TestCase):
17+
def setUp(self) -> None:
18+
class Model(torch.nn.Module):
19+
def __init__(self):
20+
super().__init__()
21+
22+
def forward(self, x):
23+
y = torch.sin(x)
24+
return torch.cos(y)
25+
26+
model = Model()
27+
model_inputs = (torch.ones(1, 1),)
28+
program = (
29+
exir.capture(model, model_inputs, exir.CaptureConfig(pt2_mode=True))
30+
.to_edge()
31+
.to_executorch()
32+
)
33+
34+
# Create nodes for testing mapping
35+
# nodes: [arg0_1, alloc, aten_sin_default, alloc_1, aten_cos_default, output]
36+
# debug handles: [0, None, 1, None, 2, 3]
37+
self.nodes = list(program.graph_module.graph.nodes)
38+
39+
def test_basic_generated_identifier(self):
40+
delegate_builder = DelegateMappingBuilder(generated_identifiers=True)
41+
42+
expected_mapping = {0: (0, 1, 2, 3)}
43+
self.assertEqual(delegate_builder.upsert_delegate_mapping_entry(self.nodes), 0)
44+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
45+
46+
expected_mapping = {0: (0, 1, 2, 3), 1: (0,)}
47+
self.assertEqual(
48+
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0]), 1
49+
)
50+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
51+
52+
def test_basic_manual_int_identifier(self):
53+
self._test_basic_manual_identifier(iter([22, 55]))
54+
55+
def test_basic_manual_string_identifier(self):
56+
self._test_basic_manual_identifier(iter(["22", "55"]))
57+
58+
def test_appending_nodes_generated_identifier(self):
59+
delegate_builder = DelegateMappingBuilder(generated_identifiers=True)
60+
61+
expected_mapping = {0: (0,)}
62+
self.assertEqual(
63+
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0]), 0
64+
)
65+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
66+
self._test_appending_nodes(delegate_builder, 0)
67+
68+
def test_appending_nodes_manual_int_identifier(self):
69+
delegate_builder = DelegateMappingBuilder()
70+
71+
expected_mapping = {22: (0,)}
72+
self.assertEqual(
73+
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0], 22), 22
74+
)
75+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
76+
self._test_appending_nodes(delegate_builder, 22)
77+
78+
def test_appending_nodes_manual_string_identifier(self):
79+
delegate_builder = DelegateMappingBuilder()
80+
81+
expected_mapping = {"22": (0,)}
82+
self.assertEqual(
83+
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0], "22"), "22"
84+
)
85+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
86+
self._test_appending_nodes(delegate_builder, "22")
87+
88+
def test_adding_manual_identifier_when_generated(self):
89+
delegate_builder = DelegateMappingBuilder(generated_identifiers=True)
90+
91+
self.assertRaises(
92+
Exception,
93+
lambda: delegate_builder.upsert_delegate_mapping_entry(self.nodes, "22"),
94+
)
95+
96+
def test_omitting_identifier_when_not_generated(self):
97+
delegate_builder = DelegateMappingBuilder()
98+
99+
self.assertRaises(
100+
Exception,
101+
lambda: delegate_builder.upsert_delegate_mapping_entry(self.nodes),
102+
)
103+
104+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
105+
106+
def _test_basic_manual_identifier(self, identifiers: Iterator[Union[int, str]]):
107+
"""
108+
Using the iteration of identifiers:
109+
1) Create a Delegate Map Builder
110+
2) Add an entry with a list of Nodes using the first identifier
111+
3) Add an entry with a single node using the second identifier
112+
113+
Verify behavior results
114+
"""
115+
delegate_builder = DelegateMappingBuilder()
116+
117+
# Entry with a list of nodes
118+
iden_1 = next(identifiers)
119+
expected_mapping = {iden_1: (0, 1, 2, 3)}
120+
self.assertEqual(
121+
delegate_builder.upsert_delegate_mapping_entry(self.nodes, iden_1), iden_1
122+
)
123+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
124+
125+
# Entry with a single node
126+
iden_2 = next(identifiers)
127+
expected_mapping = {iden_1: (0, 1, 2, 3), iden_2: (0,)}
128+
self.assertEqual(
129+
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0], iden_2),
130+
iden_2,
131+
)
132+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
133+
134+
def _test_appending_nodes(
135+
self, delegate_builder: DelegateMappingBuilder, identifier: Union[int, str]
136+
):
137+
"""
138+
For the provided delegate_builder and identifier:
139+
1) Append a duplicate Node (previously in builder)
140+
2) Append a list of Nodes
141+
3) Append a single Node
142+
143+
Verify behavior results
144+
"""
145+
# Add a duplicate node
146+
expected_mapping = {identifier: (0,)}
147+
self.assertEqual(
148+
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0], identifier),
149+
identifier,
150+
)
151+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
152+
153+
# Add a list of nodes
154+
expected_mapping = {identifier: (0, 1, 2)}
155+
self.assertEqual(
156+
delegate_builder.upsert_delegate_mapping_entry(self.nodes[1:5], identifier),
157+
identifier,
158+
)
159+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
160+
161+
# Add a single node
162+
expected_mapping = {identifier: (0, 1, 2, 3)}
163+
self.assertEqual(
164+
delegate_builder.upsert_delegate_mapping_entry(self.nodes[5], identifier),
165+
identifier,
166+
)
167+
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)

exir/backend/utils.py

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
import logging
8+
from collections import defaultdict
79
from functools import lru_cache
8-
from typing import Iterable, List, Tuple
10+
from typing import Dict, Iterable, List, Optional, Set, Tuple, Union
911

1012
import torch
1113
from executorch.exir.dialects._ops import ops as exir_ops
1214

1315
from executorch.exir.lowered_backend_module import create_submodule_from_nodes
16+
from torch.fx.node import Node
1417
from torch.fx.passes.utils.source_matcher_utils import SourcePartition
1518

1619
T_QuantPerTensor = exir_ops.edge.quantized_decomposed.quantize_per_tensor.default
1720
T_DQuantPerTensor = exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default
1821

22+
log: logging.Logger = logging.getLogger(__name__)
23+
1924

2025
@lru_cache(maxsize=128)
2126
def is_same_node(
@@ -158,3 +163,100 @@ def replace_quantized_partition_with_op(
158163
graph_module.recompile()
159164

160165
return (replaced_op, dequant_nodes, quant_nodes)
166+
167+
168+
# TODO - style: use templated types
169+
class DelegateMappingBuilder:
170+
"""
171+
Profiling helper class for building Delegate Maps.
172+
Delegate Maps are a mapping from log entry identifiers to node
173+
debug handles. Specifically this is used to log within backend delegates
174+
175+
Args:
176+
generated_identifiers (bool, optional): Whether delegate map keys are
177+
generated automatically. Defaults to False.
178+
"""
179+
180+
def __init__(self, generated_identifiers: bool = False):
181+
self._generated_identifiers = generated_identifiers
182+
183+
# Note that the internal struct has a Set value, while the getter
184+
# function returns the values as a tuple
185+
self._debug_handle_map: Union[
186+
Dict[int, Set[int]], Dict[str, Set[int]]
187+
] = defaultdict(set)
188+
self._next_index: int = 0
189+
190+
def get_delegate_mapping(
191+
self,
192+
) -> Union[Dict[int, Tuple[int]], Dict[str, Tuple[int]]]:
193+
"""
194+
Returns:
195+
Union[Dict[int, Tuple[int]], Dict[str, Tuple[int]]]:
196+
A map of delegate debug identifier to a list of debug handles
197+
The keys (identifier) are either integers or strings
198+
The values are a sorted tuple of integer debug handles
199+
"""
200+
# pyre-ignore Warning between Union[Dict[K, V], Dict[K2, V]] vs Dict[Union[K, K2], V]
201+
return {k: tuple(sorted(v)) for k, v in self._debug_handle_map.items()}
202+
203+
def upsert_delegate_mapping_entry(
204+
self,
205+
nodes: Union[Node, List[Node]],
206+
identifier: Optional[Union[int, str]] = None,
207+
) -> Union[int, str]:
208+
"""
209+
Add or append to an existing delegate mapping entry
210+
211+
If self._generated_identifiers = False:
212+
- An identifier must be provided, else this is no-op
213+
214+
If self._generated_identifiers = True:
215+
- NEW identifiers cannot be manually provided. Existing identifier
216+
can be provided
217+
- New identifiers are generated incrementally, 0 indexed
218+
219+
If a provided identifier already exists, node entries are appended
220+
221+
Args:
222+
nodes (Union[Node, List[Node]]): A (list of) Node(s)
223+
identifier (Optional[Union[int, str]]):
224+
Debug identifier corresponding to the Node(s)
225+
226+
Returns:
227+
Optional[Union[int, str]]:
228+
Delegate debug identifier corresponding to the node group
229+
None is returned, if no identifier is associated
230+
"""
231+
232+
# Check for manual addition of identifier (with generated identifiers enabled)
233+
if (
234+
self._generated_identifiers
235+
and identifier is not None
236+
and identifier not in self._debug_handle_map
237+
):
238+
raise Exception(
239+
f"Builders using generated identifiers can't manually add identifiers: {identifier}. Failed to add or update entry"
240+
)
241+
242+
# Resolve Identifier
243+
if identifier is None:
244+
if self._generated_identifiers:
245+
identifier = self._next_index
246+
self._next_index += 1
247+
else:
248+
raise Exception(
249+
"No identifier provided. Failed to add or update entry."
250+
)
251+
252+
# Get all debug handles found in the nodes
253+
# Note that missing debug handles are not surfaced
254+
new_debug_handles = {
255+
handle
256+
for node in (nodes if isinstance(nodes, List) else [nodes])
257+
if (handle := node.meta.get("debug_handle")) is not None
258+
}
259+
260+
# pyre-ignore Warning from Union[int, st] keys
261+
self._debug_handle_map[identifier].update(new_debug_handles)
262+
return identifier

0 commit comments

Comments
 (0)