Skip to content

Commit e60110a

Browse files
Jack-Khuufacebook-github-bot
authored andcommitted
Convert Flatcc ETDump Profile Entries into Underlying EventBlocks (#367)
Summary: Pull Request resolved: #367 This diff ingests the `ETDump Flatcc` and populates the EventBlocks/Events in the `Inspector` constructor Notably: This diff only consumes the profile events entries Reviewed By: tarun292 Differential Revision: D49100624 fbshipit-source-id: 2466017b2db25655219dbde45808d93a34b3c92a
1 parent 4e03536 commit e60110a

File tree

4 files changed

+322
-1
lines changed

4 files changed

+322
-1
lines changed

sdk/etdb/TARGETS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ python_library(
4040
"//caffe2:torch",
4141
"//executorch/exir:lib",
4242
"//executorch/sdk/edir:et_schema",
43+
"//executorch/sdk/etdump:schema_flatcc",
4344
"//executorch/sdk/etrecord:etrecord",
4445
],
4546
)

sdk/etdb/inspector.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
# LICENSE file in the root directory of this source tree.
66

77
import dataclasses
8+
from collections import defaultdict, OrderedDict
89
from dataclasses import dataclass
9-
from typing import Dict, List, Mapping, Optional, Union
10+
from typing import Dict, List, Mapping, NewType, Optional, Tuple, Union
1011

1112
import numpy as np
1213
import pandas as pd
@@ -15,10 +16,39 @@
1516

1617
from executorch.sdk.edir.et_schema import OperatorGraphWithStats
1718
from executorch.sdk.etdb._inspector_utils import gen_graphs_from_etrecord
19+
from executorch.sdk.etdump.schema_flatcc import ETDumpFlatCC, ProfileEvent
1820
from executorch.sdk.etrecord import parse_etrecord
1921
from tabulate import tabulate
2022

2123

24+
# Signature of a ProfileEvent
25+
@dataclass(frozen=True, order=True)
26+
class ProfileEventSignature:
27+
name: str
28+
instruction_id: Optional[int]
29+
delegate_id: Optional[int] = None
30+
delegate_id_str: Optional[str] = None
31+
32+
@staticmethod
33+
def _gen_from_event(event: ProfileEvent) -> "ProfileEventSignature":
34+
"""
35+
Given a ProfileEvent, extract the fields into a signature
36+
37+
ProfileEvents from ETDump default to "" and -1 when the field is not populated
38+
The Signature will convert these back to the intended None value
39+
"""
40+
return ProfileEventSignature(
41+
event.name,
42+
event.instruction_id if event.instruction_id != -1 else None,
43+
event.delegate_debug_id_int if event.delegate_debug_id_int != -1 else None,
44+
event.delegate_debug_id_str if event.delegate_debug_id_str != "" else None,
45+
)
46+
47+
48+
# Signature of a RunData as defined by its ProfileEvents
49+
RunSignature = NewType("RunSignature", Tuple[ProfileEventSignature])
50+
51+
2252
@dataclass
2353
class PerfData:
2454
def __init__(self, raw: List[float]):
@@ -75,6 +105,35 @@ class Event:
75105
delegate_backend_name: Optional[str] = None
76106
debug_data: List[torch.Tensor] = dataclasses.field(default_factory=list)
77107

108+
@staticmethod
109+
def _gen_from_profile_events(
110+
signature: ProfileEventSignature, events: List[ProfileEvent]
111+
) -> "Event":
112+
"""
113+
Given a ProfileEventSignature and a list of ProfileEvents with that signature,
114+
return an Event object matching the ProfileEventSignature, with perf_data
115+
populated from the list of ProfileEvents
116+
"""
117+
delegate_debug_identifier = (
118+
signature.delegate_id or signature.delegate_id_str or None
119+
)
120+
121+
# Use the delegate identifier as the event name if delegated
122+
is_delegated_op = delegate_debug_identifier is not None
123+
name = signature.name if not is_delegated_op else str(delegate_debug_identifier)
124+
125+
perf_data = PerfData(
126+
[float(event.end_time - event.start_time) / 1000 for event in events]
127+
)
128+
129+
return Event(
130+
name=name,
131+
perf_data=perf_data,
132+
instruction_id=signature.instruction_id,
133+
delegate_debug_identifier=delegate_debug_identifier,
134+
is_delegated_op=is_delegated_op,
135+
)
136+
78137

79138
@dataclass
80139
class EventBlock:
@@ -118,6 +177,53 @@ def to_dataframe(self) -> pd.DataFrame:
118177
df = pd.DataFrame(data)
119178
return df
120179

180+
@staticmethod
181+
def _gen_from_etdump(etdump: ETDumpFlatCC) -> List["EventBlock"]:
182+
"""
183+
Given an etdump, generate a list of EventBlocks corresponding to the
184+
contents
185+
"""
186+
187+
# Group all the RunData by the set of profile events
188+
profile_run_groups: Mapping[
189+
RunSignature,
190+
OrderedDict[ProfileEventSignature, List[ProfileEvent]],
191+
] = defaultdict(OrderedDict)
192+
for run in etdump.run_data:
193+
if (run_events := run.events) is None:
194+
continue
195+
196+
# Identify all the ProfileEventSignatures
197+
profile_events: OrderedDict[
198+
ProfileEventSignature, ProfileEvent
199+
] = OrderedDict()
200+
for event in run_events:
201+
if (profile_event := event.profile_event) is not None:
202+
signature = ProfileEventSignature._gen_from_event(profile_event)
203+
profile_events[signature] = profile_event
204+
205+
# Create a RunSignature from the ProfileEventSignature found
206+
run_signature: RunSignature = RunSignature(tuple(profile_events.keys()))
207+
208+
# Update the Profile Run Groups, indexed on the RunSignature
209+
run_signature_events: OrderedDict[
210+
ProfileEventSignature, List[ProfileEvent]
211+
] = profile_run_groups[run_signature]
212+
for event_signature, event in profile_events.items():
213+
run_signature_events.setdefault(event_signature, []).append(event)
214+
215+
# Create EventBlocks from the Profile Run Groups
216+
return [
217+
EventBlock(
218+
name=str(index),
219+
events=[
220+
Event._gen_from_profile_events(signature, event)
221+
for signature, event in profile_events.items()
222+
],
223+
)
224+
for index, profile_events in enumerate(profile_run_groups.values())
225+
]
226+
121227

122228
class Inspector:
123229
"""

sdk/etdb/tests/TARGETS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,12 @@ python_unittest(
99
"//executorch/sdk/etdb:inspector",
1010
],
1111
)
12+
13+
python_unittest(
14+
name = "event_blocks_test",
15+
srcs = ["event_blocks_test.py"],
16+
deps = [
17+
"//executorch/sdk/etdb:inspector",
18+
"//executorch/sdk/etdump:schema_flatcc",
19+
],
20+
)

sdk/etdb/tests/event_blocks_test.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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+
# pyre-strict
8+
import unittest
9+
from typing import List, Optional, Tuple, Union
10+
11+
import executorch.sdk.etdump.schema_flatcc as flatcc
12+
from executorch.sdk.etdb.inspector import (
13+
Event,
14+
EventBlock,
15+
PerfData,
16+
ProfileEventSignature,
17+
)
18+
from executorch.sdk.etdump.schema_flatcc import ETDumpFlatCC
19+
20+
21+
class TestEventBlock(unittest.TestCase):
22+
23+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test Helpers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24+
@staticmethod
25+
def _gen_sample_profile_event(
26+
name: str,
27+
instruction_id: int,
28+
time: Tuple[int, int],
29+
delegate_debug_id: Optional[Union[int, str]] = None,
30+
) -> flatcc.ProfileEvent:
31+
"""
32+
Helper for generating test ProfileEvents
33+
34+
Notably:
35+
- the timestamp is specified as a tuple of two separate integers
36+
- delegate_debug_id takes either the str or int representation
37+
- chain_idx is auto-populated to 0
38+
"""
39+
delegate_debug_id_int = (
40+
delegate_debug_id if isinstance(delegate_debug_id, int) else -1
41+
)
42+
delegate_debug_id_str = (
43+
delegate_debug_id if isinstance(delegate_debug_id, str) else ""
44+
)
45+
return flatcc.ProfileEvent(
46+
name,
47+
0,
48+
instruction_id,
49+
delegate_debug_id_int,
50+
delegate_debug_id_str,
51+
start_time=time[0],
52+
end_time=time[1],
53+
)
54+
55+
@staticmethod
56+
def _get_sample_etdump_flatcc() -> flatcc.ETDumpFlatCC:
57+
"""
58+
Helper for getting a sample ETDumpFlatCC object with 3 RunData:
59+
- run_data_1 has a signature with just profile_1
60+
- run_data_2 has the same signature with run_data_1, but differnt times
61+
- run_data_3 has a signature with both (profile_1, profile_2)
62+
"""
63+
profile_event_1 = TestEventBlock._gen_sample_profile_event(
64+
name="profile_1", instruction_id=1, time=(0, 1), delegate_debug_id=100
65+
)
66+
run_data_1 = flatcc.RunData(
67+
name="run_data_1",
68+
allocators=[],
69+
events=[
70+
flatcc.Event(
71+
allocation_event=None,
72+
debug_event=None,
73+
profile_event=profile_event_1,
74+
)
75+
],
76+
)
77+
profile_event_2 = TestEventBlock._gen_sample_profile_event(
78+
name="profile_1", instruction_id=1, time=(2, 4), delegate_debug_id=100
79+
)
80+
run_data_2 = flatcc.RunData(
81+
name="run_data_2",
82+
allocators=[],
83+
events=[
84+
flatcc.Event(
85+
allocation_event=None,
86+
debug_event=None,
87+
profile_event=profile_event_2,
88+
)
89+
],
90+
)
91+
92+
profile_event_3 = TestEventBlock._gen_sample_profile_event(
93+
name="profile_1", instruction_id=1, time=(5, 6), delegate_debug_id=100
94+
)
95+
profile_event_4 = TestEventBlock._gen_sample_profile_event(
96+
name="profile_2", instruction_id=2, time=(7, 8), delegate_debug_id=100
97+
)
98+
run_data_3 = flatcc.RunData(
99+
name="run_data_3",
100+
allocators=[],
101+
events=[
102+
flatcc.Event(
103+
allocation_event=None,
104+
debug_event=None,
105+
profile_event=profile_event_3,
106+
),
107+
flatcc.Event(
108+
allocation_event=None,
109+
debug_event=None,
110+
profile_event=profile_event_4,
111+
),
112+
],
113+
)
114+
115+
return ETDumpFlatCC(version=0, run_data=[run_data_1, run_data_2, run_data_3])
116+
117+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
118+
119+
def test_gen_from_etdump(self) -> None:
120+
"""
121+
Test "e2e" generation of EventBlocks given an ETDump
122+
- Generated via EventBock.gen_from_etdump
123+
124+
Specifically it tests for external correctness:
125+
- Correct number of EventBlocks
126+
- Correct number of Events and Raw Data values (iterations)
127+
"""
128+
etdump: ETDumpFlatCC = TestEventBlock._get_sample_etdump_flatcc()
129+
blocks: List[EventBlock] = EventBlock._gen_from_etdump(etdump)
130+
131+
self.assertEqual(len(blocks), 2, f"Expected 2 runs, got {len(blocks)}")
132+
133+
# One EventBlock should have 1 event with 2 iterations
134+
# The other EventBlock should have 2 events with 1 iterations
135+
run_counts = {
136+
(len(block.events), len(block.events[0].perf_data.raw)) for block in blocks
137+
}
138+
self.assertSetEqual(run_counts, {(1, 2), (2, 1)})
139+
140+
def test_inspector_event_generation(self) -> None:
141+
"""
142+
Test Inspector.Event derivation from various ProfileEvent cases
143+
- Non Delegated
144+
- Delegate with Int Debug ID
145+
- Delegate with String Debug ID
146+
"""
147+
148+
def _test_profile_event_generation(
149+
name: str,
150+
instruction_id: int,
151+
delegate_debug_id_int: Optional[int] = None,
152+
delegate_debug_id_str: Optional[str] = None,
153+
) -> None:
154+
"""
155+
Helper function for testing that the provided ProfileEvent fields are
156+
properly translated to Inspector.ProfileEventSignature and Inspector.Event
157+
"""
158+
delegate_debug_id = delegate_debug_id_int or delegate_debug_id_str
159+
profile_event: flatcc.ProfileEvent = (
160+
TestEventBlock._gen_sample_profile_event(
161+
name,
162+
instruction_id,
163+
(0, 1),
164+
delegate_debug_id,
165+
)
166+
)
167+
168+
# Test Signature Generation
169+
signature = ProfileEventSignature._gen_from_event(profile_event)
170+
expected_signature = ProfileEventSignature(
171+
name, instruction_id, delegate_debug_id_int, delegate_debug_id_str
172+
)
173+
self.assertEqual(signature, expected_signature)
174+
175+
# Test Event Generation
176+
durations = [10, 20, 30]
177+
profile_events: List[flatcc.ProfileEvent] = [
178+
TestEventBlock._gen_sample_profile_event(
179+
name,
180+
instruction_id,
181+
(0, 10),
182+
delegate_debug_id,
183+
)
184+
for time in durations
185+
]
186+
event = Event._gen_from_profile_events(signature, profile_events)
187+
188+
is_delegated = delegate_debug_id is not None
189+
expected_event = Event(
190+
str(delegate_debug_id) if is_delegated else name,
191+
PerfData([float(duration) / 1000 for duration in durations]),
192+
instruction_id=signature.instruction_id,
193+
delegate_debug_identifier=delegate_debug_id,
194+
is_delegated_op=is_delegated,
195+
)
196+
self.assertEqual(event, expected_event)
197+
198+
# Non Delegated
199+
_test_profile_event_generation("non-delegate", 1)
200+
201+
# Delegate with Int Debug ID
202+
_test_profile_event_generation("delegate", 1, 100)
203+
204+
# Delegate with String Debug ID
205+
_test_profile_event_generation("delegate", 1, None, "identifier")

0 commit comments

Comments
 (0)