| //===--- 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 |