-
Notifications
You must be signed in to change notification settings - Fork 13.6k
[SPIR-V] Expose an API call to initialize SPIRV target and translate input LLVM IR module to SPIR-V #107216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SPIR-V] Expose an API call to initialize SPIRV target and translate input LLVM IR module to SPIR-V #107216
Conversation
✅ With the latest revision this PR passed the C/C++ code formatter. |
@llvm/pr-subscribers-backend-spir-v Author: Vyacheslav Levytskyy (VyacheslavLevytskyy) ChangesThe goal of this PR is to facilitate integration of SPIRV Backend into misc 3rd party tools and libraries by means of exposing an API call that translate LLVM module to SPIR-V and write results into a string as binary SPIR-V output, providing diagnostics on fail and means of configuring translation in a style of command line options. An example of a use case may be Khronos Translator that provides bidirectional translation LLVM IR <=> SPIR-V, where LLVM IR => SPIR-V step may be substituted by the call to SPIR-V Backend API, implemented by this PR. Full diff: https://github.com/llvm/llvm-project/pull/107216.diff 5 Files Affected:
diff --git a/llvm/lib/Target/SPIRV/CMakeLists.txt b/llvm/lib/Target/SPIRV/CMakeLists.txt
index 5f8aea5fc8d84d..df7869b1552caa 100644
--- a/llvm/lib/Target/SPIRV/CMakeLists.txt
+++ b/llvm/lib/Target/SPIRV/CMakeLists.txt
@@ -14,6 +14,7 @@ tablegen(LLVM SPIRVGenTables.inc -gen-searchable-tables)
add_public_tablegen_target(SPIRVCommonTableGen)
add_llvm_target(SPIRVCodeGen
+ SPIRVAPI.cpp
SPIRVAsmPrinter.cpp
SPIRVBuiltins.cpp
SPIRVCallLowering.cpp
diff --git a/llvm/lib/Target/SPIRV/SPIRVAPI.cpp b/llvm/lib/Target/SPIRV/SPIRVAPI.cpp
new file mode 100644
index 00000000000000..b4ada1947a4888
--- /dev/null
+++ b/llvm/lib/Target/SPIRV/SPIRVAPI.cpp
@@ -0,0 +1,167 @@
+//===-- SPIRVAPI.cpp - SPIR-V Backend API ---------------------*- C++ -*---===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Analysis/TargetLibraryInfo.h"
+#include "llvm/CodeGen/CommandFlags.h"
+#include "llvm/CodeGen/MachineFunctionPass.h"
+#include "llvm/CodeGen/MachineModuleInfo.h"
+#include "llvm/CodeGen/TargetPassConfig.h"
+#include "llvm/CodeGen/TargetSubtargetInfo.h"
+#include "llvm/IR/DataLayout.h"
+#include "llvm/IR/LLVMContext.h"
+#include "llvm/IR/LegacyPassManager.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IR/Verifier.h"
+#include "llvm/InitializePasses.h"
+#include "llvm/MC/MCTargetOptionsCommandFlags.h"
+#include "llvm/MC/TargetRegistry.h"
+#include "llvm/Pass.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/FormattedStream.h"
+#include "llvm/Support/InitLLVM.h"
+#include "llvm/Support/TargetSelect.h"
+#include "llvm/Target/TargetLoweringObjectFile.h"
+#include "llvm/Target/TargetMachine.h"
+#include "llvm/TargetParser/SubtargetFeature.h"
+#include "llvm/TargetParser/Triple.h"
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace llvm;
+
+namespace {
+
+// Mimic limited number of command line flags from llc to provide a better
+// user experience when passing options into the translate API call.
+static cl::opt<char> SpvOptLevel(" O", cl::Hidden, cl::Prefix, cl::init('0'));
+static cl::opt<std::string> SpvTargetTriple(" mtriple", cl::Hidden,
+ cl::init(""));
+
+// Utility to accept options in a command line style.
+void parseSPIRVCommandLineOptions(const std::vector<std::string> &Options,
+ raw_ostream *Errs) {
+ static constexpr const char *Origin = "SPIRVTranslateModule";
+ if (!Options.empty()) {
+ std::vector<const char *> Argv(1, Origin);
+ for (const auto &Arg : Options)
+ Argv.push_back(Arg.c_str());
+ cl::ParseCommandLineOptions(Argv.size(), Argv.data(), Origin, Errs);
+ }
+}
+
+std::once_flag InitOnceFlag;
+void InitializeSPIRVTarget() {
+ std::call_once(InitOnceFlag, []() {
+ LLVMInitializeSPIRVTargetInfo();
+ LLVMInitializeSPIRVTarget();
+ LLVMInitializeSPIRVTargetMC();
+ LLVMInitializeSPIRVAsmPrinter();
+ });
+}
+} // namespace
+
+namespace llvm {
+
+// The goal of this function is to facilitate integration of SPIRV Backend into
+// tools and libraries by means of exposing an API call that translate LLVM
+// module to SPIR-V and write results into a string as binary SPIR-V output,
+// providing diagnostics on fail and means of configuring translation in a style
+// of command line options.
+extern "C" LLVM_EXTERNAL_VISIBILITY bool
+SPIRVTranslateModule(Module *M, std::string &SpirvObj, std::string &ErrMsg,
+ const std::vector<std::string> &Opts) {
+ // Fallbacks for option values.
+ static const std::string DefaultTriple = "spirv64-unknown-unknown";
+ static const std::string DefaultMArch = "";
+
+ // Parse Opts as if it'd be command line arguments.
+ std::string Errors;
+ raw_string_ostream ErrorStream(Errors);
+ parseSPIRVCommandLineOptions(Opts, &ErrorStream);
+ if (!Errors.empty()) {
+ ErrMsg = Errors;
+ return false;
+ }
+
+ llvm::CodeGenOptLevel OLevel;
+ if (auto Level = CodeGenOpt::parseLevel(SpvOptLevel)) {
+ OLevel = *Level;
+ } else {
+ ErrMsg = "Invalid optimization level!";
+ return false;
+ }
+
+ // SPIR-V-specific target initialization.
+ InitializeSPIRVTarget();
+
+ Triple TargetTriple(SpvTargetTriple.empty()
+ ? M->getTargetTriple()
+ : Triple::normalize(SpvTargetTriple));
+ if (TargetTriple.getTriple().empty()) {
+ TargetTriple.setTriple(DefaultTriple);
+ M->setTargetTriple(DefaultTriple);
+ }
+ const Target *TheTarget =
+ TargetRegistry::lookupTarget(DefaultMArch, TargetTriple, ErrMsg);
+ if (!TheTarget)
+ return false;
+
+ // A call to codegen::InitTargetOptionsFromCodeGenFlags(TargetTriple)
+ // hits the following assertion: llvm/lib/CodeGen/CommandFlags.cpp:78:
+ // llvm::FPOpFusion::FPOpFusionMode llvm::codegen::getFuseFPOps(): Assertion
+ // `FuseFPOpsView && "RegisterCodeGenFlags not created."' failed.
+ TargetOptions Options;
+ std::optional<Reloc::Model> RM;
+ std::optional<CodeModel::Model> CM;
+ std::unique_ptr<TargetMachine> Target =
+ std::unique_ptr<TargetMachine>(TheTarget->createTargetMachine(
+ TargetTriple.getTriple(), "", "", Options, RM, CM, OLevel));
+ if (!Target) {
+ ErrMsg = "Could not allocate target machine!";
+ return false;
+ }
+
+ if (M->getCodeModel())
+ Target->setCodeModel(*M->getCodeModel());
+
+ std::string DLStr = M->getDataLayoutStr();
+ Expected<DataLayout> MaybeDL = DataLayout::parse(
+ DLStr.empty() ? Target->createDataLayout().getStringRepresentation()
+ : DLStr);
+ if (!MaybeDL) {
+ ErrMsg = toString(MaybeDL.takeError());
+ return false;
+ }
+ M->setDataLayout(MaybeDL.get());
+
+ TargetLibraryInfoImpl TLII(Triple(M->getTargetTriple()));
+ legacy::PassManager PM;
+ PM.add(new TargetLibraryInfoWrapperPass(TLII));
+ LLVMTargetMachine &LLVMTM = static_cast<LLVMTargetMachine &>(*Target);
+ MachineModuleInfoWrapperPass *MMIWP =
+ new MachineModuleInfoWrapperPass(&LLVMTM);
+ const_cast<TargetLoweringObjectFile *>(LLVMTM.getObjFileLowering())
+ ->Initialize(MMIWP->getMMI().getContext(), *Target);
+
+ SmallString<4096> OutBuffer;
+ raw_svector_ostream OutStream(OutBuffer);
+ if (Target->addPassesToEmitFile(PM, OutStream, nullptr,
+ CodeGenFileType::ObjectFile)) {
+ ErrMsg = "Target machine cannot emit a file of this type";
+ return false;
+ }
+
+ PM.run(*M);
+ SpirvObj = OutBuffer.str();
+
+ return true;
+}
+
+} // namespace llvm
diff --git a/llvm/lib/Target/SPIRV/SPIRVAPI.h b/llvm/lib/Target/SPIRV/SPIRVAPI.h
new file mode 100644
index 00000000000000..c3786c6975a890
--- /dev/null
+++ b/llvm/lib/Target/SPIRV/SPIRVAPI.h
@@ -0,0 +1,23 @@
+//===-- SPIRVAPI.h - SPIR-V Backend API interface ---------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIB_TARGET_SPIRV_SPIRVAPI_H
+#define LLVM_LIB_TARGET_SPIRV_SPIRVAPI_H
+
+#include <string>
+#include <vector>
+
+namespace llvm {
+class Module;
+
+extern "C" bool SPIRVTranslateModule(Module *M, std::string &Buffer,
+ std::string &ErrMsg,
+ const std::vector<std::string> &Opts);
+} // namespace llvm
+
+#endif // LLVM_LIB_TARGET_SPIRV_SPIRVAPI_H
diff --git a/llvm/unittests/Target/SPIRV/CMakeLists.txt b/llvm/unittests/Target/SPIRV/CMakeLists.txt
index 83ae215c512ca2..e9fe4883e5b024 100644
--- a/llvm/unittests/Target/SPIRV/CMakeLists.txt
+++ b/llvm/unittests/Target/SPIRV/CMakeLists.txt
@@ -6,6 +6,7 @@ include_directories(
set(LLVM_LINK_COMPONENTS
Analysis
AsmParser
+ BinaryFormat
Core
SPIRVCodeGen
SPIRVAnalysis
@@ -14,5 +15,6 @@ set(LLVM_LINK_COMPONENTS
add_llvm_target_unittest(SPIRVTests
SPIRVConvergenceRegionAnalysisTests.cpp
+ SPIRVAPITest.cpp
)
diff --git a/llvm/unittests/Target/SPIRV/SPIRVAPITest.cpp b/llvm/unittests/Target/SPIRV/SPIRVAPITest.cpp
new file mode 100644
index 00000000000000..d58c1f3fe9b460
--- /dev/null
+++ b/llvm/unittests/Target/SPIRV/SPIRVAPITest.cpp
@@ -0,0 +1,152 @@
+//===- llvm/unittest/CodeGen/SPIRVAPITest.cpp -----------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+/// \file
+/// Test that SPIR-V Backend provides an API call that translates LLVM IR Module
+/// into SPIR-V.
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/AsmParser/Parser.h"
+#include "llvm/BinaryFormat/Magic.h"
+#include "llvm/IR/Module.h"
+#include "llvm/Support/SourceMgr.h"
+#include "gtest/gtest.h"
+#include <gmock/gmock.h>
+#include <string>
+#include <utility>
+
+using ::testing::StartsWith;
+
+namespace llvm {
+
+extern "C" bool SPIRVTranslateModule(Module *M, std::string &Buffer,
+ std::string &ErrMsg,
+ const std::vector<std::string> &Opts);
+
+class SPIRVAPITest : public testing::Test {
+protected:
+ bool toSpirv(StringRef Assembly, std::string &Result, std::string &ErrMsg,
+ const std::vector<std::string> &Opts) {
+ SMDiagnostic ParseError;
+ M = parseAssemblyString(Assembly, ParseError, Context);
+ if (!M) {
+ ParseError.print("IR parsing failed: ", errs());
+ report_fatal_error("Can't parse input assembly.");
+ }
+ bool Status = SPIRVTranslateModule(M.get(), Result, ErrMsg, Opts);
+ if (!Status)
+ errs() << ErrMsg;
+ return Status;
+ }
+
+ LLVMContext Context;
+ std::unique_ptr<Module> M;
+
+ static constexpr StringRef ExtensionAssembly = R"(
+ define dso_local spir_func void @test1() {
+ entry:
+ %res1 = tail call spir_func i32 @_Z26__spirv_GroupBitwiseAndKHR(i32 2, i32 0, i32 0)
+ ret void
+ }
+
+ declare dso_local spir_func i32 @_Z26__spirv_GroupBitwiseAndKHR(i32, i32, i32)
+ )";
+ static constexpr StringRef OkAssembly = R"(
+ %struct = type { [1 x i64] }
+
+ define spir_kernel void @foo(ptr noundef byval(%struct) %arg) {
+ entry:
+ call spir_func void @bar(<2 x i32> noundef <i32 0, i32 1>)
+ ret void
+ }
+
+ define spir_func void @bar(<2 x i32> noundef) {
+ entry:
+ ret void
+ }
+ )";
+};
+
+TEST_F(SPIRVAPITest, checkTranslateOk) {
+ StringRef Assemblies[] = {"", OkAssembly};
+ // Those command line arguments that overlap with registered by llc/codegen
+ // are to be started with the ' ' symbol.
+ std::vector<std::string> SetOfOpts[] = {
+ {}, {"- mtriple=spirv32-unknown-unknown"}};
+ for (const auto &Opts : SetOfOpts) {
+ for (StringRef &Assembly : Assemblies) {
+ std::string Result, Error;
+ bool Status = toSpirv(Assembly, Result, Error, Opts);
+ EXPECT_TRUE(Status && Error.empty() && !Result.empty());
+ EXPECT_EQ(identify_magic(Result), file_magic::spirv_object);
+ }
+ }
+}
+
+TEST_F(SPIRVAPITest, checkTranslateError) {
+ std::string Result, Error;
+ bool Status =
+ toSpirv(OkAssembly, Result, Error, {"-mtriple=spirv32-unknown-unknown"});
+ EXPECT_FALSE(Status);
+ EXPECT_TRUE(Result.empty());
+ EXPECT_THAT(Error,
+ StartsWith("SPIRVTranslateModule: Unknown command line argument "
+ "'-mtriple=spirv32-unknown-unknown'"));
+ Status = toSpirv(OkAssembly, Result, Error, {"- O 5"});
+ EXPECT_FALSE(Status);
+ EXPECT_TRUE(Result.empty());
+ EXPECT_EQ(Error, "Invalid optimization level!");
+}
+
+TEST_F(SPIRVAPITest, checkTranslateSupportExtension) {
+ std::string Result, Error;
+ std::vector<std::string> Opts{
+ "--spirv-ext=+SPV_KHR_uniform_group_instructions"};
+ bool Status = toSpirv(ExtensionAssembly, Result, Error, Opts);
+ EXPECT_TRUE(Status && Error.empty() && !Result.empty());
+ EXPECT_EQ(identify_magic(Result), file_magic::spirv_object);
+}
+
+TEST_F(SPIRVAPITest, checkTranslateAllExtensions) {
+ std::string Result, Error;
+ std::vector<std::string> Opts{"--spirv-ext=all"};
+ bool Status = toSpirv(ExtensionAssembly, Result, Error, Opts);
+ EXPECT_TRUE(Status && Error.empty() && !Result.empty());
+ EXPECT_EQ(identify_magic(Result), file_magic::spirv_object);
+}
+
+#if !defined(NDEBUG) && GTEST_HAS_DEATH_TEST
+TEST_F(SPIRVAPITest, checkTranslateExtensionError) {
+ std::string Result, Error;
+ std::vector<std::string> Opts;
+ EXPECT_DEATH_IF_SUPPORTED(
+ { toSpirv(ExtensionAssembly, Result, Error, Opts); },
+ "LLVM ERROR: __spirv_GroupBitwiseAndKHR: the builtin requires the "
+ "following SPIR-V extension: SPV_KHR_uniform_group_instructions");
+}
+
+TEST_F(SPIRVAPITest, checkTranslateUnknownExtension) {
+ std::string Result, Error;
+ std::vector<std::string> Opts{"--spirv-ext=+SPV_XYZ_my_unknown_extension"};
+ EXPECT_DEATH_IF_SUPPORTED(
+ { toSpirv(ExtensionAssembly, Result, Error, Opts); },
+ "SPIRVTranslateModule: for the --spirv-ext option: Unknown SPIR-V");
+}
+
+TEST_F(SPIRVAPITest, checkTranslateWrongExtension) {
+ std::string Result, Error;
+ std::vector<std::string> Opts{"--spirv-ext=+SPV_KHR_subgroup_rotate"};
+ EXPECT_DEATH_IF_SUPPORTED(
+ { toSpirv(ExtensionAssembly, Result, Error, Opts); },
+ "LLVM ERROR: __spirv_GroupBitwiseAndKHR: the builtin requires the "
+ "following SPIR-V extension: SPV_KHR_uniform_group_instructions");
+}
+#endif
+
+} // end namespace llvm
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not super familiar with how 3rd party use LLVM, but AFAIK they do have access to the public LLVM headers no?
If that's correct, why not require a struct with the option we need in an already parsed format?
struct BuildOptions {
CodeGenOptLevel OptLevel;
Triple TargetTriple;
std::vector<std::string> AllowedExtensions;
...
};
SPIRVTranslateModule(Module *M, const BuildOptions& Options, ...)
?
Duplicating the CLI feels a bit weird IMO
The idea was to introduce a loose coupling between SPIRV BE and its user to achieve mainly two goals: avoid forcing a user to include one more header file that would describe this interface in C++ terms (like |
Ok, so if I understand, you want the 3rd party library to be able to call the function on a newer LLVM version, without having to relink/recompile again with the updated headers. Vulkan already does something for that: the chain of structs, with the But once again, not very knowledgeable about the LLVM API interfaces, so maybe parsing command-line is accepted the way for this kind of back-compat. |
@Keenuts Thank you for the insight and advices. I've tweaked the interface a bit to account for the fact that the list of extensions is one of the most frequently used command line argument with more complicated composition and parsing. After the latest change the list of extensions is an argument to the API call. What is your opinion about the change, does it look slightly better now? |
If back/forward-compatibility is the most important part, I'd prefer to either:
I feel like having part in the to-parse command-line, and one part as classic parameter is weird. |
I've been thinking about this. Explicit extensions may simplify the client part though and slightly improve performance for the expected use cases. If you don't mind, I'd continue with the current version as the first attempt, to be able to verify this approach in Khronos Translator, Intel XPU backend for the Triton compiler and SYCL offloading. We can re-visit this API call later in case if integration would show that this is needed or recommended. |
From my perspective as SPIR-V To LLVM IR translator developer current approach is OK. For the proposed options above:
This won't affect the use case described in the PR description, but it may affect other 3rd party projects, as they would need to parse options once to get extensions string and then pass it to llvm:toSpirv call along with other options, while the whole parsing might be done just here in the SPIR-V backend. So I'm slightly favorable for this proposal, but don't have a strong opinion about this.
Personally that would be my least favorable option. But I'm not an expert in llvm's guideline in the regard of how to parse and pass options to binaries and library calls. |
The goal of this PR is to facilitate integration of SPIRV Backend into misc 3rd party tools and libraries by means of exposing an API call that translate LLVM module to SPIR-V and write results into a string as binary SPIR-V output, providing diagnostics on fail and means of configuring translation in a style of command line options.
An example of a use case may be Khronos Translator that provides bidirectional translation LLVM IR <=> SPIR-V, where LLVM IR => SPIR-V step may be substituted by the call to SPIR-V Backend API, implemented by this PR.