// Copyright 2019 The IREE Authors // // Licensed under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // IREE source.mlir/mlirbc -> execution output runner. // This is meant to be called from LIT for FileCheck tests or as a developer // tool to emulate what an online compiler does. It tries to match the interface // of iree-compile so it's easy to run tests or approximate an // `iree-compile | iree-run-module` sequence. If you want a more generalized // runner for standalone precompiled IREE modules use iree-run-module instead. // // If there's a single exported function that will be executed and if there are // multiple functions --function= can be used to specify which is executed. // Function inputs can be provided with --input=. Results from the executed // function will be printed to stdout for checking or can be written to files // with --output=. // // Similar to iree-run-module the --device= flag can be used to specify which // drivers and devices should be used to execute the function. The tool will // try to infer which iree-compile flags are required for the devices used but // this can be overridden by passing the --iree-hal-target-backends= and related // flags explicitly. Likewise if only the target backend is specified the // devices to use will be inferred unless explicitly specified. // // Example usage to compile and run with CUDA: // $ iree-run-mlir --device=cuda://0 file.mlir // or to compile with the LLVM CPU backend and run with the local-task driver: // $ iree-run-mlir file.mlir \ // --Xcompiler,iree-hal-target-backends=llvm-cpu --device=local-task // // Example usage in a lit test: // // RUN: iree-run-mlir --device= %s --function=foo --input=2xf32=2,3 | \ // // RUN: FileCheck %s // // CHECK-LABEL: @foo // // CHECK: 2xf32=[2 3] // func.func @foo(%arg0: tensor<2xf32>) -> tensor<2xf32> { // return %arg0 : tensor<2xf32> // } // // Command line arguments are handled by LLVM's parser by default but -- can be // used to separate the compiler flags from the runtime flags, such as: // $ iree-run-mlir source.mlir --device=local-task -- \ // --iree-hal-target-backends=llvm-cpu // // In addition compiler/runtime flags can be passed in any order by prefixing // them with --Xcompiler or --Xruntime like `--Xruntime,device=local-task` or // `--Xruntime --device=local-task`. #include #include #include #include #include #include #include #include #include #include #include #include #include "iree/base/api.h" #include "iree/base/internal/flags.h" #include "iree/compiler/embedding_api.h" #include "iree/hal/api.h" #include "iree/tooling/context_util.h" #include "iree/tooling/device_util.h" #include "iree/tooling/run_module.h" #include "iree/vm/api.h" namespace iree { namespace { // Polyfill for std::string_view::starts_with, coming in C++ 20. // https://en.cppreference.com/w/cpp/string/basic_string_view/starts_with bool starts_with(std::string_view prefix, std::string_view in_str) { return in_str.size() >= prefix.size() && in_str.compare(0, prefix.size(), prefix) == 0; } // Tries to guess a default device name from the |target_backend| when possible. // Users are still able to override this by passing in --device= flags. std::string InferDefaultDeviceFromTargetBackend( std::string_view target_backend) { if (target_backend == "" || target_backend == "vmvx-inline") { // Plain VM or vmvx-inline targets do not need a HAL device. return ""; } else if (target_backend == "llvm-cpu" || target_backend == "vmvx") { // Locally-executable targets default to the multithreaded task system // driver; users can override by specifying --device=local-sync instead. return "local-task"; } // Many other backends use the `driver-pipeline` naming like `vulkan-spirv` // and we try that; device creation will fail if it's a bad guess. size_t dash = target_backend.find('-'); if (dash == std::string::npos) { return std::string(target_backend); } else { return std::string(target_backend.substr(0, dash)); } } // Tries to guess a target backend from the given |device_uri| when possible. // Returns empty string if no backend is required or one could not be inferred. std::string InferTargetBackendFromDevice(iree_string_view_t device_uri) { // Get the driver name from URIs in the `driver://...` form. iree_string_view_t driver = iree_string_view_empty(); iree_string_view_split(device_uri, ':', &driver, nullptr); if (iree_string_view_is_empty(driver)) { // Plain VM or vmvx-inline targets do not need a HAL device. return ""; } else if (iree_string_view_starts_with(driver, IREE_SV("local-"))) { // Locally-executable devices default to the llvm-cpu target as that's // usually what people want for CPU execution; users can override by // specifying --iree-hal-target-backends=vmvx instead. return "llvm-cpu"; } // Many other backends have aliases that allow using the driver name. If there // are multiple pipelines available whatever the compiler defaults to is // chosen. return std::string(driver.data, driver.size); } // Tries to guess a set of target backends from the |device_flag_values| when // possible. Since multiple target backends can be used for a particular device // (such as llvm-cpu or vmvx for local-sync and local-task) this is just // guesswork. If we can't produce a target backend flag value we bail. // Returns a comma-delimited list of target backends. StatusOr InferTargetBackendsFromDevices( iree_string_view_list_t device_uris) { // No-op when no devices specified (probably no HAL). if (device_uris.count == 0) return ""; // If multiple devices were provided we need to target all of them. std::set target_backends; for (iree_host_size_t i = 0; i < device_uris.count; ++i) { auto target_backend = InferTargetBackendFromDevice(device_uris.values[i]); if (!target_backend.empty()) { target_backends.insert(std::move(target_backend)); } } // Join all target backends together. std::string result; for (auto& target_backend : target_backends) { if (!result.empty()) result.append(","); result.append(target_backend); } return result; } // Configures the --iree-hal-target-backends= flag based on the --device= flags // set by the user. Ignored if any target backends are explicitly specified. // Online compilers would want to do some more intelligent device selection on // their own. Status ConfigureTargetBackends(iree_compiler_session_t* session, std::string* out_default_device_uri) { // Query the session for the currently set --iree-hal-target-backends= flag. // It may be empty string. std::string target_backends_flag; ireeCompilerSessionGetFlags( session, /*nonDefaultOnly=*/true, [](const char* flag_str, size_t length, void* user_data) { // NOTE: flag_str has the full `--flag=value` string. std::string_view prefix = "--iree-hal-target-backends="; std::string_view flag = std::string_view(flag_str, length); if (starts_with(prefix, flag)) { flag.remove_prefix(prefix.size()); if (flag.empty()) return; // ignore empty auto* result = static_cast(user_data); *result = std::string(flag); } }, static_cast(&target_backends_flag)); // Query the tooling utils for the --device= flag values. Note that zero or // more devices may be specified. iree_string_view_list_t device_uris = iree_hal_device_flag_list(); // No-op if no target backends or devices are specified - this can be an // intentional decision as the user may be running a program that doesn't use // the HAL. if (target_backends_flag.empty() && device_uris.count == 0) { return OkStatus(); } // No-op if both target backends and devices are set as the user has // explicitly specified a configuration. if (!target_backends_flag.empty() && device_uris.count > 0) { return OkStatus(); } // If target backends are specified then we can infer the runtime devices from // the compiler configuration. This only works if there's a single backend // specified; if the user wants multiple target backends then they must // specify the device(s) to use. if (device_uris.count == 0) { if (target_backends_flag.find(',') != std::string::npos) { return iree_make_status( IREE_STATUS_INVALID_ARGUMENT, "if multiple target backends are specified the device to use must " "also be specified with --device= (have " "`--iree-hal-target-backends=%.*s`)", (int)target_backends_flag.size(), target_backends_flag.data()); } *out_default_device_uri = InferDefaultDeviceFromTargetBackend(target_backends_flag); return OkStatus(); } // Infer target backends from the runtime device configuration. // This can get arbitrarily complex but for now this simple runner just // guesses. In the future we'll have more ways of configuring the compiler // from available runtime devices (not just the target backend but also // target-specific settings). IREE_ASSIGN_OR_RETURN(auto target_backends, InferTargetBackendsFromDevices(device_uris)); if (!target_backends.empty()) { auto target_backends_flag = std::string("--iree-hal-target-backends=") + target_backends; const char* compiler_argv[1] = { target_backends_flag.c_str(), }; auto error = ireeCompilerSessionSetFlags( session, IREE_ARRAYSIZE(compiler_argv), compiler_argv); if (error) { return iree_make_status( IREE_STATUS_INVALID_ARGUMENT, "unable to set inferred target backend flag to `%.*s`", (int)target_backends_flag.size(), target_backends_flag.data()); } } return OkStatus(); } // Runs the given .mlir file based on the current flags. StatusOr CompileAndRunFile(iree_compiler_session_t* session, const char* mlir_filename) { IREE_TRACE_SCOPE_NAMED("CompileAndRunFile"); // Configure the --iree-hal-target-backends= flag and/or get the default // device to use at runtime if either are not explicitly specified. // Note that target backends and the runtime devices aren't 1:1 and this is // an imperfect guess. In this simple online compiler we assume homogenous // device sets and only a single global target backend but library/hosting // layers can configure heterogenous and per-invocation target configurations. std::string default_device_uri; IREE_RETURN_IF_ERROR(ConfigureTargetBackends(session, &default_device_uri)); // RAII container for the compiler invocation. struct InvocationState { iree_compiler_invocation_t* invocation = nullptr; iree_compiler_source_t* source = nullptr; iree_compiler_output_t* output = nullptr; explicit InvocationState(iree_compiler_session_t* session) { invocation = ireeCompilerInvocationCreate(session); } ~InvocationState() { if (source) ireeCompilerSourceDestroy(source); if (output) ireeCompilerOutputDestroy(output); ireeCompilerInvocationDestroy(invocation); } Status emitError(iree_compiler_error_t* error, iree_status_code_t status_code, std::string_view while_performing = "") { const char* msg = ireeCompilerErrorGetMessage(error); fprintf(stderr, "error compiling input file: %s\n", msg); iree_status_t status = iree_make_status(status_code, msg); if (!while_performing.empty()) { status = iree_status_annotate( status, iree_make_string_view(while_performing.data(), while_performing.size())); } ireeCompilerErrorDestroy(error); return status; } } state(session); // Open the source file on disk or stdin if `-`. if (auto error = ireeCompilerSourceOpenFile(session, mlir_filename, &state.source)) { return state.emitError(error, IREE_STATUS_NOT_FOUND, "opening source file"); } // Open a writeable memory buffer that we can stream the compilation outputs // into. This may be backed by a memory-mapped file to allow for very large // results. if (auto error = ireeCompilerOutputOpenMembuffer(&state.output)) { return state.emitError(error, IREE_STATUS_INTERNAL, "open output memory buffer"); } // TODO: make parsing/pipeline execution return an error object. // We could capture diagnostics, stash them on the state, and emit with // ireeCompilerInvocationEnableCallbackDiagnostics. // For now we route all errors to stderr. ireeCompilerInvocationEnableConsoleDiagnostics(state.invocation); // Parse the source MLIR input and log verbose errors. Syntax errors or // version mismatches will hit here. if (!ireeCompilerInvocationParseSource(state.invocation, state.source)) { return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, "failed to parse input file"); } // Invoke the standard compilation pipeline to produce the compiled module. if (!ireeCompilerInvocationPipeline(state.invocation, IREE_COMPILER_PIPELINE_STD)) { return iree_make_status(IREE_STATUS_INTERNAL, "failed to invoke main compiler pipeline"); } // Flush the output to the memory buffer. if (auto error = ireeCompilerInvocationOutputVMBytecode(state.invocation, state.output)) { return state.emitError(error, IREE_STATUS_INTERNAL, "emitting output VM module binary"); } // Get a raw host pointer to the output that we can pass to the runtime. void* binary_data = nullptr; uint64_t binary_size = 0; if (auto error = ireeCompilerOutputMapMemory(state.output, &binary_data, &binary_size)) { return state.emitError(error, IREE_STATUS_INTERNAL, "mapping output buffer"); } // Hosting libraries can route all runtime allocations to their own allocator // for statistics, isolation, or efficiency. Here we use the system // malloc/free. iree_allocator_t host_allocator = iree_allocator_system(); // The same VM instance should be shared across many contexts. Here we only // use this once but a library would want to retain this and the devices it // creates for as long as practical. vm::ref instance; IREE_RETURN_IF_ERROR(iree_tooling_create_instance(host_allocator, &instance), "creating instance"); // Run the compiled module using the global flags for I/O (if any). // This loads the module, creates a VM context with it and any dependencies, // parses inputs from flags, and routes/verifies outputs as specified. Hosting // libraries should always reuse contexts if possible to amortize loading // costs and carry state (variables/etc) across invocations. // // This returns a process exit code based on the run mode (verifying expected // outputs, etc) that may be non-zero even if the status is success // ("execution completed successfully but values did not match"). int exit_code = EXIT_SUCCESS; IREE_RETURN_IF_ERROR( iree_tooling_run_module_with_data( instance.get(), iree_make_string_view(default_device_uri.data(), default_device_uri.size()), iree_make_const_byte_span(binary_data, (iree_host_size_t)binary_size), host_allocator, &exit_code), "running compiled module"); return exit_code; } // Parses a combined list of compiler and runtime flags. // Each argument list is stored in canonical argc/argv format with a trailing // NULL string in the storage (excluded from the count). class ArgParser { public: int compiler_argc() { return compiler_args_.size() - 1; } const char** compiler_argv() { return const_cast(compiler_args_.data()); } int runtime_argc() { return runtime_args_.size() - 1; } char** runtime_argv() { return runtime_args_.data(); } // Parses arguments from a raw command line argc/argv set. // Returns true if parsing was successful. bool Parse(int argc_raw, char** argv_raw) { // Pre-process the arguments with the compiler's argument parser since it // has super-powers on Windows and must work on the default main arguments. ireeCompilerGetProcessCLArgs(&argc_raw, const_cast(&argv_raw)); // Always add the progname to both flag sets. compiler_args_.push_back(argv_raw[0]); runtime_args_.push_back(argv_raw[0]); // Everything before -- goes to the runtime. // Everything after -- goes to the compiler. // To make it easier to form command lines in scripts we also allow // prefixing flags with -Xcompiler/-Xruntime on either side of the --. bool parsing_runtime_args = true; for (int i = 1; i < argc_raw; ++i) { char* current_arg_cstr = argv_raw[i]; char* next_arg_cstr = argv_raw[i + 1]; // ok because list is NULL-terminated auto current_arg = std::string_view(current_arg_cstr); if (current_arg == "--") { // Switch default parsing to compiler flags. parsing_runtime_args = false; } else if (current_arg == "-Xcompiler" || current_arg == "--Xcompiler") { // Next arg is routed to the compiler. compiler_args_.push_back(next_arg_cstr); } else if (current_arg == "-Xruntime" || current_arg == "--Xruntime") { // Next arg is routed to the runtime. runtime_args_.push_back(next_arg_cstr); } else if (starts_with("-Xcompiler,", current_arg) || starts_with("--Xcompiler,", current_arg)) { // Split and send the rest of the flag to the compiler. AppendPrefixedArg(current_arg, &compiler_args_); } else if (starts_with("-Xruntime,", current_arg) || starts_with("--Xruntime,", current_arg)) { // Split and send the rest of the flag to the runtime. AppendPrefixedArg(current_arg, &runtime_args_); } else { // Route to either runtime or compiler arg sets based on which side of // the -- we are on. if (parsing_runtime_args) { runtime_args_.push_back(current_arg_cstr); } else { compiler_args_.push_back(current_arg_cstr); } } } // Add nullptrs to end to match real argv behavior. compiler_args_.push_back(nullptr); runtime_args_.push_back(nullptr); return true; } private: // Drops the prefix from |prefixed_arg| and appends the arg to |out_args|. // Example: --Xcompiler,ab=cd,ef -> --ab=cd,ef void AppendPrefixedArg(std::string_view prefixed_arg, std::vector* out_args) { std::string_view sub_arg = prefixed_arg.substr(prefixed_arg.find(',') + 1); auto stable_arg = std::make_unique("--"); stable_arg->append(sub_arg); temp_strings_.push_back(std::move(stable_arg)); out_args->push_back(temp_strings_.back()->data()); } std::vector> temp_strings_; std::vector runtime_args_; std::vector compiler_args_; }; } // namespace extern "C" int main(int argc, char** argv) { IREE_TRACE_APP_ENTER(); IREE_TRACE_ZONE_BEGIN_NAMED(z0, "iree-run-mlir"); // Initialize the compiler once on startup before using any other APIs. ireeCompilerGlobalInitialize(); // Parse full argument list and split into compiler/runtime flag sets. ArgParser arg_parser; if (!arg_parser.Parse(argc, argv)) { ireeCompilerGlobalShutdown(); IREE_TRACE_ZONE_END(z0); IREE_TRACE_APP_EXIT(EXIT_FAILURE); return EXIT_FAILURE; } // Pass along compiler flags. // Since this is a command line tool we initialize the global compiler // command line environment prior to processing the sources. // In-process/library uses would usually not do this and would set session // specific arguments as needed from whatever configuration mechanisms they // use (kwargs passed to python functions, etc). ireeCompilerSetupGlobalCL(arg_parser.compiler_argc(), arg_parser.compiler_argv(), "iree-run-mlir", /*installSignalHandlers=*/true); // Pass along runtime flags. // Note that positional args are left in runtime_argv (after progname). // Runtime flags are generally only useful in command line tools where there's // a fixed set of devices, a short lifetime, a single thread, and a single // context/set of modules/etc. Hosting applications can programmatically // do most of what the flags do in a way that avoids the downsides of such // global one-shot configuration. int runtime_argc = arg_parser.runtime_argc(); char** runtime_argv = arg_parser.runtime_argv(); iree_flags_parse_checked(IREE_FLAGS_PARSE_MODE_DEFAULT, &runtime_argc, &runtime_argv); // Ensure a source file was found. if (runtime_argc != 2) { fprintf(stderr, "ERROR: one source MLIR file must be specified.\n" "Pass either the path to a .mlir/mlirbc file or `-` to read from " "stdin.\n"); fflush(stderr); IREE_TRACE_ZONE_END(z0); IREE_TRACE_APP_EXIT(EXIT_FAILURE); return EXIT_FAILURE; } const char* source_filename = runtime_argv[1]; // Sessions can be reused for many compiler invocations. iree_compiler_session_t* session = ireeCompilerSessionCreate(); // The process return code is 0 for success and non-zero otherwise. // We don't differentiate between compiler or runtime error codes here but // could if someone found it useful. int exit_code = EXIT_SUCCESS; // Compile and run the provided source file and get the exit code determined // based on the run mode. auto status_or = CompileAndRunFile(session, source_filename); if (status_or.ok()) { exit_code = status_or.value(); } else { exit_code = 2; iree_status_fprint(stderr, status_or.status().get()); fflush(stderr); } ireeCompilerSessionDestroy(session); // No more compiler APIs can be called after this point. ireeCompilerGlobalShutdown(); IREE_TRACE_ZONE_END(z0); IREE_TRACE_APP_EXIT(exit_code); return exit_code; } } // namespace iree