Exemplo n.º 1
0
def test_add_benchmark_invalid_path(env: CompilerEnv):
    with tempfile.TemporaryDirectory() as d:
        tmp = Path(d) / "not_a_file"
        with pytest.raises(FileNotFoundError) as ctx:
            env.reset(benchmark=Benchmark.from_file("benchmark://foo", tmp))
        # Use endswith() because on macOS there may be a /private prefix.
        assert str(ctx.value).endswith(str(tmp))
Exemplo n.º 2
0
def test_benchmark_path_empty_file(env: LlvmEnv):
    with tempfile.TemporaryDirectory() as tmpdir:
        tmpdir = Path(tmpdir)
        (tmpdir / "test.bc").touch()

        benchmark = Benchmark.from_file("benchmark://new", tmpdir / "test.bc")

        with pytest.raises(ValueError, match="Failed to parse LLVM bitcode"):
            env.reset(benchmark=benchmark)
Exemplo n.º 3
0
def test_invalid_benchmark_path_contents(env: LlvmEnv):
    with tempfile.TemporaryDirectory() as tmpdir:
        tmpdir = Path(tmpdir)
        with open(str(tmpdir / "test.bc"), "w") as f:
            f.write("Invalid bitcode")

        benchmark = Benchmark.from_file("benchmark://new", tmpdir / "test.bc")

        with pytest.raises(ValueError, match="Failed to parse LLVM bitcode"):
            env.reset(benchmark=benchmark)
def benchmark_from_flags() -> Optional[Union[Benchmark, str]]:
    """Returns either the name of the benchmark, or a Benchmark message."""
    if FLAGS.benchmark:
        if FLAGS.benchmark.startswith("file:///"):
            path = Path(FLAGS.benchmark[len("file:///") :])
            uri = f"benchmark://user-v0/{path}"
            return Benchmark.from_file(uri=uri, path=path)
        else:
            return FLAGS.benchmark
    else:
        # No benchmark was specified.
        return None
Exemplo n.º 5
0
    def benchmark(self, uri: str) -> Benchmark:
        self.install()

        benchmark_name = uri[len(self.name) + 1:]
        if not benchmark_name:
            raise LookupError(f"No benchmark specified: {uri}")

        # Most of the source files are named after the parent directory, but not
        # all.
        c_file_name = {
            "blowfish": "bf.c",
            "motion": "mpeg2.c",
            "sha": "sha_driver.c",
            "jpeg": "main.c",
        }.get(benchmark_name, f"{benchmark_name}.c")
        source_dir_path = self.dataset_root / benchmark_name
        source_path = source_dir_path / c_file_name
        preprocessed_path = source_dir_path / "src.c"

        # If the file does not exist, preprocess it on-demand.
        if not preprocessed_path.is_file():
            if not source_path.is_file():
                raise LookupError(
                    f"Benchmark not found: {uri} (file not found: {source_path})"
                )

            with atomic_file_write(preprocessed_path) as tmp_path:
                # TODO(github.com/facebookresearch/CompilerGym/issues/325): Send
                # over the unprocessed code to the service, have the service
                # preprocess. Until then, we do it client side with GCC having
                # to fixed by an environment variable
                self.gcc(
                    "-E",
                    "-o",
                    tmp_path.name,
                    c_file_name,
                    cwd=source_dir_path,
                    timeout=300,
                )

        return Benchmark.from_file(uri, preprocessed_path)
Exemplo n.º 6
0
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)
Exemplo n.º 7
0
def test_benchmark_from_file_not_found(tmpwd: Path):
    path = tmpwd / "foo.txt"
    with pytest.raises(FileNotFoundError, match=str(path)):
        Benchmark.from_file("benchmark://example-v0/foo", path)
Exemplo n.º 8
0
def test_benchmark_from_file(tmpwd: Path):
    path = tmpwd / "foo.txt"
    with open(path, "w") as f:
        f.write("Hello, world!")
    benchmark = Benchmark.from_file("benchmark://example-v0/foo", path)
    assert benchmark.proto.program.contents.decode("utf-8") == "Hello, world!"
Exemplo n.º 9
0
def test_custom_benchmark(env: LlvmEnv):
    benchmark = Benchmark.from_file("benchmark://new", EXAMPLE_BITCODE_FILE)
    env.reset(benchmark=benchmark)
    assert env.benchmark == "benchmark://new"
Exemplo n.º 10
0
def test_custom_benchmark_constructor():
    benchmark = Benchmark.from_file("benchmark://new", EXAMPLE_BITCODE_FILE)
    with gym.make("llvm-v0", benchmark=benchmark) as env:
        env.reset()
        assert env.benchmark == "benchmark://new"