From 4088ba16f61140ac815d866bcf50684685880746 Mon Sep 17 00:00:00 2001 From: Utkarsh Bahuguna Date: Mon, 12 Oct 2026 00:19:43 +0530 Subject: [PATCH 1/2] Normalize ETRecord map keys in Inspector --- devtools/inspector/_inspector.py | 27 +++++++++++---- devtools/inspector/tests/inspector_test.py | 39 ++++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/devtools/inspector/_inspector.py b/devtools/inspector/_inspector.py index 1d5a52715c9..a15dcb0d86c 100644 --- a/devtools/inspector/_inspector.py +++ b/devtools/inspector/_inspector.py @@ -1132,15 +1132,28 @@ def _consume_etrecord(self, reference_graph_name) -> None: return # (1) Debug Handle Symbolification + # These maps are keyed by instruction id: ints on an ETRecord object + # from get_etrecord(), strs once it went through save() and + # parse_etrecord() (JSON). _gen_resolve_debug_handles looks up strs. + def with_str_keys(mapping: Optional[Dict]) -> Optional[Dict]: + if mapping is None: + return None + return {str(key): value for key, value in mapping.items()} + + debug_handle_map = with_str_keys(self._etrecord._debug_handle_map[FORWARD]) + delegate_map = ( + with_str_keys(self._etrecord._delegate_map[FORWARD]) + if self._etrecord._delegate_map is not None + else None + ) + instruction_id_to_num_outs_map = with_str_keys( + self._etrecord._instruction_id_to_num_outs_map[FORWARD] + ) for event_block in self.event_blocks: event_block._gen_resolve_debug_handles( - self._etrecord._debug_handle_map[FORWARD], - ( - self._etrecord._delegate_map[FORWARD] - if self._etrecord._delegate_map is not None - else None - ), - self._etrecord._instruction_id_to_num_outs_map[FORWARD], + debug_handle_map, + delegate_map, + instruction_id_to_num_outs_map, ) # (2) Event Metadata Association diff --git a/devtools/inspector/tests/inspector_test.py b/devtools/inspector/tests/inspector_test.py index 4c59190650c..320ec2f9e21 100644 --- a/devtools/inspector/tests/inspector_test.py +++ b/devtools/inspector/tests/inspector_test.py @@ -390,6 +390,45 @@ def test_inspector_get_exported_program(self): ) ) + def test_inspector_with_etrecord_object_resolves_debug_handles(self): + _, _, et_output = TestETRecord().get_test_model(generate_etrecord=True) + etrecord = et_output.get_etrecord() + # An ETRecord object from get_etrecord() keys its maps by int instruction id + instruction_id = next(iter(etrecord._debug_handle_map["forward"])) + self.assertIsInstance(instruction_id, int) + + def gen_event_blocks(*args, **kwargs): + return [ + EventBlock( + name=EVENT_BLOCK_NAME, + events=[Event(name="op", _instruction_id=instruction_id)], + ) + ] + + with tempfile.TemporaryDirectory() as tmp_dir: + etrecord_path = os.path.join(tmp_dir, "etrecord.bin") + etrecord.save(etrecord_path) + + with patch.object( + _inspector, "gen_etdump_object", return_value=None + ), patch.object( + EventBlock, "_gen_from_etdump", side_effect=gen_event_blocks + ): + inspector_from_object = Inspector( + etdump_path=ETDUMP_PATH, etrecord=etrecord + ) + inspector_from_file = Inspector( + etdump_path=ETDUMP_PATH, etrecord=etrecord_path + ) + + event_from_object = inspector_from_object.event_blocks[0].events[0] + event_from_file = inspector_from_file.event_blocks[0].events[0] + self.assertIsNotNone(event_from_file.debug_handles) + # Passing the ETRecord object directly must give the same result as + # passing the path of the saved ETRecord + self.assertEqual(event_from_object.debug_handles, event_from_file.debug_handles) + self.assertEqual(event_from_object.op_types, event_from_file.op_types) + def test_populate_debugging_related_fields_raises_for_inconsistent_events(self): ret_event: Event = Event( name="event", From f2e916f003e654360d20f560625111e76bfa5a80 Mon Sep 17 00:00:00 2001 From: Utkarsh Bahuguna Date: Wed, 4 Nov 2026 11:27:12 +0530 Subject: [PATCH 2/2] Normalize nested ETRecord maps via JSON round trip --- devtools/inspector/_inspector.py | 43 +++++++-------- devtools/inspector/tests/inspector_test.py | 61 ++++++++++++++++++++++ 2 files changed, 81 insertions(+), 23 deletions(-) diff --git a/devtools/inspector/_inspector.py b/devtools/inspector/_inspector.py index a15dcb0d86c..968296883a1 100644 --- a/devtools/inspector/_inspector.py +++ b/devtools/inspector/_inspector.py @@ -8,6 +8,7 @@ # pyre-unsafe import dataclasses +import json import logging import sys import warnings @@ -932,7 +933,7 @@ def _gen_resolve_debug_handles( self, handle_map: Dict[str, List[int]], delegate_map: Optional[Dict[str, DelegateMetadata]] = None, - instruction_id_to_num_outs_map: Dict[int, int] = None, + instruction_id_to_num_outs_map: Dict[str, int] = None, ): """ Given mappings from instruction id to debug handles, populate the @@ -1132,29 +1133,25 @@ def _consume_etrecord(self, reference_graph_name) -> None: return # (1) Debug Handle Symbolification - # These maps are keyed by instruction id: ints on an ETRecord object - # from get_etrecord(), strs once it went through save() and - # parse_etrecord() (JSON). _gen_resolve_debug_handles looks up strs. - def with_str_keys(mapping: Optional[Dict]) -> Optional[Dict]: - if mapping is None: - return None - return {str(key): value for key, value in mapping.items()} - - debug_handle_map = with_str_keys(self._etrecord._debug_handle_map[FORWARD]) - delegate_map = ( - with_str_keys(self._etrecord._delegate_map[FORWARD]) - if self._etrecord._delegate_map is not None - else None - ) - instruction_id_to_num_outs_map = with_str_keys( - self._etrecord._instruction_id_to_num_outs_map[FORWARD] - ) - for event_block in self.event_blocks: - event_block._gen_resolve_debug_handles( - debug_handle_map, - delegate_map, - instruction_id_to_num_outs_map, + if self.event_blocks: + # An ETRecord object from get_etrecord() has int keys (at every level) + # and tuples; save()/parse_etrecord() gives the JSON shape (str keys, + # lists) that _gen_resolve_debug_handles expects, so use that for both. + debug_handle_map = json.loads(json.dumps(self._etrecord._debug_handle_map)) + delegate_map = ( + json.loads(json.dumps(self._etrecord._delegate_map))[FORWARD] + if self._etrecord._delegate_map is not None + else None ) + num_outs_map = json.loads( + json.dumps(self._etrecord._instruction_id_to_num_outs_map) + ) + for event_block in self.event_blocks: + event_block._gen_resolve_debug_handles( + debug_handle_map[FORWARD], + delegate_map, + num_outs_map[FORWARD], + ) # (2) Event Metadata Association self.op_graph_dict = gen_graphs_from_etrecord( diff --git a/devtools/inspector/tests/inspector_test.py b/devtools/inspector/tests/inspector_test.py index 320ec2f9e21..887033f4b7d 100644 --- a/devtools/inspector/tests/inspector_test.py +++ b/devtools/inspector/tests/inspector_test.py @@ -429,6 +429,67 @@ def gen_event_blocks(*args, **kwargs): self.assertEqual(event_from_object.debug_handles, event_from_file.debug_handles) self.assertEqual(event_from_object.op_types, event_from_file.op_types) + def test_inspector_with_delegated_etrecord_object_matches_saved_file(self): + _, _, et_output = TestETRecord().get_test_model(generate_etrecord=True) + etrecord = et_output.get_etrecord() + instruction_id = next(iter(etrecord._debug_handle_map["forward"])) + # Shape produced by DelegateMappingBuilder: int keys, tuple values + etrecord._delegate_map = { + "forward": { + instruction_id: { + "name": "TestBackend", + "delegate_map": {0: (1, 2), 5: (3,)}, + } + } + } + + def gen_event_blocks(*args, **kwargs): + return [ + EventBlock( + name=EVENT_BLOCK_NAME, + events=[ + Event(name="op", _instruction_id=instruction_id), + Event( + name="delegated", + _instruction_id=instruction_id, + delegate_debug_identifier=0, + ), + # not in the delegate map + Event( + name="unmapped", + _instruction_id=instruction_id, + delegate_debug_identifier=7, + ), + ], + ) + ] + + with tempfile.TemporaryDirectory() as tmp_dir: + etrecord_path = os.path.join(tmp_dir, "etrecord.bin") + etrecord.save(etrecord_path) + + with patch.object( + _inspector, "gen_etdump_object", return_value=None + ), patch.object( + EventBlock, "_gen_from_etdump", side_effect=gen_event_blocks + ): + inspector_from_object = Inspector( + etdump_path=ETDUMP_PATH, etrecord=etrecord + ) + inspector_from_file = Inspector( + etdump_path=ETDUMP_PATH, etrecord=etrecord_path + ) + + events_from_object = inspector_from_object.event_blocks[0].events + events_from_file = inspector_from_file.event_blocks[0].events + self.assertEqual(events_from_file[1].debug_handles, [1, 2]) + for from_object, from_file in zip(events_from_object, events_from_file): + self.assertEqual(from_object.debug_handles, from_file.debug_handles) + self.assertEqual(from_object.num_outputs, from_file.num_outputs) + self.assertEqual( + from_object.delegate_backend_name, from_file.delegate_backend_name + ) + def test_populate_debugging_related_fields_raises_for_inconsistent_events(self): ret_event: Event = Event( name="event",