def benchmark_from_seed(self, seed: int) -> Benchmark: """Get a benchmark from a uint32 seed. :param seed: A number in the range 0 <= n < 2^32. :return: A benchmark instance. """ self.install() # Run llvm-stress with the given seed and pipe the output to llvm-as to # assemble a bitcode. try: with Popen( [str(llvm.llvm_stress_path()), f"--seed={seed}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) as llvm_stress: with Popen( [str(llvm.llvm_as_path()), "-"], stdin=llvm_stress.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) as llvm_as: stdout, _ = llvm_as.communicate(timeout=60) llvm_stress.communicate(timeout=60) if llvm_stress.returncode or llvm_as.returncode: raise BenchmarkInitError( "Failed to generate benchmark") except subprocess.TimeoutExpired: raise BenchmarkInitError("Benchmark generation timed out") return Benchmark.from_file_contents(f"{self.name}/{seed}", stdout)
def benchmark_from_parsed_uri(self, uri: BenchmarkUri) -> Benchmark: self.install() benchmark_name = uri.path[1:] if not benchmark_name: raise LookupError(f"No benchmark specified: {uri}") # The absolute path of the file, without an extension. path_stem = os.path.normpath(f"{self.dataset_root}/{uri.path}") bc_path, cl_path = Path(f"{path_stem}.bc"), Path(f"{path_stem}.cl") # If the file does not exist, compile it on-demand. if not bc_path.is_file(): if not cl_path.is_file(): raise LookupError( f"Benchmark not found: {uri} (file not found: {cl_path}, path_stem {path_stem})" ) # Compile the OpenCL kernel into a bitcode file. with atomic_file_write(bc_path) as tmp_bc_path: compile_command: List[str] = ClangInvocation.from_c_file( cl_path, copt=[ "-isystem", str(self.libclc_dir), "-include", str(self.opencl_h_path), "-target", "nvptx64-nvidia-nvcl", "-ferror-limit=1", # Stop on first error. "-w", # No warnings. ], ).command(outpath=tmp_bc_path) logger.debug("Exec %s", compile_command) try: with Popen( compile_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) as clang: _, stderr = communicate(clang, timeout=300) except subprocess.TimeoutExpired: raise BenchmarkInitError( f"Benchmark compilation timed out: {uri}") if clang.returncode: compile_command = " ".join(compile_command) error = truncate(stderr.decode("utf-8"), max_lines=20, max_line_len=20000) raise BenchmarkInitError(f"Compilation job failed!\n" f"Command: {compile_command}\n" f"Error: {error}") return BenchmarkWithSource.create(uri, bc_path, "kernel.cl", cl_path)
def reset(self, *args, **kwargs): try: observation = super().reset(*args, **kwargs) except ValueError as e: # Catch and re-raise some known benchmark initialization errors with # a more informative error type. if "Failed to compute .text size cost" in str(e): raise BenchmarkInitError( f"Failed to initialize benchmark {self._benchmark_in_use.uri}: {e}" ) from e elif ("File not found:" in str(e) or "File is empty:" in str(e) or "Error reading file:" in str(e)): raise BenchmarkInitError(str(e)) from e raise # Resend the runtimes-per-observation session parameter, if it is a # non-default value. if self._runtimes_per_observation_count is not None: self.runtime_observation_count = self._runtimes_per_observation_count if self._runtimes_warmup_per_observation_count is not None: self.runtime_warmup_runs_count = self._runtimes_warmup_per_observation_count return observation
def reset(self, benchmark, observation_view) -> None: # If we are changing the benchmark then check that it is runnable. if benchmark != self.current_benchmark: if not observation_view["IsRunnable"]: raise BenchmarkInitError( f"Benchmark is not runnable: {benchmark}") self.current_benchmark = benchmark self.starting_runtime = None # Compute initial runtime if required, else use previously computed # value. if self.starting_runtime is None: self.starting_runtime = self.estimator(observation_view["Runtime"]) self.previous_runtime = self.starting_runtime
def make_benchmark_from_command_line( self, cmd: Union[str, List[str]], replace_driver: bool = True, system_includes: bool = True, timeout: int = 600, ) -> Benchmark: """Create a benchmark for use with this environment. This function takes a command line compiler invocation as input, modifies it to produce an unoptimized LLVM-IR bitcode, and then runs the modified command line to produce a bitcode benchmark. For example, the command line: >>> benchmark = env.make_benchmark_from_command_line( ... ["gcc", "-DNDEBUG", "a.c", "b.c", "-o", "foo", "-lm"] ... ) Will compile a.c and b.c to an unoptimized benchmark that can be then passed to :meth:`reset() <compiler_env.envs.CompilerEnv.reset>`. The way this works is to change the first argument of the command line invocation to the version of clang shipped with CompilerGym, and to then append command line flags that causes the compiler to produce LLVM-IR with optimizations disabled. For example the input command line: .. code-block:: gcc -DNDEBUG a.c b.c -o foo -lm Will be rewritten to be roughly equivalent to: .. code-block:: /path/to/compiler_gym/clang -DNDEG a.c b.c \\ -Xclang -disable-llvm-passes -Xclang -disable-llvm-optzns \\ -c -emit-llvm -o - The generated benchmark then has a method :meth:`compile() <compiler_env.envs.llvm.BenchmarkFromCommandLine.compile>` which completes the linking and compilatilion to executable. For the above example, this would be roughly equivalent to: .. code-block:: /path/to/compiler_gym/clang environment-bitcode.bc -o foo -lm :param cmd: A command line compiler invocation, either as a list of arguments (e.g. :code:`["clang", "in.c"]`) or as a single shell string (e.g. :code:`"clang in.c"`). :param replace_driver: Whether to replace the first argument of the command with the clang driver used by this environment. :param system_includes: Whether to include the system standard libraries during compilation jobs. This requires a system toolchain. See :func:`get_system_library_flags`. :param timeout: The maximum number of seconds to allow the compilation job to run before terminating. :return: A :class:`BenchmarkFromCommandLine <compiler_gym.envs.llvm.BenchmarkFromCommandLine>` instance. :raises ValueError: If no command line is provided. :raises BenchmarkInitError: If executing the command line fails. :raises TimeoutExpired: If a compilation job exceeds :code:`timeout` seconds. """ if not cmd: raise ValueError("Input command line is empty") # Split the command line if passed a single string. if isinstance(cmd, str): cmd = shlex.split(cmd) rewritten_cmd: List[str] = cmd.copy() if len(cmd) < 2: raise ValueError( f"Input command line '{join_cmd(cmd)}' is too short") # Append include flags for the system headers if requested. if system_includes: rewritten_cmd += get_system_library_flags() # Use the CompilerGym clang binary in place of the original driver. if replace_driver: rewritten_cmd[0] = str(clang_path()) # Strip the -S flag, if present, as that changes the output format. rewritten_cmd = [c for c in rewritten_cmd if c != "-S"] invocation = GccInvocation(rewritten_cmd) # Strip the output specifier(s). This is not strictly required since we # override it later, but makes the generated command easier to # understand. for i in range(len(rewritten_cmd) - 2, -1, -1): if rewritten_cmd[i] == "-o": del rewritten_cmd[i + 1] del rewritten_cmd[i] # Fail early. if "-" in invocation.sources: raise ValueError("Input command line reads from stdin, " f"which is not supported: '{join_cmd(cmd)}'") # Convert all of the C/C++ sources to bitcodes which can then be linked # into a single bitcode. We must process them individually because the # '-c' flag does not support multiple sources when we are specifying the # output path using '-o'. sources = set(s for s in invocation.sources if not s.endswith(".o")) if not sources: raise ValueError( f"Input command line has no source file inputs: '{join_cmd(cmd)}'" ) bitcodes: List[bytes] = [] for source in sources: # Adapt and execute the command line so that it will generate an # unoptimized bitecode file. emit_bitcode_command = rewritten_cmd.copy() # Strip the name of other sources: if len(sources) > 1: emit_bitcode_command = [ c for c in emit_bitcode_command if c == source or c not in sources ] # Append the flags to emit the bitcode and disable the optimization # passes. emit_bitcode_command += [ "-c", "-emit-llvm", "-o", "-", "-Xclang", "-disable-llvm-passes", "-Xclang", "-disable-llvm-optzns", ] with Popen(emit_bitcode_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as clang: logger.debug( f"Generating LLVM bitcode benchmark: {join_cmd(emit_bitcode_command)}" ) bitcode, stderr = clang.communicate(timeout=timeout) if clang.returncode: raise BenchmarkInitError( f"Failed to generate LLVM bitcode with error:\n" f"{stderr.decode('utf-8').rstrip()}\n" f"Running command: {join_cmd(emit_bitcode_command)}\n" f"From original commandline: {join_cmd(cmd)}") bitcodes.append(bitcode) # If there were multiple sources then link the bitcodes together. if len(bitcodes) > 1: with TemporaryDirectory(dir=transient_cache_path("."), prefix="llvm-benchmark-") as dir: # Write the bitcodes to files. for i, bitcode in enumerate(bitcodes): with open(os.path.join(dir, f"{i}.bc"), "wb") as f: f.write(bitcode) # Link the bitcode files. llvm_link_cmd = [str(llvm_link_path()), "-o", "-"] + [ os.path.join(dir, f"{i}.bc") for i in range(len(bitcodes)) ] with Popen(llvm_link_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as llvm_link: bitcode, stderr = llvm_link.communicate(timeout=timeout) if llvm_link.returncode: raise BenchmarkInitError( f"Failed to link LLVM bitcodes with error: {stderr.decode('utf-8')}" ) return BenchmarkFromCommandLine(invocation, bitcode, timeout)
def make_benchmark( inputs: Union[str, Path, ClangInvocation, List[Union[str, Path, ClangInvocation]]], copt: Optional[List[str]] = None, system_includes: bool = True, timeout: int = 600, ) -> Benchmark: """Create a benchmark for use by LLVM environments. This function takes one or more inputs and uses them to create an LLVM bitcode benchmark that can be passed to :meth:`compiler_gym.envs.LlvmEnv.reset`. The following input types are supported: +-----------------------------------------------------+---------------------+-------------------------------------------------------------+ | **File Suffix** | **Treated as** | **Converted using** | +-----------------------------------------------------+---------------------+-------------------------------------------------------------+ | :code:`.bc` | LLVM IR bitcode | No conversion required. | +-----------------------------------------------------+---------------------+-------------------------------------------------------------+ | :code:`.ll` | LLVM IR text format | Assembled to bitcode using llvm-as. | +-----------------------------------------------------+---------------------+-------------------------------------------------------------+ | :code:`.c`, :code:`.cc`, :code:`.cpp`, :code:`.cxx` | C / C++ source | Compiled to bitcode using clang and the given :code:`copt`. | +-----------------------------------------------------+---------------------+-------------------------------------------------------------+ .. note:: The LLVM IR format has no compatability guarantees between versions (see `LLVM docs <https://llvm.org/docs/DeveloperPolicy.html#ir-backwards-compatibility>`_). You must ensure that any :code:`.bc` and :code:`.ll` files are compatible with the LLVM version used by CompilerGym, which can be reported using :func:`env.compiler_version <compiler_gym.envs.CompilerEnv.compiler_version>`. E.g. for single-source C/C++ programs, you can pass the path of the source file: >>> benchmark = make_benchmark('my_app.c') >>> env = gym.make("llvm-v0") >>> env.reset(benchmark=benchmark) The clang invocation used is roughly equivalent to: .. code-block:: $ clang my_app.c -O0 -c -emit-llvm -o benchmark.bc Additional compile-time arguments to clang can be provided using the :code:`copt` argument: >>> benchmark = make_benchmark('/path/to/my_app.cpp', copt=['-O2']) If you need more fine-grained control over the options, you can directly construct a :class:`ClangInvocation <compiler_gym.envs.llvm.ClangInvocation>` to pass a list of arguments to clang: >>> benchmark = make_benchmark( ClangInvocation(['/path/to/my_app.c'], system_includes=False, timeout=10) ) For multi-file programs, pass a list of inputs that will be compiled separately and then linked to a single module: >>> benchmark = make_benchmark([ 'main.c', 'lib.cpp', 'lib2.bc', 'foo/input.bc' ]) :param inputs: An input, or list of inputs. :param copt: A list of command line options to pass to clang when compiling source files. :param system_includes: Whether to include the system standard libraries during compilation jobs. This requires a system toolchain. See :func:`get_system_library_flags`. :param timeout: The maximum number of seconds to allow clang to run before terminating. :return: A :code:`Benchmark` instance. :raises FileNotFoundError: If any input sources are not found. :raises TypeError: If the inputs are of unsupported types. :raises OSError: If a suitable compiler cannot be found. :raises BenchmarkInitError: If a compilation job fails. :raises TimeoutExpired: If a compilation job exceeds :code:`timeout` seconds. """ copt = copt or [] bitcodes: List[Path] = [] clang_jobs: List[ClangInvocation] = [] ll_paths: List[Path] = [] def _add_path(path: Path): if not path.is_file(): raise FileNotFoundError(path) if path.suffix == ".bc": bitcodes.append(path.absolute()) elif path.suffix in {".c", ".cc", ".cpp", ".cxx"}: clang_jobs.append( ClangInvocation.from_c_file( path, copt=copt, system_includes=system_includes, timeout=timeout ) ) elif path.suffix == ".ll": ll_paths.append(path) else: raise ValueError(f"Unrecognized file type: {path.name}") # Determine from inputs the list of pre-compiled bitcodes and the clang # invocations required to compile the bitcodes. if isinstance(inputs, str) or isinstance(inputs, Path): _add_path(Path(inputs)) elif isinstance(inputs, ClangInvocation): clang_jobs.append(inputs) else: for input in inputs: if isinstance(input, str) or isinstance(input, Path): _add_path(Path(input)) elif isinstance(input, ClangInvocation): clang_jobs.append(input) else: raise TypeError(f"Invalid input type: {type(input).__name__}") # Shortcut if we only have a single pre-compiled bitcode. if len(bitcodes) == 1 and not clang_jobs and not ll_paths: bitcode = bitcodes[0] return Benchmark.from_file(uri=f"benchmark://file-v0{bitcode}", path=bitcode) tmpdir_root = transient_cache_path(".") tmpdir_root.mkdir(exist_ok=True, parents=True) with tempfile.TemporaryDirectory( dir=tmpdir_root, prefix="llvm-make_benchmark-" ) as d: working_dir = Path(d) clang_outs = [ working_dir / f"clang-out-{i}.bc" for i in range(1, len(clang_jobs) + 1) ] llvm_as_outs = [ working_dir / f"llvm-as-out-{i}.bc" for i in range(1, len(ll_paths) + 1) ] # Run the clang and llvm-as invocations in parallel. Avoid running this # code path if possible as get_thread_pool_executor() requires locking. if clang_jobs or ll_paths: llvm_as_path = str(llvm.llvm_as_path()) executor = get_thread_pool_executor() llvm_as_commands = [ [llvm_as_path, str(ll_path), "-o", bc_path] for ll_path, bc_path in zip(ll_paths, llvm_as_outs) ] # Fire off the clang and llvm-as jobs. futures = [ executor.submit(run_command, job.command(out), job.timeout) for job, out in zip(clang_jobs, clang_outs) ] + [ executor.submit(run_command, command, timeout) for command in llvm_as_commands ] # Block until finished. list(future.result() for future in as_completed(futures)) # Check that the expected files were generated. for clang_job, bc_path in zip(clang_jobs, clang_outs): if not bc_path.is_file(): raise BenchmarkInitError( f"clang failed: {' '.join(clang_job.command(bc_path))}" ) for command, bc_path in zip(llvm_as_commands, llvm_as_outs): if not bc_path.is_file(): raise BenchmarkInitError(f"llvm-as failed: {command}") all_outs = bitcodes + clang_outs + llvm_as_outs if not all_outs: raise ValueError("No inputs") elif len(all_outs) == 1: # We only have a single bitcode so read it. with open(str(all_outs[0]), "rb") as f: bitcode = f.read() else: # Link all of the bitcodes into a single module. llvm_link_cmd = [str(llvm.llvm_link_path()), "-o", "-"] + [ str(path) for path in bitcodes + clang_outs ] with Popen( llvm_link_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) as llvm_link: bitcode, stderr = llvm_link.communicate(timeout=timeout) if llvm_link.returncode: raise BenchmarkInitError( f"Failed to link LLVM bitcodes with error: {stderr.decode('utf-8')}" ) timestamp = datetime.now().strftime("%Y%m%HT%H%M%S") uri = f"benchmark://user-v0/{timestamp}-{random.randrange(16**4):04x}" return Benchmark.from_file_contents(uri, bitcode)
def benchmark_from_parsed_uri(self, uri: BenchmarkUri) -> Benchmark: self.install() # The absolute path of the file, without an extension. path_stem = os.path.normpath(f"{self.dataset_root}/{uri.path}") # If the file does not exist, compile it on-demand. bitcode_path = Path(f"{path_stem}.bc") cc_file_path = Path(f"{path_stem}.txt") if not bitcode_path.is_file(): if not cc_file_path.is_file(): raise LookupError( f"Benchmark not found: {uri} (file not found: {cc_file_path})" ) # Load the C++ source into memory and pre-process it. with open(cc_file_path) as f: src = self.preprocess_poj104_source(f.read()) # Compile the C++ source into a bitcode file. with atomic_file_write(bitcode_path) as tmp_bitcode_path: compile_cmd = ClangInvocation.from_c_file( "-", copt=[ "-xc++", "-ferror-limit=1", # Stop on first error. "-w", # No warnings. # Some of the programs use the gets() function that was # deprecated in C++11 and removed in C++14. "-std=c++11", ], ).command(outpath=tmp_bitcode_path) logger.debug("Exec %s", compile_cmd) try: with Popen( compile_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) as clang: _, stderr = clang.communicate( input=src.encode("utf-8"), timeout=300 ) except subprocess.TimeoutExpired: raise BenchmarkInitError(f"Benchmark compilation timed out: {uri}") if clang.returncode: compile_cmd = " ".join(compile_cmd) error = truncate(stderr.decode("utf-8"), max_lines=20, max_line_len=100) if tmp_bitcode_path.is_file(): tmp_bitcode_path.unlink() raise BenchmarkInitError( f"Compilation job failed!\n" f"Command: {compile_cmd}\n" f"Error: {error}" ) if not bitcode_path.is_file(): raise BenchmarkInitError( f"Compilation job failed to produce output file!\nCommand: {compile_cmd}" ) return BenchmarkWithSource.create(uri, bitcode_path, "source.cc", cc_file_path)
def benchmark_from_seed(self, seed: int, max_retries: int = 3, retry_count: int = 0) -> CsmithBenchmark: """Get a benchmark from a uint32 seed. :param seed: A number in the range 0 <= n < 2^32. :return: A benchmark instance. :raises OSError: If Csmith fails. :raises BenchmarkInitError: If the C program generated by Csmith cannot be lowered to LLVM-IR. """ if retry_count >= max_retries: raise OSError( f"Csmith failed after {retry_count} {plural(retry_count, 'attempt', 'attempts')} " f"with seed {seed}") self.install() # Run csmith with the given seed and pipe the output to clang to # assemble a bitcode. logger.debug("Exec csmith --seed %d", seed) try: with Popen( [str(self.csmith_bin_path), "--seed", str(seed)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) as csmith: # Generate the C source. src, stderr = communicate(csmith, timeout=300) if csmith.returncode: try: stderr = "\n".join( truncate(stderr.decode("utf-8"), max_line_len=200, max_lines=20)) logger.warning("Csmith failed with seed %d: %s", seed, stderr) except UnicodeDecodeError: # Failed to interpret the stderr output, generate a generic # error message. logger.warning("Csmith failed with seed %d", seed) return self.benchmark_from_seed(seed, max_retries=max_retries, retry_count=retry_count + 1) # Compile to IR. with Popen( self.clang_compile_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ) as clang: stdout, _ = communicate(clang, input=src, timeout=300) if clang.returncode: compile_cmd = " ".join(self.clang_compile_command) raise BenchmarkInitError(f"Compilation job failed!\n" f"Csmith seed: {seed}\n" f"Command: {compile_cmd}\n") except subprocess.TimeoutExpired: raise BenchmarkInitError( f"Benchmark generation using seed {seed} timed out") return self.benchmark_class.create(f"{self.name}/{seed}", stdout, src)