#!/usr/bin/env python3 """CLI for OSACA""" import argparse import io import os import re import sys from functools import lru_cache from osaca.db_interface import import_benchmark_output, sanity_check from osaca.frontend import Frontend from osaca.parser import BaseParser, ParserAArch64, ParserX86ATT from osaca.semantics import ( INSTR_FLAGS, ArchSemantics, KernelDG, MachineModel, reduce_to_section, ) SUPPORTED_ARCHS = [ "SNB", "IVB", "HSW", "BDW", "SKX", "CSX", "ICL", "ZEN1", "ZEN2", "TX2", "N1", "A64FX", ] DEFAULT_ARCHS = { "aarch64": "A64FX", "x86": "SKX", } # Stolen from pip def __read(*names, **kwargs): """Reads in file""" with io.open( os.path.join(os.path.dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8"), ) as fp: return fp.read() # Stolen from pip def __find_version(*file_paths): """Searches for a version attribute in the given file(s)""" version_file = __read(*file_paths) version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") def get_version(): """ Gets the current OSACA version stated in the __init__ file :returns: str -- the version string. """ return __find_version("__init__.py") def create_parser(parser=None): """ Return argparse parser. :param parser: Existing parser object to add the arguments, defaults to `None` :type parser: :class:`~Argparse.ArgumentParser` :returns: The newly created :class:`~Argparse.ArgumentParser` object. """ # Create parser if not parser: parser = argparse.ArgumentParser( description="Analyzes a marked innermost loop snippet for a given architecture type.", epilog="For help, examples, documentation and bug reports go to:\nhttps://github.com" "/RRZE-HPC/OSACA/ | License: AGPLv3", ) # Add arguments parser.add_argument( "-V", "--version", action="version", version="%(prog)s " + __find_version("__init__.py"), ) parser.add_argument( "--arch", type=str, help="Define architecture (SNB, IVB, HSW, BDW, SKX, CSX, ICL, ZEN1, ZEN2, TX2, N1, " "A64FX). If no architecture is given, OSACA assumes a default uarch for x86/AArch64.", ) parser.add_argument( "--fixed", action="store_true", help="Run the throughput analysis with fixed probabilities for all suitable ports per " "instruction. Otherwise, OSACA will print the optimal port utilization for the kernel.", ) parser.add_argument( "--lines", type=str, help="Define lines that should be included in the analysis. This option overwrites any" " range defined by markers in the assembly. Add either single lines or ranges defined by" ' "-" or ":", each entry separated by commas, e.g.: --lines 1,2,8-18,20:24', ) parser.add_argument( "--db-check", dest="check_db", action="store_true", help='Run a sanity check on the by "--arch" specified database. The output depends ' "on the verbosity level.", ) parser.add_argument( "--online", dest="internet_check", action="store_true", help="Run sanity check with online DB validation (currently felixcloutier) to see the " "src/dst distribution of the operands. Can be only used in combination with --db-check.", ) parser.add_argument( "--import", metavar="MICROBENCH", dest="import_data", type=str, default=argparse.SUPPRESS, help="Import a given microbenchmark output file into the corresponding architecture " 'instruction database. Define the type of microbenchmark either as "ibench" or ' '"asmbench".', ) parser.add_argument( "--insert-marker", dest="insert_marker", action="store_true", help="Try to find assembly block containing the loop to analyse and insert byte " "marker by using Kerncraft.", ) parser.add_argument( "--export-graph", metavar="EXPORT_PATH", dest="dotpath", default=None, type=str, help='Output path for .dot file export. If "." is given, the file will be stored as ' '"./osaca_dg.dot"', ) parser.add_argument( "--ignore-unknown", dest="ignore_unknown", action="store_true", help="Ignore if instructions cannot be found in the data file and print analysis anyway.", ) parser.add_argument( "--lcd-timeout", dest="lcd_timeout", metavar="SECONDS", type=int, default=10, help="Set timeout in seconds for LCD analysis. After timeout, OSACA will continue" " its analysis with the dependency paths found up to this point. Defaults to 10." " Set to -1 for no timeout.", ) parser.add_argument( "--verbose", "-v", action="count", default=0, help="Increases verbosity level." ) parser.add_argument( "--out", "-o", default=sys.stdout, type=argparse.FileType("w"), help="Write analysis to this file (default to stdout).", ) parser.add_argument( "file", type=argparse.FileType("r"), help="Path to object (ASM or instruction file).", ) return parser def check_arguments(args, parser): """ Check arguments passed by user that are not checked by argparse itself. :param args: arguments given from :class:`~argparse.ArgumentParser` after parsing :param parser: :class:`~argparse.ArgumentParser` object """ supported_import_files = ["ibench", "asmbench"] # manually set CLX to CSX to support both abbreviations if args.arch and args.arch.upper() == "CLX": args.arch = "CSX" if args.arch is None and (args.check_db or "import_data" in args): parser.error( "DB check and data import cannot work with a default microarchitecture. " "Please see --help for all valid architecture codes." ) elif args.arch is not None and args.arch.upper() not in SUPPORTED_ARCHS: parser.error( "Microarchitecture not supported. Please see --help for all valid architecture codes." ) if "import_data" in args and args.import_data not in supported_import_files: parser.error( "Microbenchmark not supported for data import. Please see --help for all valid " "microbenchmark codes." ) if args.internet_check and not args.check_db: parser.error("--online requires --check-db") def import_data(benchmark_type, arch, filepath, output_file=sys.stdout): """ Imports benchmark results from micro-benchmarks. :param benchmark_type: key for defining type of benchmark output :type benchmark_type: str :param arch: target architecture to put the data into the right database :type arch: str :param filepath: filepath of the output file" :type filepath: str :param output_file: output stream specifying where to write output, defaults to :class:`sys.stdout` :type output_file: stream, optional """ if benchmark_type.lower() == "ibench": import_benchmark_output(arch, "ibench", filepath, output=output_file) elif benchmark_type.lower() == "asmbench": import_benchmark_output(arch, "asmbench", filepath, output=output_file) else: raise NotImplementedError("This benchmark input variant is not supported.") def insert_byte_marker(args): """ Inserts byte markers into an assembly file using kerncraft. :param args: arguments given from :class:`~argparse.ArgumentParser` after parsing """ try: from kerncraft.incore_model import asm_instrumentation except ImportError: print( "Module kerncraft not installed. Use 'pip install --user " "kerncraft' for installation.\nFor more information see " "https://github.com/RRZE-HPC/kerncraft", file=sys.stderr, ) sys.exit(1) assembly = args.file.read() unmarked_assembly = io.StringIO(assembly) marked_assembly = io.StringIO() asm_instrumentation( input_file=unmarked_assembly, output_file=marked_assembly, block_selection="manual", pointer_increment="auto_with_manual_fallback", isa=MachineModel.get_isa_for_arch(args.arch), ) marked_assembly.seek(0) assembly = marked_assembly.read() with open(args.file.name, "w") as f: f.write(assembly) def inspect(args, output_file=sys.stdout): """ Does the actual throughput and critical path analysis of OSACA and prints it to the terminal. :param args: arguments given from :class:`~argparse.ArgumentParser` after parsing :param output_file: Define the stream for output, defaults to :class:`sys.stdout` :type output_file: stream, optional """ # Read file code = args.file.read() # Detect ISA if necessary arch = args.arch if args.arch is not None else DEFAULT_ARCHS[BaseParser.detect_ISA(code)] print_arch_warning = False if args.arch else True isa = MachineModel.get_isa_for_arch(arch) verbose = args.verbose ignore_unknown = args.ignore_unknown # Parse file parser = get_asm_parser(arch) try: parsed_code = parser.parse_file(code) except Exception as e: # probably the wrong parser based on heuristic if args.arch is None: # change ISA and try again arch = ( DEFAULT_ARCHS["x86"] if BaseParser.detect_ISA(code) == "aarch64" else DEFAULT_ARCHS["aarch64"] ) isa = MachineModel.get_isa_for_arch(arch) parser = get_asm_parser(arch) parsed_code = parser.parse_file(code) else: raise e # Reduce to marked kernel or chosen section and add semantics if args.lines: line_range = get_line_range(args.lines) kernel = [line for line in parsed_code if line["line_number"] in line_range] print_length_warning = False else: kernel = reduce_to_section(parsed_code, isa) # Print warning if kernel has no markers and is larger than threshold (100) print_length_warning = ( True if len(kernel) == len(parsed_code) and len(kernel) > 100 else False ) machine_model = MachineModel(arch=arch) semantics = ArchSemantics(machine_model) semantics.add_semantics(kernel) # Do optimal schedule for kernel throughput if wished if not args.fixed: semantics.assign_optimal_throughput(kernel) # Create DiGrahps kernel_graph = KernelDG(kernel, parser, machine_model, semantics, args.lcd_timeout) if args.dotpath is not None: kernel_graph.export_graph(args.dotpath if args.dotpath != "." else None) # Print analysis frontend = Frontend(args.file.name, arch=arch) print( frontend.full_analysis( kernel, kernel_graph, ignore_unknown=ignore_unknown, arch_warning=print_arch_warning, length_warning=print_length_warning, lcd_warning=kernel_graph.timed_out, verbose=verbose, ), file=output_file, ) def run(args, output_file=sys.stdout): """ Main entry point for OSACAs workflow. Decides whether to run an analysis or other things. :param args: arguments given from :class:`~argparse.ArgumentParser` after parsing :param output_file: Define the stream for output, defaults to :class:`sys.stdout` :type output_file: stream, optional """ if args.check_db: # Sanity check on DB verbose = True if args.verbose > 0 else False sanity_check( args.arch, verbose=verbose, internet_check=args.internet_check, output_file=output_file, ) elif "import_data" in args: # Import microbench output file into DB import_data(args.import_data, args.arch, args.file.name, output_file=output_file) elif args.insert_marker: # Try to add IACA marker insert_byte_marker(args) else: # Analyze kernel inspect(args, output_file=output_file) @lru_cache() def get_asm_parser(arch) -> BaseParser: """ Helper function to create the right parser for a specific architecture. :param arch: architecture code :type arch: str :returns: :class:`~osaca.parser.BaseParser` object """ isa = MachineModel.get_isa_for_arch(arch) if isa == "x86": return ParserX86ATT() elif isa == "aarch64": return ParserAArch64() def get_unmatched_instruction_ratio(kernel): """Return ratio of unmatched from total instructions in kernel.""" unmatched_counter = 0 for instruction in kernel: if ( INSTR_FLAGS.TP_UNKWN in instruction["flags"] and INSTR_FLAGS.LT_UNKWN in instruction["flags"] ): unmatched_counter += 1 return unmatched_counter / len(kernel) def get_line_range(line_str): line_str = line_str.replace(":", "-") lines = line_str.split(",") lines_int = [] for line in lines: if "-" in line: start = int(line.split("-")[0]) end = int(line.split("-")[1]) rnge = list(range(start, end + 1)) lines_int += rnge else: lines_int.append(int(line)) return lines_int def main(): """Initialize and run command line interface.""" parser = create_parser() args = parser.parse_args() check_arguments(args, parser) run(args, output_file=args.out) if __name__ == "__main__": main()