blob: f80e47cf72d1206cd08ef85d399b3ded3c27ffcd [file] [log] [blame]
// Copyright 2020 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
#include "iree/compiler/Dialect/HAL/Target/LLVM/LLVMAOTTarget.h"
#include <cstdlib>
#include "iree/compiler/Codegen/Passes.h"
#include "iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.h"
#include "iree/compiler/Dialect/HAL/Target/LLVM/LibraryBuilder.h"
#include "iree/compiler/Dialect/HAL/Target/LLVM/LinkerTool.h"
#include "iree/compiler/Dialect/HAL/Target/LLVM/StaticLibraryGenerator.h"
#include "iree/compiler/Dialect/HAL/Target/TargetRegistry.h"
#include "iree/compiler/Utils/FlatbufferUtils.h"
#include "iree/schemas/dylib_executable_def_builder.h"
#include "llvm/Bitcode/BitcodeWriter.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/TargetSelect.h"
#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
#include "mlir/Target/LLVMIR/Dialect/LLVMIR/LLVMToLLVMIRTranslation.h"
#include "mlir/Target/LLVMIR/Export.h"
namespace mlir {
namespace iree_compiler {
namespace IREE {
namespace HAL {
namespace {
constexpr char kQueryFunctionName[] = "iree_hal_executable_library_query";
llvm::Optional<FileLineColLoc> findFirstFileLoc(Location baseLoc) {
if (auto loc = baseLoc.dyn_cast<FusedLoc>()) {
for (auto &childLoc : loc.getLocations()) {
auto childResult = findFirstFileLoc(childLoc);
if (childResult) return childResult;
}
} else if (auto loc = baseLoc.dyn_cast<FileLineColLoc>()) {
return loc;
}
return llvm::None;
}
std::string guessModuleName(mlir::ModuleOp moduleOp) {
std::string moduleName =
moduleOp.getName().hasValue() ? moduleOp.getName().getValue().str() : "";
if (!moduleName.empty()) return moduleName;
auto loc = findFirstFileLoc(moduleOp.getLoc());
if (loc.hasValue()) {
return llvm::sys::path::stem(loc.getValue().getFilename()).str();
} else {
return "llvm_module";
}
}
} // namespace
class LLVMAOTTargetBackend final : public TargetBackend {
public:
explicit LLVMAOTTargetBackend(LLVMTargetOptions options)
: options_(std::move(options)) {}
std::string name() const override { return "llvm"; }
std::string deviceID() const override { return "cpu"; }
void getDependentDialects(DialectRegistry &registry) const override {
mlir::registerLLVMDialectTranslation(registry);
}
IREE::HAL::DeviceTargetAttr getDefaultDeviceTarget(
MLIRContext *context) const override {
Builder b(context);
SmallVector<NamedAttribute> configItems;
configItems.emplace_back(b.getIdentifier("executable_targets"),
getExecutableTargets(context));
auto configAttr = b.getDictionaryAttr(configItems);
return IREE::HAL::DeviceTargetAttr::get(
context, b.getStringAttr(deviceID()), configAttr);
}
void buildTranslationPassPipeline(OpPassManager &passManager) override {
auto targetMachine = createTargetMachine(options_);
if (!targetMachine) {
llvm::errs() << "failed to create target machine for target triple '"
<< options_.targetTriple << "'";
return;
}
passManager.addPass(createLLVMCPULowerExecutableTargetPass());
// Set target specific options.
LLVMCPUCodegenPassPipelineOptions codeGenOptions;
codeGenOptions.targetTriple = options_.targetTriple;
codeGenOptions.targetDataLayout =
targetMachine->createDataLayout().getStringRepresentation();
// TODO(ataei): This is temporary here, should move when target specific
// overrides options grows.
if (targetMachine->getTargetTriple().isWasm()) {
codeGenOptions.unfuseFMAOps = true;
}
buildLLVMCPUCodegenPassPipeline(passManager, codeGenOptions);
}
LogicalResult linkExecutables(mlir::ModuleOp moduleOp) override {
OpBuilder builder = OpBuilder::atBlockBegin(moduleOp.getBody());
auto sourceExecutableOps =
llvm::to_vector<8>(moduleOp.getOps<IREE::HAL::ExecutableOp>());
if (sourceExecutableOps.size() <= 1) return success();
// TODO(benvanik): rework linking to support multiple formats.
auto sharedTargetAttr = getExecutableTarget(builder.getContext());
// Guess a module name, if needed, to make the output files readable.
auto moduleName = guessModuleName(moduleOp);
// Create our new "linked" hal.executable.
std::string linkedExecutableName =
llvm::formatv("{0}_linked_{1}", moduleName, name());
auto linkedExecutableOp = builder.create<IREE::HAL::ExecutableOp>(
moduleOp.getLoc(), linkedExecutableName);
linkedExecutableOp.setVisibility(
sourceExecutableOps.front().getVisibility());
// Add our hal.executable.variant with an empty module.
builder.setInsertionPointToStart(linkedExecutableOp.getBody());
auto linkedTargetOp = builder.create<IREE::HAL::ExecutableVariantOp>(
moduleOp.getLoc(), sharedTargetAttr.getSymbolNameFragment(),
sharedTargetAttr);
builder.setInsertionPoint(&linkedTargetOp.getBlock().back());
builder.create<ModuleOp>(moduleOp.getLoc());
// Try linking together all executables in moduleOp.
return linkExecutablesInto(
moduleOp, sourceExecutableOps, linkedExecutableOp, linkedTargetOp,
[](mlir::ModuleOp moduleOp) { return moduleOp; }, builder);
}
LogicalResult serializeExecutable(IREE::HAL::ExecutableVariantOp variantOp,
OpBuilder &executableBuilder) override {
// Perform the translation in a separate context to avoid any
// multi-threading issues.
llvm::LLVMContext context;
// We name our files after the executable name so that they are easy to
// track both during compilation (logs/artifacts/etc), as outputs (final
// intermediate code/binary files), and at runtime (loaded
// libraries/symbols/etc).
auto libraryName =
variantOp->getParentOfType<IREE::HAL::ExecutableOp>().getName().str();
// Validate flags for output mode.
if (options_.linkEmbedded && !options_.staticLibraryOutput.empty()) {
return variantOp.emitError()
<< "cannot embed ELF and produce static library simultaneously";
}
// Specialize the module to the target triple.
// The executable will have been cloned into other ExecutableVariantOps for
// other triples so it's fine to mutate in-place.
llvm::Triple targetTriple(options_.targetTriple);
variantOp.getInnerModule()->setAttr(
LLVM::LLVMDialect::getTargetTripleAttrName(),
executableBuilder.getStringAttr(targetTriple.str()));
// At this moment we are leaving MLIR LLVM dialect land translating module
// into target independent LLVMIR.
auto llvmModule = mlir::translateModuleToLLVMIR(variantOp.getInnerModule(),
context, libraryName);
if (!llvmModule) {
return variantOp.emitError() << "failed to translate the MLIR LLVM "
"dialect to the native llvm::Module";
}
// Configure the functions in the module. This may override defaults set
// during the MLIR->LLVM conversion.
for (auto &func : *llvmModule) {
// Enable frame pointers to ensure that stack unwinding works, e.g. in
// Tracy. In principle this could also be achieved by enabling unwind
// tables, but we tried that and that didn't work in Tracy (which uses
// libbacktrace), while enabling frame pointers worked.
// https://github.com/google/iree/issues/3957
func.addFnAttr("frame-pointer", "all");
// -ffreestanding-like behavior.
func.addFnAttr("no-builtins");
// Our dispatches are all hot - that's kind of the point.
// This may favor more aggressive optimizations.
func.addFnAttr("hot");
}
// Build the IREE HAL executable library metadata. The runtime uses this to
// find the entry point functions and their information.
// TODO(benvanik): add a flag for this (adds a few KB/binary).
LibraryBuilder::Mode libraryBuilderMode =
LibraryBuilder::Mode::INCLUDE_REFLECTION_ATTRS;
LibraryBuilder libraryBuilder(llvmModule.get(), libraryBuilderMode,
LibraryBuilder::Version::V_0);
switch (options_.sanitizerKind) {
case SanitizerKind::kNone: {
libraryBuilder.setSanitizerKind(LibraryBuilder::SanitizerKind::NONE);
break;
}
case SanitizerKind::kAddress: {
libraryBuilder.setSanitizerKind(LibraryBuilder::SanitizerKind::ADDRESS);
for (auto &function : llvmModule->getFunctionList()) {
function.addFnAttr(llvm::Attribute::SanitizeAddress);
}
} break;
}
for (auto entryPointOp :
variantOp.getBlock().getOps<ExecutableEntryPointOp>()) {
// Find the matching function in the LLVM module.
auto *llvmFunc = llvmModule->getFunction(entryPointOp.getName());
llvmFunc->setLinkage(llvm::GlobalValue::LinkageTypes::InternalLinkage);
llvmFunc->setDSOLocal(true);
// Optionally entry points may specify that they require workgroup local
// memory. We fetch that value here and plumb it through so the runtime
// knows how much memory to reserve and pass in.
int64_t localMemorySize = entryPointOp.workgroup_local_memory()
.getValueOr(APInt(64, 0))
.getSExtValue();
libraryBuilder.addExport(entryPointOp.getName(), "",
LibraryBuilder::DispatchAttrs{localMemorySize},
llvmFunc);
}
auto queryFunctionName = std::string(kQueryFunctionName);
if (!options_.staticLibraryOutput.empty()) {
// Static library query functions must be unique to support multiple
// libraries in the same namespace.
queryFunctionName = libraryName + "_library_query";
}
auto *queryLibraryFunc = libraryBuilder.build(queryFunctionName);
// The query function must be exported for dynamic libraries.
queryLibraryFunc->setVisibility(
llvm::GlobalValue::VisibilityTypes::DefaultVisibility);
queryLibraryFunc->setLinkage(
llvm::GlobalValue::LinkageTypes::ExternalLinkage);
// Try to grab a linker tool based on the options (and target environment).
auto linkerTool = LinkerTool::getForTarget(targetTriple, options_);
if (!linkerTool) {
return mlir::emitError(variantOp.getLoc())
<< "failed to find a target linker for the given target triple '"
<< options_.targetTriple << "'";
}
// Configure the module with any code generation options required later by
// linking (such as initializer functions).
if (failed(linkerTool->configureModule(llvmModule.get(),
{queryLibraryFunc}))) {
return variantOp.emitError()
<< "failed to configure LLVM module for target linker";
}
// LLVM opt passes that perform code generation optimizations/transformation
// similar to what a frontend would do before passing to linking.
auto targetMachine = createTargetMachine(options_);
if (!targetMachine) {
return mlir::emitError(variantOp.getLoc())
<< "failed to create target machine for target triple '"
<< options_.targetTriple << "'";
}
llvmModule->setDataLayout(targetMachine->createDataLayout());
llvmModule->setTargetTriple(targetMachine->getTargetTriple().str());
if (failed(
runLLVMIRPasses(options_, targetMachine.get(), llvmModule.get()))) {
return variantOp.emitError()
<< "failed to run LLVM-IR opt passes for IREE::HAL::ExecutableOp "
"targeting '"
<< options_.targetTriple << "'";
}
// Emit object files.
SmallVector<Artifact, 4> objectFiles;
{
// NOTE: today we just use a single object file, however if we wanted to
// scale code generation and linking we'd want to generate one per
// function (or something like that). A single object file is also
// instrumental to static library generation (which only supports one
// object file per library).
std::string objectData;
if (failed(runEmitObjFilePasses(targetMachine.get(), llvmModule.get(),
&objectData))) {
return variantOp.emitError()
<< "failed to compile LLVM-IR module to an object file";
}
auto objectFile = Artifact::createTemporary(libraryName, "o");
auto &os = objectFile.outputFile->os();
os << objectData;
os.flush();
os.close();
objectFiles.push_back(std::move(objectFile));
}
// If we are keeping artifacts then let's also add the bitcode for easier
// debugging (vs just the binary object file).
if (options_.keepLinkerArtifacts) {
auto bitcodeFile =
Artifact::createVariant(objectFiles.front().path, "bc");
auto &os = bitcodeFile.outputFile->os();
llvm::WriteBitcodeToFile(*llvmModule, os);
os.flush();
os.close();
bitcodeFile.outputFile->keep();
}
if (!options_.staticLibraryOutput.empty()) {
if (objectFiles.size() != 1) {
// Static library output only supports single object libraries.
return variantOp.emitError()
<< "generating static libraries from "
"multiple object files is not supported";
}
// Copy the static object file to the specified output along with
// generated header file.
const std::string &libraryPath = options_.staticLibraryOutput;
const auto library_name = objectFiles[0].path;
if (!outputStaticLibrary(libraryName, queryFunctionName, libraryPath,
objectFiles[0].path)) {
return variantOp.emitError() << "static library generation failed";
}
}
// Link the generated object files into a dylib.
auto linkArtifactsOr =
linkerTool->linkDynamicLibrary(libraryName, objectFiles);
if (!linkArtifactsOr.hasValue()) {
return mlir::emitError(variantOp.getLoc())
<< "failed to link executable and generate target dylib using "
"linker toolchain "
<< linkerTool->getToolPath();
}
auto &linkArtifacts = linkArtifactsOr.getValue();
if (options_.keepLinkerArtifacts) {
mlir::emitRemark(variantOp.getLoc())
<< "linker artifacts for " << variantOp.getName() << " preserved:\n"
<< " " << linkArtifacts.libraryFile.path;
linkArtifacts.keepAllFiles();
for (auto &objectFile : objectFiles) {
objectFile.outputFile->keep();
}
}
if (options_.linkEmbedded) {
// Load the linked ELF file and pack into an attr.
auto elfFile = linkArtifacts.libraryFile.read();
if (!elfFile.hasValue()) {
return variantOp.emitError()
<< "failed to read back dylib temp file at "
<< linkArtifacts.libraryFile.path;
}
auto bufferAttr = DenseIntElementsAttr::get(
VectorType::get({static_cast<int64_t>(elfFile->size())},
IntegerType::get(executableBuilder.getContext(), 8)),
std::move(elfFile.getValue()));
// Add the binary to the parent hal.executable.
auto binaryOp = executableBuilder.create<IREE::HAL::ExecutableBinaryOp>(
variantOp.getLoc(), variantOp.sym_name(),
variantOp.target().getFormat(), bufferAttr);
binaryOp.mime_typeAttr(
executableBuilder.getStringAttr("application/x-elf"));
} else if (!options_.staticLibraryOutput.empty()) {
// Embed the library name in the executable binary op. This informs the
// loader which static library to load for the target binary.
std::vector<uint8_t> libraryNameVector(libraryName.begin(),
libraryName.end());
executableBuilder.create<IREE::HAL::ExecutableBinaryOp>(
variantOp.getLoc(), variantOp.sym_name(),
variantOp.target().getFormat().getValue(), libraryNameVector);
} else {
FlatbufferBuilder builder;
iree_DyLibExecutableDef_start_as_root(builder);
// Embed debug symbols at the end of the flatbuffer by adding first in the
// bottoms-up builder.
flatbuffers_uint8_vec_ref_t debugDatabaseRef = 0;
flatbuffers_string_ref_t debugDatabaseFilenameRef = 0;
if (options_.debugSymbols && linkArtifacts.debugFile.outputFile) {
debugDatabaseRef = builder.streamUint8Vec([&](raw_ostream &stream) {
return linkArtifacts.debugFile.readInto(stream);
});
debugDatabaseFilenameRef = builder.createString(
llvm::sys::path::filename(linkArtifacts.debugFile.path));
}
// Embed entire dynamic library output.
flatbuffers_uint8_vec_ref_t libraryEmbeddedRef =
builder.streamUint8Vec([&](raw_ostream &stream) {
return linkArtifacts.libraryFile.readInto(stream);
});
if (!libraryEmbeddedRef) {
return variantOp.emitError()
<< "failed to read back dylib temp file at "
<< linkArtifacts.libraryFile.path;
}
iree_DyLibExecutableDef_library_embedded_add(builder, libraryEmbeddedRef);
iree_DyLibExecutableDef_debug_database_filename_add(
builder, debugDatabaseFilenameRef);
iree_DyLibExecutableDef_debug_database_embedded_add(builder,
debugDatabaseRef);
iree_DyLibExecutableDef_end_as_root(builder);
// Add the binary data to the target executable.
auto binaryOp = executableBuilder.create<IREE::HAL::ExecutableBinaryOp>(
variantOp.getLoc(), variantOp.sym_name(),
variantOp.target().getFormat(),
builder.getBufferAttr(executableBuilder.getContext()));
binaryOp.mime_typeAttr(
executableBuilder.getStringAttr("application/x-flatbuffers"));
}
return success();
}
private:
ArrayAttr getExecutableTargets(MLIRContext *context) const {
SmallVector<Attribute> targetAttrs;
// This is where we would multiversion.
targetAttrs.push_back(getExecutableTarget(context));
return ArrayAttr::get(context, targetAttrs);
}
IREE::HAL::ExecutableTargetAttr getExecutableTarget(
MLIRContext *context) const {
std::string format;
if (options_.linkStatic) {
// Static libraries are just string references when serialized so we don't
// need to specify the target architecture.
format += "static";
} else {
// Construct the [loader]-[format]-[arch] triple.
llvm::Triple targetTriple(options_.targetTriple);
if (options_.linkEmbedded) {
// Using the IREE embedded ELF format/loader.
format += "embedded-elf-";
} else {
// System-specific shared library format.
format += "system-";
switch (targetTriple.getObjectFormat()) {
case llvm::Triple::ObjectFormatType::COFF:
format += "dll-";
break;
case llvm::Triple::ObjectFormatType::ELF:
format += "elf-";
break;
case llvm::Triple::ObjectFormatType::MachO:
format += "dylib-";
break;
case llvm::Triple::ObjectFormatType::Wasm:
format += "wasm-";
break;
default:
format += "unknown-";
break;
}
}
switch (targetTriple.getArch()) {
case llvm::Triple::ArchType::arm:
format += "arm_32";
break;
case llvm::Triple::ArchType::aarch64:
format += "arm_64";
break;
case llvm::Triple::ArchType::riscv32:
format += "riscv_32";
break;
case llvm::Triple::ArchType::riscv64:
format += "riscv_64";
break;
case llvm::Triple::ArchType::wasm32:
format += "wasm_32";
break;
case llvm::Triple::ArchType::wasm64:
format += "wasm_64";
break;
case llvm::Triple::ArchType::x86:
format += "x86_32";
break;
case llvm::Triple::ArchType::x86_64:
format += "x86_64";
break;
default:
format += "unknown";
break;
}
}
// TODO(benvanik): pack in the LLVMTargetOptions into the config dict.
return IREE::HAL::ExecutableTargetAttr::get(context, "llvm", format);
}
LLVMTargetOptions options_;
};
void registerLLVMAOTTargetBackends(
std::function<LLVMTargetOptions()> queryOptions) {
getLLVMTargetOptionsFromFlags();
#define INIT_LLVM_TARGET(TargetName) \
LLVMInitialize##TargetName##Target(); \
LLVMInitialize##TargetName##TargetMC(); \
LLVMInitialize##TargetName##TargetInfo(); \
LLVMInitialize##TargetName##AsmPrinter(); \
LLVMInitialize##TargetName##AsmParser();
auto backendFactory = [=]() {
INIT_LLVM_TARGET(X86)
INIT_LLVM_TARGET(ARM)
INIT_LLVM_TARGET(AArch64)
INIT_LLVM_TARGET(RISCV)
INIT_LLVM_TARGET(WebAssembly)
return std::make_unique<LLVMAOTTargetBackend>(queryOptions());
};
// #hal.device.target<"cpu", ...
static TargetBackendRegistration registration0("cpu", backendFactory);
// #hal.executable.target<"llvm", ...
static TargetBackendRegistration registration1("llvm", backendFactory);
// TODO(benvanik): remove legacy dylib name.
static TargetBackendRegistration registration2("dylib", backendFactory);
static TargetBackendRegistration registration3("dylib-llvm-aot",
backendFactory);
#undef INIT_LLVM_TARGET
}
} // namespace HAL
} // namespace IREE
} // namespace iree_compiler
} // namespace mlir