# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # pylint: disable=invalid-name,unused-argument """Common pass instrumentation across IR variants.""" import functools import inspect import re import shutil from pathlib import Path import tvm_ffi import tvm.runtime from . import _ffi_instrument_api @tvm_ffi.register_object("instrument.PassInstrument") class PassInstrument(tvm.runtime.Object): """A pass instrument implementation. To use, a user class can either subclass from PassInstrument directly, or can apply the :py:func:`pass_instrument` wrapper. In either case, the `enter_pass_ctx`, `exit_pass_ctx`, `should_run`, `run_before_pass`, and `run_after_pass` methods can be defined to adjust the instrument's behavior. See the no-op implementations in this class definition for more information on each. """ def __init__(self): # initialize handle in case pi_cls creation failed. cls = type(self) # If the child class declared the method, then use it. # Otherwise, pass None to avoid a C++ -> Python round trip for # a no-op. def get_child_method(name): if getattr(cls, name) is getattr(PassInstrument, name): return None return getattr(self, name) # Create runtime pass instrument object. # register instance's enter_pass_ctx,exit_pass_ctx, should_run, run_before_pass and # run_after_pass methods to it if present. self.__init_handle_by_constructor__( _ffi_instrument_api.PassInstrument, cls.__name__, get_child_method("enter_pass_ctx"), get_child_method("exit_pass_ctx"), get_child_method("should_run"), get_child_method("run_before_pass"), get_child_method("run_after_pass"), ) def enter_pass_ctx(self): """Called when entering the instrumented context. Returns ------- None """ def exit_pass_ctx(self): """Called when exiting the instrumented context. Returns ------- None """ def should_run(self, mod, info): """Determine whether to run the pass or not. Called once for each pass that is run while the instrumented context is active. Parameters ---------- mod : tvm.ir.module.IRModule The module on which an optimization pass is being run. info : tvm.transform.PassInfo The pass information. Returns ------- should_run : bool True to run the pass, or False to skip the pass. """ def run_before_pass(self, mod, info): """Instrument before the pass runs. Called once for each pass that is run while the instrumented context is active. Parameters ---------- mod : tvm.ir.module.IRModule The module on which an optimization pass is being run. info : tvm.transform.PassInfo The pass information. Returns ------- None """ def run_after_pass(self, mod, info): """Instrument after the pass runs. Called once for each pass that is run while the instrumented context is active. Parameters ---------- mod : tvm.ir.module.IRModule The module on which an optimization pass is being run. info : tvm.transform.PassInfo The pass information. Returns ------- None """ def _wrap_class_pass_instrument(pi_cls): """Wrap a python class as pass instrument""" # No additional wrapping needed if the user class already # inherits. if issubclass(pi_cls, PassInstrument): return pi_cls class PyPassInstrument(pi_cls, PassInstrument): """Internal wrapper class to create a class instance.""" def __init__(self, *args, **kwargs): # initialize handle in case pi_cls creation failed. pi_cls.__init__(self, *args, **kwargs) PassInstrument.__init__(self) functools.update_wrapper(PyPassInstrument.__init__, pi_cls.__init__) PyPassInstrument.__name__ = pi_cls.__name__ PyPassInstrument.__doc__ = pi_cls.__doc__ PyPassInstrument.__module__ = pi_cls.__module__ return PyPassInstrument def pass_instrument(pi_cls=None): """Decorate a pass instrument. Parameters ---------- pi_class : class Instrument class. See example below. Examples -------- .. code-block:: python @tvm.instrument.pass_instrument class SkipPass: def __init__(self, skip_pass_name): self.skip_pass_name = skip_pass_name # Uncomment to customize # def enter_pass_ctx(self): # pass # Uncomment to customize # def exit_pass_ctx(self): # pass # If pass name contains keyword, skip it by return False. (return True: not skip) def should_run(self, mod, pass_info) if self.skip_pass_name in pass_info.name: return False return True # Uncomment to customize # def run_before_pass(self, mod, pass_info): # pass # Uncomment to customize # def run_after_pass(self, mod, pass_info): # pass skip_annotate = SkipPass("AnnotateSpans") with tvm.transform.PassContext(instruments=[skip_annotate]): tvm.compile(mod, "llvm") """ def create_pass_instrument(pi_cls): if not inspect.isclass(pi_cls): raise TypeError("pi_cls must be a class") return _wrap_class_pass_instrument(pi_cls) if pi_cls: return create_pass_instrument(pi_cls) return create_pass_instrument @tvm_ffi.register_object("instrument.PassInstrument") class PassTimingInstrument(tvm.runtime.Object): """A wrapper to create a passes time instrument that implemented in C++""" def __init__(self): self.__init_handle_by_constructor__(_ffi_instrument_api.MakePassTimingInstrument) @staticmethod def render(): """Retrieve rendered time profile result Returns ------- string : string The rendered string result of time profiles Examples -------- .. code-block:: python timing_inst = PassTimingInstrument() with tvm.transform.PassContext(instruments=[timing_inst]): relax_mod = relax.transform.FuseOps()(relax_mod) # before exiting the context, get profile results. profiles = timing_inst.render() """ return _ffi_instrument_api.RenderTimePassProfiles() @pass_instrument class PassPrintingInstrument: """A pass instrument to print if before or print ir after each element of a named pass.""" def __init__(self, print_before_pass_names, print_after_pass_names): self.print_before_pass_names = print_before_pass_names self.print_after_pass_names = print_after_pass_names def run_before_pass(self, mod, pass_info): if pass_info.name in self.print_before_pass_names: print(f"Print IR before: {pass_info.name}\n{mod}\n\n") def run_after_pass(self, mod, pass_info): if pass_info.name in self.print_after_pass_names: print(f"Print IR after: {pass_info.name}\n{mod}\n\n") @pass_instrument class PrintAfterAll: """Print the name of the pass, the IR, only after passes execute.""" def run_after_pass(self, mod, info): print(f"After Running Pass: {info}") print(mod) @pass_instrument class PrintBeforeAll: """Print the name of the pass, the IR, only before passes execute.""" def run_before_pass(self, mod, info): print(f"Before Running Pass: {info}") print(mod) @pass_instrument class DumpIR: """Dump the IR after the pass runs.""" def __init__(self, dump_dir: Path | str, refresh: bool = False): if isinstance(dump_dir, Path): self.dump_dir = dump_dir else: self.dump_dir = Path(dump_dir) self.counter = 0 if refresh and self.dump_dir.is_dir(): self._safe_remove_dump_dir() def _safe_remove_dump_dir(self): """Remove dump directory only if it contains only dumped IR files.""" # Pattern for dumped files: {counter:03d}_{pass_name}.py dump_pattern = re.compile(r"^\d{3}_.*\.py$") # Check all files in the directory for item in self.dump_dir.iterdir(): # If there's a subdirectory or a file that doesn't match the pattern, abort if item.is_dir() or not dump_pattern.match(item.name): print( f"WARNING: Skipping removal of {self.dump_dir} as it contains " f"non-dumped files or directories. Please clean it manually." ) return # Safe to remove - only contains dumped files try: shutil.rmtree(self.dump_dir) except OSError as e: print(f"WARNING: Failed to remove directory {self.dump_dir}: {e}") def run_after_pass(self, mod, info): self.dump_dir.mkdir(parents=True, exist_ok=True) try: sanitized_pass_name = re.sub(r'[<>:"/\\|?*]', "_", info.name) with open(self.dump_dir / f"{self.counter:03d}_{sanitized_pass_name}.py", "w") as f: f.write(mod.script()) except Exception: # pylint: disable=broad-exception-caught print(f"WARNING: Failed to dump IR for pass {info.name}") finally: self.counter += 1