Skip to content

Commit 8bdaa13

Browse files
Jack-Khuufacebook-github-bot
authored andcommitted
Outline for Delegate Debug Index Documentation (#234)
Summary: Pull Request resolved: #234 Reviewed By: Jack-Khuu Differential Revision: D49060998
1 parent e379356 commit 8bdaa13

File tree

3 files changed

+164
-53
lines changed

3 files changed

+164
-53
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Profiling and debugging delegates
2+
3+
Delegate backends are a prominent component of Edge Models. One attribute of
4+
delegated backends is that they operate mostly as an opaque transformation.
5+
This gives delegate authors greater freedom when defining backend behavior,
6+
but also prevents the Executorch authoring flow from tracking underlying changes.
7+
8+
This makes associating profiling and debug information through delegated
9+
graphs difficult. We have provided a framework that will enable delegate authors
10+
to propagate this information and retrieve it for post run analysis. The process is
11+
broken down into two stages:
12+
13+
1) **Ahead-of-time delegation stage** - Delegate authors need to generate
14+
a debug handle map using the process described below.
15+
16+
2) **Runtime stage** - Delegate authors need to log the profiling data along with the
17+
delegate debug identifiers generated in stage 1 using the API's described below in
18+
the runtime section.
19+
20+
## 1). AOT (ahead-of-time) delegation Stage
21+
### Generating a debug handle map:
22+
**Delegate debug identifiers** are used by delegate authors to mark points of
23+
interest in the lowered graph. Identifiers are associated with operator
24+
nodes of the pre-lowered model graph.
25+
26+
- *For example: If a delegate author wants to signal the fusion of 3 operators
27+
into a single operator of the lowered graph, they would register the 3
28+
original operators to the delegate debug identifier ahead-of-time and then log using the
29+
delegate debug identifier at runtime.*
30+
31+
This is tracked by the `debug_handle_map` and returned as a part of
32+
**PreprocessResult** by the call to `preprocess` from the ahead-of-time implementation of the delegated
33+
backends. The `debug_handle_map` is essentially used as a mechanism to communicate what transformations
34+
occurred in the backend.
35+
36+
```python
37+
class PreprocessResult:
38+
processed_bytes: bytes = bytes()
39+
40+
debug_handle_map: Optional[
41+
Union[Dict[int, Tuple[int]], Dict[str, Tuple[int]]]
42+
] = None
43+
44+
...
45+
```
46+
47+
The construction of this map is done via a **DelegateMappingBuilder**.
48+
49+
50+
### DelegateMappingBuilder
51+
52+
**DelegateMappingBuilder** is a helper class for managing and constructing
53+
`delegate_handle_map`. A new instance should be used in each `preprocess` call
54+
and the result of this builder should be passed in when constructing
55+
`PreprocessResult`
56+
57+
First, create a DelegateMappingBuilder instance that uses either
58+
manually provided identifiers or generated identifiers for node association.
59+
60+
- `DelegateMappingBuilder()`
61+
- With __manual identifiers__, users pass in a str or int delegate debug identifier
62+
when creating entries
63+
- `DelegateMappingBuilder(generated_identifiers=True)`
64+
- With __generated identifier__, the builder will auto-assign an delegate debug identifier
65+
66+
**Note: A single DelegateMappingBuilder instance can use either manual
67+
or generated identifiers, but not both**
68+
69+
70+
Next, use `insert_delegate_mapping_entry` to iteratively construct the
71+
delegate_map. It takes Node(s) to associate and an optional
72+
delegate debug identifier (only intended to be used for the manual identifiers case described above).
73+
The identifier used is returned from the call.
74+
75+
```python
76+
def insert_delegate_mapping_entry(
77+
self,
78+
nodes: Union[fx.Node, List[fx.Node]],
79+
identifier: Optional[Union[int, str]] = None,
80+
) -> Union[int, str]:
81+
```
82+
83+
Finally, use `get_delegate_mapping` to retrieve the constructed map.
84+
The return value can be directly passed to **PreprocessResults**.
85+
86+
```python
87+
def get_delegate_mapping(
88+
self,
89+
) -> Union[Dict[int, Tuple[int]], Dict[str, Tuple[int]]]
90+
```
91+
92+
## 2). Runtime stage
93+
94+
NOTE : These API's are not available yet but shown here to give a representation of what the
95+
runtime side of things looks like.
96+
97+
### ID based API's:
98+
99+
If users used integer ID's to generate delegate_debug_identifiers during the AOT process then
100+
they should log their profiling events using the following API's.
101+
102+
Option 1 (For when users can explicitly mark the start and end of an event):
103+
```C++
104+
EventEntry event_entry = EVENT_TRACER_BEGIN_DELEGATE_PROFILING_EVENT_ID(event_tracer, id)
105+
EVENT_TRACER_END_DELEGATE_PROFILING_EVENT_ID(event_entry)
106+
```
107+
108+
Option 2 (For when users only have access to the start and end time of the events after they have occurred.)
109+
```C++
110+
EVENT_TRACER_LOG_DELEGATE_PROFILING_EVENT_ID(event_tracer, id, start_time, end_time)
111+
```
112+
113+
### String based API's:
114+
115+
If users used strings to generate delegate_debug_identifiers during the AOT process then they
116+
should log their profiling events using the following API's.
117+
118+
Option 1 (For when users can explicitly mark the start and end of an event):
119+
```C++
120+
EventEntry = EVENT_TRACER_BEGIN_DELEGATE_PROFILING_EVENT_NAME(event_tracer, name)
121+
EVENT_TRACER_END_DELEGATE_PROFILING_EVENT_NAME(event_entry)
122+
```
123+
124+
Option 2 (For when users only have access to the start and end time of the events after they have occurred.)
125+
```C++
126+
EVENT_TRACER_LOG_DELEGATE_PROFILING_EVENT_NAME(event_tracer, name, start_time, end_time)
127+
```
128+
129+
## Examples:
130+
131+
To indicate how these API's can be used we have provided an end-to-end representative example.
132+
133+
Demo backend that generates delegate mapping for a model that undergoes some simple transformations
134+
in the backend.
135+
`executorch/exir/backend/test/backend_with_delegate_mapping_demo.py`
136+
137+
Corresponding runtime backend code that logs the delegate debug identifiers that were generated
138+
during the ahead-of-time processing done in the above backend example.
139+
140+
`executorch/runtime/executor/test/test_backend_with_delegate_mapping.cpp`

exir/backend/test/test_delegate_map_builder.py

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ def test_basic_generated_identifier(self):
4040
delegate_builder = DelegateMappingBuilder(generated_identifiers=True)
4141

4242
expected_mapping = {0: (0, 1, 2, 3)}
43-
self.assertEqual(delegate_builder.upsert_delegate_mapping_entry(self.nodes), 0)
43+
self.assertEqual(delegate_builder.insert_delegate_mapping_entry(self.nodes), 0)
4444
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
4545

4646
expected_mapping = {0: (0, 1, 2, 3), 1: (0,)}
4747
self.assertEqual(
48-
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0]), 1
48+
delegate_builder.insert_delegate_mapping_entry(self.nodes[0]), 1
4949
)
5050
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
5151

@@ -60,45 +60,50 @@ def test_appending_nodes_generated_identifier(self):
6060

6161
expected_mapping = {0: (0,)}
6262
self.assertEqual(
63-
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0]), 0
63+
delegate_builder.insert_delegate_mapping_entry(self.nodes[0]), 0
6464
)
6565
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
66-
self._test_appending_nodes(delegate_builder, 0)
6766

6867
def test_appending_nodes_manual_int_identifier(self):
6968
delegate_builder = DelegateMappingBuilder()
7069

7170
expected_mapping = {22: (0,)}
7271
self.assertEqual(
73-
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0], 22), 22
72+
delegate_builder.insert_delegate_mapping_entry(self.nodes[0], 22), 22
7473
)
7574
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
76-
self._test_appending_nodes(delegate_builder, 22)
7775

7876
def test_appending_nodes_manual_string_identifier(self):
7977
delegate_builder = DelegateMappingBuilder()
8078

8179
expected_mapping = {"22": (0,)}
8280
self.assertEqual(
83-
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0], "22"), "22"
81+
delegate_builder.insert_delegate_mapping_entry(self.nodes[0], "22"), "22"
8482
)
8583
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
86-
self._test_appending_nodes(delegate_builder, "22")
8784

8885
def test_adding_manual_identifier_when_generated(self):
8986
delegate_builder = DelegateMappingBuilder(generated_identifiers=True)
9087

9188
self.assertRaises(
9289
Exception,
93-
lambda: delegate_builder.upsert_delegate_mapping_entry(self.nodes, "22"),
90+
lambda: delegate_builder.insert_delegate_mapping_entry(self.nodes, "22"),
9491
)
9592

9693
def test_omitting_identifier_when_not_generated(self):
9794
delegate_builder = DelegateMappingBuilder()
9895

9996
self.assertRaises(
10097
Exception,
101-
lambda: delegate_builder.upsert_delegate_mapping_entry(self.nodes),
98+
lambda: delegate_builder.insert_delegate_mapping_entry(self.nodes),
99+
)
100+
101+
def test_resinsert_delegate_debug_identifier(self):
102+
delegate_builder = DelegateMappingBuilder()
103+
delegate_builder.insert_delegate_mapping_entry(self.nodes[0], "1")
104+
self.assertRaises(
105+
Exception,
106+
lambda: delegate_builder.insert_delegate_mapping_entry(self.nodes[0], "1"),
102107
)
103108

104109
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -118,50 +123,15 @@ def _test_basic_manual_identifier(self, identifiers: Iterator[Union[int, str]]):
118123
iden_1 = next(identifiers)
119124
expected_mapping = {iden_1: (0, 1, 2, 3)}
120125
self.assertEqual(
121-
delegate_builder.upsert_delegate_mapping_entry(self.nodes, iden_1), iden_1
126+
delegate_builder.insert_delegate_mapping_entry(self.nodes, iden_1), iden_1
122127
)
123128
self.assertEqual(delegate_builder.get_delegate_mapping(), expected_mapping)
124129

125130
# Entry with a single node
126131
iden_2 = next(identifiers)
127132
expected_mapping = {iden_1: (0, 1, 2, 3), iden_2: (0,)}
128133
self.assertEqual(
129-
delegate_builder.upsert_delegate_mapping_entry(self.nodes[0], iden_2),
134+
delegate_builder.insert_delegate_mapping_entry(self.nodes[0], iden_2),
130135
iden_2,
131136
)
132137
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: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def get_delegate_mapping(
200200
# pyre-ignore Warning between Union[Dict[K, V], Dict[K2, V]] vs Dict[Union[K, K2], V]
201201
return {k: tuple(sorted(v)) for k, v in self._debug_handle_map.items()}
202202

203-
def upsert_delegate_mapping_entry(
203+
def insert_delegate_mapping_entry(
204204
self,
205205
nodes: Union[Node, List[Node]],
206206
identifier: Optional[Union[int, str]] = None,
@@ -230,15 +230,16 @@ def upsert_delegate_mapping_entry(
230230
"""
231231

232232
# 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-
):
233+
if self._generated_identifiers and identifier is not None:
238234
raise Exception(
239235
f"Builders using generated identifiers can't manually add identifiers: {identifier}. Failed to add or update entry"
240236
)
241237

238+
if identifier is not None and identifier in self._debug_handle_map:
239+
raise Exception(
240+
"This delegate debug identifier was already inserted. Duplicate delegate debug identifiers are not allowed."
241+
)
242+
242243
# Resolve Identifier
243244
if identifier is None:
244245
if self._generated_identifiers:

0 commit comments

Comments
 (0)