|
| 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 | + |
| 8 | +import copy |
| 9 | +import os |
| 10 | +import tempfile |
| 11 | + |
| 12 | +import uuid |
| 13 | + |
| 14 | +from executorch.backends.xnnpack.partition.xnnpack_partitioner import XnnpackPartitioner |
| 15 | +from executorch.backends.xnnpack.utils.configs import get_xnnpack_edge_compile_config |
| 16 | + |
| 17 | +from executorch.devtools import BundledProgram, generate_etrecord |
| 18 | +from executorch.devtools.bundled_program.config import MethodTestCase, MethodTestSuite |
| 19 | +from executorch.exir import to_edge |
| 20 | + |
| 21 | +from executorch.extension.pybindings.portable_lib import ( |
| 22 | + _load_for_executorch_from_buffer, # @manual |
| 23 | +) |
| 24 | +from torch.export import export |
| 25 | + |
| 26 | + |
| 27 | +def _generate_new_paths(): |
| 28 | + temp_dir = tempfile.mkdtemp() |
| 29 | + |
| 30 | + # Use uuid to generate unique filenames |
| 31 | + etrecord_filename = f"etrecord_{uuid.uuid4().hex}.bin" |
| 32 | + etdump_filename = f"etdump_{uuid.uuid4().hex}.etdp" |
| 33 | + debug_buffer_filename = f"debug_buffer_{uuid.uuid4().hex}.bin" |
| 34 | + etrecord_path = os.path.join(temp_dir, etrecord_filename) |
| 35 | + etdump_path = os.path.join(temp_dir, etdump_filename) |
| 36 | + debug_buffer_path = os.path.join(temp_dir, debug_buffer_filename) |
| 37 | + return etrecord_path, etdump_path, debug_buffer_path |
| 38 | + |
| 39 | + |
| 40 | +def generate_etrecord_and_etdump( |
| 41 | + model, |
| 42 | + model_inputs, |
| 43 | + debug_buffer_size=1024 * 1024 * 1024, |
| 44 | + method_name="forward", |
| 45 | + num_test_cases=2, |
| 46 | + disturb=False, |
| 47 | +): |
| 48 | + """ |
| 49 | + Helper to generate ETRecord and ETDump (with debug buffer) for a model. |
| 50 | +
|
| 51 | + Returns: |
| 52 | + Tuple of (etrecord_path, etdump_path, debug_buffer_path) |
| 53 | + """ |
| 54 | + |
| 55 | + etrecord_path, etdump_path, debug_buffer_path = _generate_new_paths() |
| 56 | + |
| 57 | + aten_model = export(model, model_inputs, strict=True) |
| 58 | + |
| 59 | + edge_compile_config = get_xnnpack_edge_compile_config() |
| 60 | + |
| 61 | + edge_program_manager = to_edge(aten_model, compile_config=edge_compile_config) |
| 62 | + |
| 63 | + edge_program_manager_copy = copy.deepcopy(edge_program_manager) |
| 64 | + |
| 65 | + # Apply the disturbance if the flag is set |
| 66 | + if disturb: |
| 67 | + import torch |
| 68 | + |
| 69 | + for _, exported_program in edge_program_manager_copy._edge_programs.items(): |
| 70 | + for module in exported_program.graph_module.modules(): |
| 71 | + if not isinstance(module, torch.fx.GraphModule): |
| 72 | + continue |
| 73 | + for node in module.graph.nodes: |
| 74 | + if node.op == "call_function" and node.name == "aten_add_tensor": |
| 75 | + node.target = torch.ops.aten.sub.Tensor |
| 76 | + module.recompile() |
| 77 | + module.graph.eliminate_dead_code() |
| 78 | + |
| 79 | + edge_program_manager = edge_program_manager.to_backend(XnnpackPartitioner()) |
| 80 | + |
| 81 | + et_program_manager = edge_program_manager.to_executorch() |
| 82 | + |
| 83 | + method_graphs = {method_name: export(model, model_inputs, strict=True)} |
| 84 | + inputs = [list(model_inputs) for _ in range(num_test_cases)] |
| 85 | + method_test_suites = [ |
| 86 | + MethodTestSuite( |
| 87 | + method_name=method_name, |
| 88 | + test_cases=[ |
| 89 | + MethodTestCase( |
| 90 | + inputs=inp, expected_outputs=getattr(model, method_name)(*inp) |
| 91 | + ) |
| 92 | + for inp in inputs |
| 93 | + ], |
| 94 | + ) |
| 95 | + ] |
| 96 | + executorch_program = ( |
| 97 | + to_edge(method_graphs, compile_config=edge_compile_config) |
| 98 | + .to_backend(XnnpackPartitioner()) |
| 99 | + .to_executorch() |
| 100 | + ) |
| 101 | + bundled_program = BundledProgram(executorch_program, method_test_suites) |
| 102 | + |
| 103 | + # Generate ETRecord |
| 104 | + generate_etrecord(etrecord_path, edge_program_manager_copy, bundled_program) |
| 105 | + |
| 106 | + # Generate ETDump and debug buffer |
| 107 | + buff = et_program_manager.buffer |
| 108 | + executorch_module = _load_for_executorch_from_buffer( |
| 109 | + buff, |
| 110 | + enable_etdump=True, |
| 111 | + debug_buffer_size=debug_buffer_size, |
| 112 | + ) |
| 113 | + executorch_module.run_method(method_name, tuple(model_inputs)) |
| 114 | + executorch_module.write_etdump_result_to_file(etdump_path, debug_buffer_path) |
| 115 | + |
| 116 | + return etrecord_path, etdump_path, debug_buffer_path |
| 117 | + |
| 118 | + |
| 119 | +from typing import Tuple |
| 120 | + |
| 121 | +import pandas as pd |
| 122 | +from executorch.devtools import Inspector |
| 123 | + |
| 124 | + |
| 125 | +def check_numeric_gap( |
| 126 | + etdump_path: str, |
| 127 | + etrecord_path: str, |
| 128 | + debug_buffer_path: str, |
| 129 | + metric: str, |
| 130 | + max_allowed_gap: float, |
| 131 | +) -> Tuple[bool, float]: |
| 132 | + """ |
| 133 | + Create an Inspector and check if the maximum numeric gap for a given metric is less than the allowed threshold. |
| 134 | + Args: |
| 135 | + etdump_path: Path to the ETDump file. |
| 136 | + etrecord_path: Path to the ETRecord file. |
| 137 | + debug_buffer_path: Path to the debug buffer file. |
| 138 | + metric: The metric name to calculate the numeric gap for (e.g., "MSE"). |
| 139 | + max_allowed_gap: The maximum allowed gap threshold. |
| 140 | + Returns: |
| 141 | + A tuple (is_within_threshold, max_gap) where: |
| 142 | + - is_within_threshold (bool): True if max gap < max_allowed_gap, else False. |
| 143 | + - max_gap (float): The maximum gap value found. |
| 144 | + """ |
| 145 | + inspector = Inspector( |
| 146 | + etdump_path=etdump_path, |
| 147 | + etrecord=etrecord_path, |
| 148 | + debug_buffer_path=debug_buffer_path, |
| 149 | + ) |
| 150 | + df: pd.DataFrame = inspector.calculate_numeric_gap(metric) |
| 151 | + max_gap = df["gap"].apply(lambda x: max(x) if isinstance(x, list) else x).max() |
| 152 | + is_within_threshold = max_gap < max_allowed_gap |
| 153 | + return is_within_threshold, max_gap |
| 154 | + |
| 155 | + |
| 156 | +def check_disturbance( |
| 157 | + etdump_path: str, |
| 158 | + etrecord_path: str, |
| 159 | + debug_buffer_path: str, |
| 160 | + metric: str, |
| 161 | + row: int, |
| 162 | + max_allowed_gap: float, |
| 163 | + disturbance_threshold: float, |
| 164 | +) -> bool: |
| 165 | + """ |
| 166 | + Check if the given row in the DataFrame has a gap greater than the disturbance threshold. |
| 167 | +
|
| 168 | + Args: |
| 169 | + etdump_path: Path to the ETDump file. |
| 170 | + etrecord_path: Path to the ETRecord file. |
| 171 | + debug_buffer_path: Path to the debug buffer file. |
| 172 | + metric: The metric name to calculate the numeric gap for (e.g., "MSE"). |
| 173 | + disturbance_threshold: The threshold to detect a disturbance. |
| 174 | + max_allowed_gap: The maximum allowed gap threshold before the disturbance(row). |
| 175 | + row: The row number to check for a disturbance. |
| 176 | + """ |
| 177 | + inspector = Inspector( |
| 178 | + etdump_path=etdump_path, |
| 179 | + etrecord=etrecord_path, |
| 180 | + debug_buffer_path=debug_buffer_path, |
| 181 | + ) |
| 182 | + df: pd.DataFrame = inspector.calculate_numeric_gap(metric) |
| 183 | + |
| 184 | + # Get the maximum gap for the given row |
| 185 | + disturbance_row_gap = max(df.loc[row, "gap"]) |
| 186 | + # Get the maximum gap for the rows before the given row |
| 187 | + if row > 0: |
| 188 | + before_disturbance_row_gap = max(df.loc[: row - 1, "gap"].apply(max)) |
| 189 | + else: |
| 190 | + before_disturbance_row_gap = 0 |
| 191 | + |
| 192 | + return ( |
| 193 | + disturbance_row_gap > disturbance_threshold |
| 194 | + and before_disturbance_row_gap < max_allowed_gap |
| 195 | + ) |
0 commit comments