| //===--- Level Zero Target RTL Implementation -----------------------------===// | |
| // | |
| // Part of the LLVM Project, 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 | |
| // | |
| //===----------------------------------------------------------------------===// | |
| // | |
| // Level Zero Program abstraction. | |
| // | |
| //===----------------------------------------------------------------------===// | |
| #include "L0Plugin.h" | |
| #include "L0Program.h" | |
| namespace llvm::omp::target::plugin { | |
| Error L0GlobalHandlerTy::getGlobalMetadataFromDevice(GenericDeviceTy &Device, | |
| DeviceImageTy &Image, | |
| GlobalTy &DeviceGlobal) { | |
| const char *GlobalName = DeviceGlobal.getName().data(); | |
| size_t SymbolSize = 0; | |
| void *SymbolAddr = nullptr; | |
| L0ProgramTy &Program = L0ProgramTy::makeL0Program(Image); | |
| if (auto Err = | |
| Program.getSymbolMetadata(GlobalName, &SymbolAddr, &SymbolSize)) | |
| return Err; | |
| // Save the pointer to the symbol allowing nullptr. | |
| DeviceGlobal.setPtr(SymbolAddr); | |
| DeviceGlobal.setSize(SymbolSize); | |
| return Plugin::success(); | |
| } | |
| bool L0GlobalHandlerTy::isExportedSymbol(uint32_t Flags) { | |
| // Images returned by the Level Zero runtime do not correctly expose kernel | |
| // functions as global symbols. Bypass the normal ELF handling.here. | |
| uint32_t Ignored = SymbolRef::SF_Undefined | SymbolRef::SF_Hidden | | |
| SymbolRef::SF_FormatSpecific; | |
| return !(Flags & Ignored); | |
| } | |
| inline L0DeviceTy &L0ProgramTy::getL0Device() const { | |
| return L0DeviceTy::makeL0Device(getDevice()); | |
| } | |
| Error L0ProgramTy::deinit() { | |
| for (auto *Kernel : Kernels) { | |
| if (auto Err = Kernel->deinit()) | |
| return Err; | |
| getL0Device().getPlugin().free(Kernel); | |
| } | |
| for (auto Module : Modules) { | |
| CALL_ZE_RET_ERROR(zeModuleDestroy, Module); | |
| } | |
| return Plugin::success(); | |
| } | |
| /// Print the contents of a module build or link log. Unless \p FullLog is set, | |
| /// only the first few lines are printed. | |
| static void printBuildLog(ze_module_build_log_handle_t Log, const char *Title, | |
| bool FullLog) { | |
| constexpr size_t MaxLines = 10; | |
| MESSAGE("%s:", Title); | |
| size_t LogSize = 0; | |
| ze_result_t RC; | |
| CALL_ZE(RC, zeModuleBuildLogGetString, Log, &LogSize, /*LogString=*/nullptr); | |
| if (RC != ZE_RESULT_SUCCESS) { | |
| MESSAGE0(" <failed to get build log>"); | |
| return; | |
| } | |
| if (LogSize <= 1) { | |
| MESSAGE0(" <empty>"); | |
| return; | |
| } | |
| std::string LogString(LogSize, '\0'); | |
| CALL_ZE(RC, zeModuleBuildLogGetString, Log, &LogSize, LogString.data()); | |
| if (RC != ZE_RESULT_SUCCESS) { | |
| MESSAGE0(" <failed to get build log>"); | |
| return; | |
| } | |
| StringRef Rest(LogString.c_str()); | |
| for (size_t NumLines = 0; !Rest.empty(); ++NumLines) { | |
| if (!FullLog && NumLines >= MaxLines) { | |
| MESSAGE0(" (suppressed remaining log)"); | |
| break; | |
| } | |
| StringRef Line; | |
| std::tie(Line, Rest) = Rest.split('\n'); | |
| MESSAGE(" '%.*s'", static_cast<int>(Line.size()), Line.data()); | |
| } | |
| } | |
| Error L0ProgramBuilderTy::addModule(size_t Size, const uint8_t *Image, | |
| const std::string_view CommonBuildOptions, | |
| ze_module_format_t Format) { | |
| auto &L0Device = getL0Device(); | |
| const ze_module_constants_t SpecConstants = | |
| L0Device.getPlugin() | |
| .getOptions() | |
| .CommonSpecConstants.getModuleConstants(); | |
| std::string BuildOptions(CommonBuildOptions); | |
| bool IsLibModule = | |
| BuildOptions.find("-library-compilation") != std::string::npos; | |
| ze_module_desc_t ModuleDesc{}; | |
| ModuleDesc.stype = ZE_STRUCTURE_TYPE_MODULE_DESC; | |
| ModuleDesc.pNext = nullptr; | |
| ModuleDesc.format = Format; | |
| ze_module_handle_t Module = nullptr; | |
| ze_module_build_log_handle_t BuildLog = nullptr; | |
| // Build a single module from a single image. | |
| ModuleDesc.inputSize = Size; | |
| ModuleDesc.pInputModule = Image; | |
| ModuleDesc.pBuildFlags = BuildOptions.c_str(); | |
| ModuleDesc.pConstants = &SpecConstants; | |
| ze_result_t RC; | |
| CALL_ZE(RC, zeModuleCreate, getZeContext(), L0Device.getZeDevice(), | |
| &ModuleDesc, &Module, &BuildLog); | |
| if (BuildLog) { | |
| const bool BuildFailed = RC != ZE_RESULT_SUCCESS; | |
| const bool ShowBuildLog = | |
| L0Device.getPlugin().getOptions().Flags.ShowBuildLog; | |
| // Only show the log of library modules (-library-compilation) if their | |
| // build failed. | |
| if (BuildFailed || (ShowBuildLog && !IsLibModule)) { | |
| if (BuildFailed) | |
| MESSAGE0("Error: module creation failed"); | |
| printBuildLog(BuildLog, "Target build log", ShowBuildLog); | |
| } | |
| zeModuleBuildLogDestroy(BuildLog); | |
| } | |
| if (RC != ZE_RESULT_SUCCESS) { | |
| // zeModuleCreate compiles/loads the provided image, so a build failure here | |
| // means the image itself could not be loaded for this device (e.g. a | |
| // truncated or malformed binary) rather than a generic JIT failure of an | |
| // otherwise valid program. Report it as INVALID_BINARY in that case (as | |
| // opposed to the default mapping of ZE_RESULT_ERROR_MODULE_BUILD_FAILURE | |
| // to ErrorCode::COMPILE_FAILURE). | |
| const auto ErrCode = RC == ZE_RESULT_ERROR_MODULE_BUILD_FAILURE | |
| ? ErrorCode::INVALID_BINARY | |
| : getOffloadErrorCode(RC); | |
| return Plugin::error(ErrCode, "zeModuleCreate failed with error %d, %s", RC, | |
| getZeErrorName(RC)); | |
| } | |
| // Check if module link is required. We do not need this check for | |
| // library module. | |
| if (!RequiresModuleLink && !IsLibModule) { | |
| ze_module_properties_t Properties = {ZE_STRUCTURE_TYPE_MODULE_PROPERTIES, | |
| nullptr, 0}; | |
| ze_result_t RC; | |
| CALL_ZE(RC, zeModuleGetProperties, Module, &Properties); | |
| if (RC == ZE_RESULT_SUCCESS) | |
| RequiresModuleLink = Properties.flags & ZE_MODULE_PROPERTY_FLAG_IMPORTS; | |
| } | |
| // For now, assume the first module contains libraries, globals. | |
| if (Modules.empty()) | |
| GlobalModule = Module; | |
| Modules.push_back(Module); | |
| L0Device.addGlobalModule(Module); | |
| return Plugin::success(); | |
| } | |
| Error L0ProgramBuilderTy::linkModules() { | |
| auto &L0Device = getL0Device(); | |
| if (!RequiresModuleLink) { | |
| ODBG(OLDT_Module) << "Module link is not required"; | |
| return Plugin::success(); | |
| } | |
| if (Modules.empty()) | |
| return Plugin::error(ErrorCode::UNKNOWN, | |
| "Invalid number of modules when linking modules"); | |
| ze_module_build_log_handle_t LinkLog = nullptr; | |
| ze_result_t RC; | |
| CALL_ZE(RC, zeModuleDynamicLink, | |
| static_cast<uint32_t>(L0Device.getNumGlobalModules()), | |
| L0Device.getGlobalModulesArray(), &LinkLog); | |
| if (LinkLog) { | |
| const bool LinkFailed = RC != ZE_RESULT_SUCCESS; | |
| const bool ShowBuildLog = | |
| L0Device.getPlugin().getOptions().Flags.ShowBuildLog; | |
| if (LinkFailed || ShowBuildLog) { | |
| if (LinkFailed) | |
| MESSAGE0("Error: module link failed"); | |
| printBuildLog(LinkLog, "Target link log", ShowBuildLog); | |
| } | |
| zeModuleBuildLogDestroy(LinkLog); | |
| } | |
| if (RC != ZE_RESULT_SUCCESS) | |
| return Plugin::error(getOffloadErrorCode(RC), | |
| "zeModuleDynamicLink failed with error %d, %s", RC, | |
| getZeErrorName(RC)); | |
| return Plugin::success(); | |
| } | |
| static void replaceDriverOptsWithBackendOpts(const L0DeviceTy &Device, | |
| std::string &Options) { | |
| // Options that need to be replaced with backend-specific options | |
| static const struct { | |
| std::string Option; | |
| std::string BackendOption; | |
| } OptionTranslationTable[] = { | |
| {"-ftarget-compile-fast", | |
| "-igc_opts 'PartitionUnit=1,SubroutineThreshold=50000'"}, | |
| {"-foffload-fp32-prec-div", "-ze-fp32-correctly-rounded-divide-sqrt"}, | |
| {"-foffload-fp32-prec-sqrt", "-ze-fp32-correctly-rounded-divide-sqrt"}, | |
| }; | |
| for (const auto &OptPair : OptionTranslationTable) { | |
| const size_t Pos = Options.find(OptPair.Option); | |
| if (Pos != std::string::npos) | |
| Options.replace(Pos, OptPair.Option.length(), OptPair.BackendOption); | |
| } | |
| } | |
| Error L0ProgramBuilderTy::buildModules(const std::string_view BuildOptions) { | |
| auto &L0Device = getL0Device(); | |
| auto Image = getMemoryBuffer(); | |
| // Check if image is an inner OffloadBinary (nested format) | |
| if (identify_magic(Image.getBuffer()) == file_magic::offload_binary) { | |
| ODBG(OLDT_Module) << "Processing nested OffloadBinary image"; | |
| // Parse inner OffloadBinary | |
| auto InnerBinariesOrErr = llvm::object::OffloadBinary::create(Image); | |
| if (!InnerBinariesOrErr) | |
| return Plugin::error( | |
| ErrorCode::INVALID_BINARY, "Failed to parse inner OffloadBinary: %s", | |
| llvm::toString(InnerBinariesOrErr.takeError()).c_str()); | |
| auto &InnerBinaries = *InnerBinariesOrErr; | |
| // Should contain exactly one image | |
| if (InnerBinaries.size() != 1) | |
| return Plugin::error(ErrorCode::INVALID_BINARY, | |
| "Expected single inner OffloadBinary entry, got %zu", | |
| InnerBinaries.size()); | |
| const llvm::object::OffloadBinary *InnerBinary = InnerBinaries[0].get(); | |
| llvm::object::ImageKind ImageKind = InnerBinary->getImageKind(); | |
| // Extract image data from inner binary | |
| llvm::StringRef ImageData = InnerBinary->getImage(); | |
| const uint8_t *ImgBegin = | |
| reinterpret_cast<const uint8_t *>(ImageData.data()); | |
| // Read metadata from inner binary | |
| llvm::StringRef Version = InnerBinary->getString("version"); | |
| llvm::StringRef CompileOpts = InnerBinary->getString("compile-opts"); | |
| llvm::StringRef LinkOpts = InnerBinary->getString("link-opts"); | |
| ODBG(OLDT_Module) << "Inner OffloadBinary metadata: version=" << Version | |
| << ", kind=" << ImageKind; | |
| // Build options string combining BuildOptions with compile/link opts | |
| std::string Options(BuildOptions); | |
| if (!CompileOpts.empty() || !LinkOpts.empty()) { | |
| if (!CompileOpts.empty()) | |
| Options += " " + CompileOpts.str(); | |
| if (!LinkOpts.empty()) | |
| Options += " " + LinkOpts.str(); | |
| replaceDriverOptsWithBackendOpts(L0Device, Options); | |
| ODBG(OLDT_Module) << "Using compile options: " << CompileOpts | |
| << ", link options: " << LinkOpts; | |
| } | |
| // Determine module format based on image kind | |
| ze_module_format_t ModuleFormat; | |
| if (ImageKind == llvm::object::IMG_SPIRV) { | |
| // SPIR-V intermediate language | |
| ODBG(OLDT_Module) << "Loading SPIR-V module"; | |
| ModuleFormat = ZE_MODULE_FORMAT_IL_SPIRV; | |
| } else if (ImageKind == llvm::object::IMG_Object) { | |
| // Native binary format | |
| ODBG(OLDT_Module) << "Loading native binary module"; | |
| ModuleFormat = ZE_MODULE_FORMAT_NATIVE; | |
| } else { | |
| return Plugin::error(ErrorCode::INVALID_BINARY, | |
| "Unsupported image kind %d in inner OffloadBinary", | |
| static_cast<int>(ImageKind)); | |
| } | |
| // Load module into Level Zero | |
| auto Err = addModule(ImageData.size(), ImgBegin, Options, ModuleFormat); | |
| if (Err) | |
| return Err; | |
| if (RequiresModuleLink) { | |
| ODBG(OLDT_Module) << "Linking modules after adding OffloadBinary image"; | |
| if (auto Err = linkModules()) | |
| return Err; | |
| } | |
| return Plugin::success(); | |
| } | |
| if (identify_magic(Image.getBuffer()) == file_magic::spirv_object) { | |
| ODBG(OLDT_Module) << "Processing raw SPIR-V image"; | |
| const uint8_t *ImgBegin = | |
| reinterpret_cast<const uint8_t *>(Image.getBufferStart()); | |
| auto Err = addModule(Image.getBufferSize(), ImgBegin, BuildOptions, | |
| ZE_MODULE_FORMAT_IL_SPIRV); | |
| if (Err) | |
| return Err; | |
| if (RequiresModuleLink) { | |
| ODBG(OLDT_Module) << "Linking modules after adding SPIR-V image"; | |
| if (auto Err = linkModules()) | |
| return Err; | |
| } | |
| return Plugin::success(); | |
| } | |
| return Plugin::error(ErrorCode::INVALID_BINARY, | |
| "Unsupported image format for L0 plugin"); | |
| } | |
| Expected<std::unique_ptr<MemoryBuffer>> L0ProgramBuilderTy::getELF() { | |
| assert(GlobalModule != nullptr && "GlobalModule is null"); | |
| size_t Size = 0; | |
| CALL_ZE_RET_ERROR(zeModuleGetNativeBinary, GlobalModule, &Size, nullptr); | |
| std::vector<uint8_t> ELFData(Size); | |
| CALL_ZE_RET_ERROR(zeModuleGetNativeBinary, GlobalModule, &Size, | |
| ELFData.data()); | |
| return MemoryBuffer::getMemBufferCopy( | |
| StringRef(reinterpret_cast<const char *>(ELFData.data()), Size), | |
| /*BufferName=*/"L0Program ELF"); | |
| } | |
| Error L0ProgramTy::getSymbolMetadata(const char *Name, void **AddrPtr, | |
| size_t *SizePtr) const { | |
| if (!Name || !AddrPtr || !SizePtr) | |
| return Plugin::error(ErrorCode::INVALID_ARGUMENT, | |
| "Invalid arguments to getSymbolDeviceAddr"); | |
| size_t SymbolSize = 0; | |
| void *SymbolAddr = nullptr; | |
| ze_result_t RC; | |
| for (auto Module : Modules) { | |
| CALL_ZE(RC, zeModuleGetGlobalPointer, Module, Name, &SymbolSize, | |
| &SymbolAddr); | |
| if (RC == ZE_RESULT_SUCCESS && SymbolAddr) { | |
| *AddrPtr = SymbolAddr; | |
| *SizePtr = SymbolSize; | |
| return Plugin::success(); | |
| } | |
| CALL_ZE(RC, zeModuleGetFunctionPointer, Module, Name, &SymbolAddr); | |
| if (RC == ZE_RESULT_SUCCESS && SymbolAddr) { | |
| *AddrPtr = SymbolAddr; | |
| *SizePtr = 0; | |
| return Plugin::success(); | |
| } | |
| } | |
| return Plugin::error(ErrorCode::NOT_FOUND, "symbol '%s' not found on device", | |
| Name); | |
| } | |
| Error L0ProgramTy::readGlobalVariable(const char *Name, size_t Size, | |
| void *HostPtr) { | |
| size_t SizeDummy = 0; | |
| void *DevicePtr = nullptr; | |
| ze_result_t RC; | |
| CALL_ZE(RC, zeModuleGetGlobalPointer, GlobalModule, Name, &SizeDummy, | |
| &DevicePtr); | |
| if (RC != ZE_RESULT_SUCCESS || !DevicePtr) { | |
| return Plugin::error(ErrorCode::INVALID_ARGUMENT, | |
| "Cannot read from device global variable %s", Name); | |
| } | |
| return getL0Device().enqueueMemCopyAndSync(HostPtr, DevicePtr, Size); | |
| } | |
| Error L0ProgramTy::writeGlobalVariable(const char *Name, size_t Size, | |
| const void *HostPtr) { | |
| size_t SizeDummy = 0; | |
| void *DevicePtr = nullptr; | |
| ze_result_t RC; | |
| CALL_ZE(RC, zeModuleGetGlobalPointer, GlobalModule, Name, &SizeDummy, | |
| &DevicePtr); | |
| if (RC != ZE_RESULT_SUCCESS || !DevicePtr) { | |
| return Plugin::error(ErrorCode::INVALID_ARGUMENT, | |
| "Cannot write to device global variable %s", Name); | |
| } | |
| return getL0Device().enqueueMemCopyAndSync(DevicePtr, HostPtr, Size); | |
| } | |
| Error L0ProgramTy::loadModuleKernels() { | |
| // We need to build kernels here before filling the offload entries since we | |
| // don't know which module contains a specific kernel with a name. | |
| for (auto Module : Modules) { | |
| uint32_t Count = 0; | |
| CALL_ZE_RET_ERROR(zeModuleGetKernelNames, Module, &Count, | |
| /*Names=*/nullptr); | |
| if (Count == 0) | |
| continue; | |
| llvm::SmallVector<const char *> Names(Count); | |
| CALL_ZE_RET_ERROR(zeModuleGetKernelNames, Module, &Count, Names.data()); | |
| for (auto *Name : Names) { | |
| KernelsToModuleMap.emplace(Name, Module); | |
| } | |
| } | |
| return Plugin::success(); | |
| } | |
| } // namespace llvm::omp::target::plugin |