def __init__( self, working_directory: Path, action_space: ActionSpace, benchmark: Benchmark, use_custom_opt: bool = True, ): super().__init__(working_directory, action_space, benchmark) logging.info("Started a compilation session for %s", benchmark.uri) self._benchmark = benchmark self._action_space = action_space self.inst2vec = _INST2VEC_ENCODER # Resolve the paths to LLVM binaries once now. self._clang = str(llvm.clang_path()) self._llc = str(llvm.llc_path()) self._llvm_diff = str(llvm.llvm_diff_path()) self._opt = str(llvm.opt_path()) # LLVM's opt does not always enforce the loop optimization options passed as cli arguments. # Hence, we created our own exeutable with custom unrolling and vectorization pass in examples/loops_opt_service/opt_loops that enforces the unrolling and vectorization factors passed in its cli. # if self._use_custom_opt is true, use our custom exeutable, otherwise use LLVM's opt self._use_custom_opt = use_custom_opt # Dump the benchmark source to disk. self._src_path = str(self.working_dir / "benchmark.c") with open(self.working_dir / "benchmark.c", "wb") as f: f.write(benchmark.program.contents) self._llvm_path = str(self.working_dir / "benchmark.ll") self._llvm_before_path = str(self.working_dir / "benchmark.previous.ll") self._obj_path = str(self.working_dir / "benchmark.o") self._exe_path = str(self.working_dir / "benchmark.exe") run_command( [ self._clang, "-Xclang", "-disable-O0-optnone", "-emit-llvm", "-S", self._src_path, "-o", self._llvm_path, ], timeout=30, )
def get_observation(self, observation_space: ObservationSpace) -> Observation: logging.info("Computing observation from space %s", observation_space.name) if observation_space.name == "ir": return Observation(string_value=self.ir) elif observation_space.name == "features": stats = utils.extract_statistics_from_ir(self.ir) observation = Observation() observation.int64_list.value[:] = list(stats.values()) return observation elif observation_space.name == "runtime": # compile LLVM to object file run_command( [ self._llc, "-filetype=obj", self._llvm_path, "-o", self._obj_path, ], timeout=30, ) # build object file to binary run_command( [ "clang", self._obj_path, "-O3", "-o", self._exe_path, ], timeout=30, ) # TODO: add documentation that benchmarks need print out execution time # Running 5 times and taking the average of middle 3 exec_times = [] for _ in range(5): stdout = run_command( [self._exe_path], timeout=30, ) try: exec_times.append(int(stdout)) except ValueError: raise ValueError( f"Error in parsing execution time from output of command\n" f"Please ensure that the source code of the benchmark measures execution time and prints to stdout\n" f"Stdout of the program: {stdout}") exec_times = np.sort(exec_times) avg_exec_time = np.mean(exec_times[1:4]) return Observation(scalar_double=avg_exec_time) elif observation_space.name == "size": # compile LLVM to object file run_command( [ self._llc, "-filetype=obj", self._llvm_path, "-o", self._obj_path, ], timeout=30, ) # build object file to binary run_command( [ "clang", self._obj_path, "-Oz", "-o", self._exe_path, ], timeout=30, ) binary_size = os.path.getsize(self._exe_path) return Observation(scalar_double=binary_size) else: raise KeyError(observation_space.name)
def apply_action( self, action: Action) -> Tuple[bool, Optional[ActionSpace], bool]: num_choices = len( self._action_space.choice[0].named_discrete_space.value) if len(action.choice) != 1: raise ValueError("Invalid choice count") # This is the index into the action space's values ("a", "b", "c") that # the user selected, e.g. 0 -> "a", 1 -> "b", 2 -> "c". choice_index = action.choice[0].named_discrete_value_index if choice_index < 0 or choice_index >= num_choices: raise ValueError("Out-of-range") args = self._action_space.choice[0].named_discrete_space.value[ choice_index] logging.info( "Applying action %d, equivalent command-line arguments: '%s'", choice_index, args, ) args = args.split() # make a copy of the LLVM file to compare its contents after applying the action shutil.copyfile(self._llvm_path, self._llvm_before_path) # apply action if self._use_custom_opt: # our custom unroller has an additional `f` at the beginning of each argument for i, arg in enumerate(args): # convert -<argument> to -f<argument> arg = arg[0] + "f" + arg[1:] args[i] = arg run_command( [ "../loop_unroller/loop_unroller", self._llvm_path, *args, "-S", "-o", self._llvm_path, ], timeout=30, ) else: run_command( [ self._opt, *args, self._llvm_path, "-S", "-o", self._llvm_path, ], timeout=30, ) # compare the IR files to check if the action had an effect try: subprocess.check_call( [self._llvm_diff, self._llvm_before_path, self._llvm_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60, ) action_had_no_effect = True except subprocess.CalledProcessError: action_had_no_effect = False end_of_session = False # TODO: this needs investigation: for how long can we apply loop unrolling? e.g., detect if there are no more loops in the IR? new_action_space = None return (end_of_session, new_action_space, action_had_no_effect)
def get_observation(self, observation_space: ObservationSpace) -> Event: logging.info("Computing observation from space %s", observation_space.name) if observation_space.name == "ir": return Event(string_value=self.ir) elif observation_space.name == "Inst2vec": Inst2vec_str = self.inst2vec.preprocess(self.ir) Inst2vec_ids = self.inst2vec.encode(Inst2vec_str) return Event(int64_tensor=Int64Tensor(shape=[len(Inst2vec_ids)], value=Inst2vec_ids)) elif observation_space.name == "Autophase": Autophase_str = run_command( [ os.path.join( os.path.dirname(__file__), "../../../compiler_gym/third_party/autophase/compute_autophase-prelinked", ), self._llvm_path, ], timeout=30, ) Autophase_list = list(map(int, list(Autophase_str.split(" ")))) return Event(int64_tensor=Int64Tensor(shape=[len(Autophase_list)], value=Autophase_list)) elif observation_space.name == "AutophaseDict": Autophase_str = run_command( [ os.path.join( os.path.dirname(__file__), "../../../compiler_gym/third_party/autophase/compute_autophase-prelinked", ), self._llvm_path, ], timeout=30, ) Autophase_list = list(map(int, list(Autophase_str.split(" ")))) Autophase_dict = { name: Event(int64_value=val) for name, val in zip(AUTOPHASE_FEATURE_NAMES, Autophase_list) } return Event(event_dict=DictEvent(event=Autophase_dict)) elif observation_space.name == "Programl": Programl_str = run_command( [ os.path.join( os.path.dirname(__file__), "../../../compiler_gym/third_party/programl/compute_programl", ), self._llvm_path, ], timeout=30, ) return Event(string_value=Programl_str) elif observation_space.name == "runtime": # compile LLVM to object file run_command( [ self._llc, "-filetype=obj", self._llvm_path, "-o", self._obj_path, ], timeout=30, ) # build object file to binary run_command( [ self._clang, self._obj_path, "-O3", "-o", self._exe_path, ], timeout=30, ) # TODO: add documentation that benchmarks need print out execution time # Running 5 times and taking the average of middle 3 exec_times = [] for _ in range(5): stdout = run_command( [self._exe_path], timeout=30, ) try: exec_times.append(int(stdout)) except ValueError: raise ValueError( f"Error in parsing execution time from output of command\n" f"Please ensure that the source code of the benchmark measures execution time and prints to stdout\n" f"Stdout of the program: {stdout}") exec_times = np.sort(exec_times) avg_exec_time = np.mean(exec_times[1:4]) return Event(double_value=avg_exec_time) elif observation_space.name == "size": # compile LLVM to object file run_command( [ self._llc, "-filetype=obj", self._llvm_path, "-o", self._obj_path, ], timeout=30, ) # build object file to binary run_command( [ self._clang, self._obj_path, "-Oz", "-o", self._exe_path, ], timeout=30, ) binary_size = os.path.getsize(self._exe_path) return Event(double_value=binary_size) else: raise KeyError(observation_space.name)