//=-------- clang-sycl-linker/ClangSYCLLinker.cpp - SYCL Linker util -------=// // // 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 // //===---------------------------------------------------------------------===// // // This tool executes a sequence of steps required to link device code in SYCL // device images. SYCL device code linking requires a complex sequence of steps // that include linking of llvm bitcode files, linking device library files // with the fully linked source bitcode file(s), running several SYCL specific // post-link steps on the fully linked bitcode file(s), and finally generating // target-specific device code. //===---------------------------------------------------------------------===// #include "clang/Basic/OffloadArch.h" #include "clang/Basic/Version.h" #include "llvm/ADT/StringExtras.h" #include "llvm/BinaryFormat/Magic.h" #include "llvm/Bitcode/BitcodeWriter.h" #include "llvm/CodeGen/CommandFlags.h" #include "llvm/IR/DiagnosticPrinter.h" #include "llvm/IR/LLVMContext.h" #include "llvm/IRReader/IRReader.h" #include "llvm/LTO/LTO.h" #include "llvm/Linker/Linker.h" #include "llvm/MC/TargetRegistry.h" #include "llvm/Object/Archive.h" #include "llvm/Object/ArchiveWriter.h" #include "llvm/Object/Binary.h" #include "llvm/Object/ELFObjectFile.h" #include "llvm/Object/IRObjectFile.h" #include "llvm/Object/ObjectFile.h" #include "llvm/Object/OffloadBinary.h" #include "llvm/Option/ArgList.h" #include "llvm/Option/OptTable.h" #include "llvm/Option/Option.h" #include "llvm/Remarks/HotnessThresholdParser.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/FileOutputBuffer.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/InitLLVM.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "llvm/Support/Program.h" #include "llvm/Support/Signals.h" #include "llvm/Support/StringSaver.h" #include "llvm/Support/TargetSelect.h" #include "llvm/Support/TimeProfiler.h" #include "llvm/Support/WithColor.h" #include "llvm/Target/TargetMachine.h" using namespace llvm; using namespace llvm::opt; using namespace llvm::object; using namespace clang; /// Save intermediary results. static bool SaveTemps = false; /// Print arguments without executing. static bool DryRun = false; /// Print verbose output. static bool Verbose = false; /// Filename of the output being created. static StringRef OutputFile; /// Directory to dump SPIR-V IR if requested by user. static SmallString<128> SPIRVDumpDir; using OffloadingImage = OffloadBinary::OffloadingImage; static void printVersion(raw_ostream &OS) { OS << clang::getClangToolFullVersion("clang-sycl-linker") << '\n'; } /// The value of `argv[0]` when run. static const char *Executable; /// Temporary files to be cleaned up. static SmallVector> TempFiles; namespace { // Must not overlap with llvm::opt::DriverFlag. enum LinkerFlags { LinkerOnlyOption = (1 << 4) }; enum ID { OPT_INVALID = 0, // This is not an option ID. #define OPTION(...) LLVM_MAKE_OPT_ID(__VA_ARGS__), #include "SYCLLinkOpts.inc" LastOption #undef OPTION }; #define OPTTABLE_STR_TABLE_CODE #include "SYCLLinkOpts.inc" #undef OPTTABLE_STR_TABLE_CODE #define OPTTABLE_PREFIXES_TABLE_CODE #include "SYCLLinkOpts.inc" #undef OPTTABLE_PREFIXES_TABLE_CODE static constexpr OptTable::Info InfoTable[] = { #define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__), #include "SYCLLinkOpts.inc" #undef OPTION }; class LinkerOptTable : public opt::GenericOptTable { public: LinkerOptTable() : opt::GenericOptTable(OptionStrTable, OptionPrefixesTable, InfoTable) {} }; const OptTable &getOptTable() { static const LinkerOptTable *Table = []() { auto Result = std::make_unique(); return Result.release(); }(); return *Table; } [[noreturn]] void reportError(Error E) { outs().flush(); logAllUnhandledErrors(std::move(E), WithColor::error(errs(), Executable)); exit(EXIT_FAILURE); } std::string getMainExecutable(const char *Name) { void *Ptr = (void *)(intptr_t)&getMainExecutable; auto COWPath = sys::fs::getMainExecutable(Name, Ptr); return sys::path::parent_path(COWPath).str(); } Expected createTempFile(const ArgList &Args, const Twine &Prefix, StringRef Extension) { SmallString<128> OutputFile; if (Args.hasArg(OPT_save_temps)) { // Generate a unique path name without creating a file sys::fs::createUniquePath(Prefix + "-%%%%%%." + Extension, OutputFile, /*MakeAbsolute=*/false); } else { if (std::error_code EC = sys::fs::createTemporaryFile(Prefix, Extension, OutputFile)) return createFileError(OutputFile, EC); } TempFiles.emplace_back(std::move(OutputFile)); return TempFiles.back(); } Expected findProgram(const ArgList &Args, StringRef Name, ArrayRef Paths) { if (Args.hasArg(OPT_dry_run)) return Name.str(); ErrorOr Path = sys::findProgramByName(Name, Paths); if (!Path) Path = sys::findProgramByName(Name); if (!Path) return createStringError(Path.getError(), "Unable to find '" + Name + "' in path"); return *Path; } void printCommands(ArrayRef CmdArgs) { if (CmdArgs.empty()) return; llvm::errs() << " \"" << CmdArgs.front() << "\" "; llvm::errs() << llvm::join(std::next(CmdArgs.begin()), CmdArgs.end(), " ") << "\n"; } /// Execute the command \p ExecutablePath with the arguments \p Args. Error executeCommands(StringRef ExecutablePath, ArrayRef Args) { if (Verbose || DryRun) printCommands(Args); if (!DryRun) if (sys::ExecuteAndWait(ExecutablePath, Args)) return createStringError( "'%s' failed", sys::path::filename(ExecutablePath).str().c_str()); return Error::success(); } Expected> getInput(const ArgList &Args) { // Collect all input bitcode files to be passed to the device linking stage. SmallVector BitcodeFiles; for (const opt::Arg *Arg : Args.filtered(OPT_INPUT)) { std::optional Filename = std::string(Arg->getValue()); if (!Filename || !sys::fs::exists(*Filename) || sys::fs::is_directory(*Filename)) continue; file_magic Magic; if (auto EC = identify_magic(*Filename, Magic)) return createStringError("Failed to open file " + *Filename); // TODO: Current use case involves LLVM IR bitcode files as input. // This will be extended to support SPIR-V IR files. if (Magic != file_magic::bitcode) return createStringError("Unsupported file type"); BitcodeFiles.push_back(*Filename); } return BitcodeFiles; } /// Handle cases where input file is a LLVM IR bitcode file. /// When clang-sycl-linker is called via clang-linker-wrapper tool, input files /// are LLVM IR bitcode files. // TODO: Support SPIR-V IR files. Expected> getBitcodeModule(StringRef File, LLVMContext &C) { SMDiagnostic Err; auto M = getLazyIRFileModule(File, Err, C); if (M) return std::move(M); return createStringError(Err.getMessage()); } /// Gather all SYCL device library files that will be linked with input device /// files. /// The list of files and its location are passed from driver. Expected> getSYCLDeviceLibs(const ArgList &Args) { SmallVector DeviceLibFiles; StringRef LibraryPath; if (Arg *A = Args.getLastArg(OPT_library_path_EQ)) LibraryPath = A->getValue(); if (Arg *A = Args.getLastArg(OPT_device_libs_EQ)) { if (A->getValues().size() == 0) return createStringError( inconvertibleErrorCode(), "Number of device library files cannot be zero."); for (StringRef Val : A->getValues()) { SmallString<128> LibName(LibraryPath); llvm::sys::path::append(LibName, Val); if (llvm::sys::fs::exists(LibName)) DeviceLibFiles.push_back(std::string(LibName)); else return createStringError(inconvertibleErrorCode(), "\'" + std::string(LibName) + "\'" + " SYCL device library file is not found."); } } return DeviceLibFiles; } /// Following tasks are performed: /// 1. Link all SYCL device bitcode images into one image. Device linking is /// performed using the linkInModule API. /// 2. Gather all SYCL device library bitcode images. /// 3. Link all the images gathered in Step 2 with the output of Step 1 using /// linkInModule API. LinkOnlyNeeded flag is used. Expected linkDeviceCode(ArrayRef InputFiles, const ArgList &Args, LLVMContext &C) { llvm::TimeTraceScope TimeScope("SYCL link device code"); assert(InputFiles.size() && "No inputs to link"); auto LinkerOutput = std::make_unique("sycl-device-link", C); Linker L(*LinkerOutput); // Link SYCL device input files. for (auto &File : InputFiles) { auto ModOrErr = getBitcodeModule(File, C); if (!ModOrErr) return ModOrErr.takeError(); if (L.linkInModule(std::move(*ModOrErr))) return createStringError("Could not link IR"); } // Get all SYCL device library files, if any. auto SYCLDeviceLibFiles = getSYCLDeviceLibs(Args); if (!SYCLDeviceLibFiles) return SYCLDeviceLibFiles.takeError(); // Link in SYCL device library files. const llvm::Triple Triple(Args.getLastArgValue(OPT_triple_EQ)); for (auto &File : *SYCLDeviceLibFiles) { auto LibMod = getBitcodeModule(File, C); if (!LibMod) return LibMod.takeError(); if ((*LibMod)->getTargetTriple() == Triple) { unsigned Flags = Linker::Flags::LinkOnlyNeeded; if (L.linkInModule(std::move(*LibMod), Flags)) return createStringError("Could not link IR"); } } // Dump linked output for testing. if (Args.hasArg(OPT_print_linked_module)) outs() << *LinkerOutput; // Create a new file to write the linked device file to. auto BitcodeOutput = createTempFile(Args, sys::path::filename(OutputFile), "bc"); if (!BitcodeOutput) return BitcodeOutput.takeError(); // Write the final output into 'BitcodeOutput' file. int FD = -1; if (std::error_code EC = sys::fs::openFileForWrite(*BitcodeOutput, FD)) return errorCodeToError(EC); llvm::raw_fd_ostream OS(FD, true); WriteBitcodeToFile(*LinkerOutput, OS); if (Verbose) { std::string Inputs = llvm::join(InputFiles.begin(), InputFiles.end(), ", "); std::string LibInputs = llvm::join((*SYCLDeviceLibFiles).begin(), (*SYCLDeviceLibFiles).end(), ", "); errs() << formatv( "sycl-device-link: inputs: {0} libfiles: {1} output: {2}\n", Inputs, LibInputs, *BitcodeOutput); } return *BitcodeOutput; } /// Run LLVM to SPIR-V translation. /// Converts 'File' from LLVM bitcode to SPIR-V format using SPIR-V backend. /// 'Args' encompasses all arguments required for linking device code and will /// be parsed to generate options required to be passed into the backend. static Error runSPIRVCodeGen(StringRef File, const ArgList &Args, StringRef OutputFile, LLVMContext &C) { llvm::TimeTraceScope TimeScope("SPIR-V code generation"); // Parse input module. SMDiagnostic Err; std::unique_ptr M = parseIRFile(File, Err, C); if (!M) return createStringError(Err.getMessage()); if (Error Err = M->materializeAll()) return Err; Triple TargetTriple(Args.getLastArgValue(OPT_triple_EQ)); M->setTargetTriple(TargetTriple); // Get a handle to SPIR-V target backend. std::string Msg; const Target *T = TargetRegistry::lookupTarget(M->getTargetTriple(), Msg); if (!T) return createStringError(Msg + ": " + M->getTargetTriple().str()); // Allocate SPIR-V target machine. TargetOptions Options; std::optional RM; std::optional CM; std::unique_ptr TM( T->createTargetMachine(M->getTargetTriple(), /* CPU */ "", /* Features */ "", Options, RM, CM)); if (!TM) return createStringError("Could not allocate target machine!"); // Set data layout if needed. if (M->getDataLayout().isDefault()) M->setDataLayout(TM->createDataLayout()); // Open output file for writing. int FD = -1; if (std::error_code EC = sys::fs::openFileForWrite(OutputFile, FD)) return errorCodeToError(EC); auto OS = std::make_unique(FD, true); // Run SPIR-V codegen passes to generate SPIR-V file. legacy::PassManager CodeGenPasses; TargetLibraryInfoImpl TLII(M->getTargetTriple()); CodeGenPasses.add(new TargetLibraryInfoWrapperPass(TLII)); if (TM->addPassesToEmitFile(CodeGenPasses, *OS, nullptr, CodeGenFileType::ObjectFile)) return createStringError("Failed to execute SPIR-V Backend"); CodeGenPasses.run(*M); if (Verbose) errs() << formatv("SPIR-V Backend: input: {0}, output: {1}\n", File, OutputFile); return Error::success(); } /// Run AOT compilation for Intel CPU. /// Calls opencl-aot tool to generate device code for the Intel OpenCL CPU /// Runtime. /// \param InputFile The input SPIR-V file. /// \param OutputFile The output file name. /// \param Args Encompasses all arguments required for linking and wrapping /// device code and will be parsed to generate options required to be passed /// into the SYCL AOT compilation step. static Error runAOTCompileIntelCPU(StringRef InputFile, StringRef OutputFile, const ArgList &Args) { SmallVector CmdArgs; Expected OpenCLAOTPath = findProgram(Args, "opencl-aot", {getMainExecutable("opencl-aot")}); if (!OpenCLAOTPath) return OpenCLAOTPath.takeError(); CmdArgs.push_back(*OpenCLAOTPath); CmdArgs.push_back("--device=cpu"); StringRef ExtraArgs = Args.getLastArgValue(OPT_opencl_aot_options_EQ); ExtraArgs.split(CmdArgs, " ", /*MaxSplit=*/-1, /*KeepEmpty=*/false); CmdArgs.push_back("-o"); CmdArgs.push_back(OutputFile); CmdArgs.push_back(InputFile); if (Error Err = executeCommands(*OpenCLAOTPath, CmdArgs)) return Err; return Error::success(); } /// Run AOT compilation for Intel GPU. /// Calls ocloc tool to generate device code for the Intel Graphics Compute /// Runtime. /// \param InputFile The input SPIR-V file. /// \param OutputFile The output file name. /// \param Args Encompasses all arguments required for linking and wrapping /// device code and will be parsed to generate options required to be passed /// into the SYCL AOT compilation step. static Error runAOTCompileIntelGPU(StringRef InputFile, StringRef OutputFile, const ArgList &Args) { SmallVector CmdArgs; Expected OclocPath = findProgram(Args, "ocloc", {getMainExecutable("ocloc")}); if (!OclocPath) return OclocPath.takeError(); CmdArgs.push_back(*OclocPath); // The next line prevents ocloc from modifying the image name CmdArgs.push_back("-output_no_suffix"); CmdArgs.push_back("-spirv_input"); StringRef Arch(Args.getLastArgValue(OPT_arch_EQ)); if (Arch.empty()) return createStringError(inconvertibleErrorCode(), "Arch must be specified for AOT compilation"); CmdArgs.push_back("-device"); CmdArgs.push_back(Arch); StringRef ExtraArgs = Args.getLastArgValue(OPT_ocloc_options_EQ); ExtraArgs.split(CmdArgs, " ", /*MaxSplit=*/-1, /*KeepEmpty=*/false); CmdArgs.push_back("-output"); CmdArgs.push_back(OutputFile); CmdArgs.push_back("-file"); CmdArgs.push_back(InputFile); if (Error Err = executeCommands(*OclocPath, CmdArgs)) return Err; return Error::success(); } /// Run AOT compilation for Intel CPU/GPU. /// \param InputFile The input SPIR-V file. /// \param OutputFile The output file name. /// \param Args Encompasses all arguments required for linking and wrapping /// device code and will be parsed to generate options required to be passed /// into the SYCL AOT compilation step. static Error runAOTCompile(StringRef InputFile, StringRef OutputFile, const ArgList &Args) { StringRef Arch = Args.getLastArgValue(OPT_arch_EQ); OffloadArch OffloadArch = StringToOffloadArch(Arch); if (IsIntelGPUOffloadArch(OffloadArch)) return runAOTCompileIntelGPU(InputFile, OutputFile, Args); if (IsIntelCPUOffloadArch(OffloadArch)) return runAOTCompileIntelCPU(InputFile, OutputFile, Args); return createStringError(inconvertibleErrorCode(), "Unsupported arch"); } /// Performs the following steps: /// 1. Link input device code (user code and SYCL device library code). /// 2. Run SPIR-V code generation. Error runSYCLLink(ArrayRef Files, const ArgList &Args) { llvm::TimeTraceScope TimeScope("SYCL device linking"); LLVMContext C; // Link all input bitcode files and SYCL device library files, if any. auto LinkedFile = linkDeviceCode(Files, Args, C); if (!LinkedFile) return LinkedFile.takeError(); // TODO: SYCL post link functionality involves device code splitting and will // result in multiple bitcode codes. // The following lines are placeholders to represent multiple files and will // be refactored once SYCL post link support is available. SmallVector SplitModules; SplitModules.emplace_back(*LinkedFile); bool IsAOTCompileNeeded = IsIntelOffloadArch( StringToOffloadArch(Args.getLastArgValue(OPT_arch_EQ))); // SPIR-V code generation step. for (size_t I = 0, E = SplitModules.size(); I != E; ++I) { StringRef Stem = OutputFile.rsplit('.').first; std::string SPVFile = (Stem + "_" + Twine(I) + ".spv").str(); if (Error Err = runSPIRVCodeGen(SplitModules[I], Args, SPVFile, C)) return Err; if (!IsAOTCompileNeeded) { SplitModules[I] = SPVFile; } else { // AOT compilation step. std::string AOTFile = (Stem + "_" + Twine(I) + ".out").str(); if (Error Err = runAOTCompile(SPVFile, AOTFile, Args)) return Err; SplitModules[I] = AOTFile; } } // Write the final output into file. int FD = -1; if (std::error_code EC = sys::fs::openFileForWrite(OutputFile, FD)) return errorCodeToError(EC); llvm::raw_fd_ostream FS(FD, /*shouldClose=*/true); for (size_t I = 0, E = SplitModules.size(); I != E; ++I) { auto File = SplitModules[I]; llvm::ErrorOr> FileOrErr = llvm::MemoryBuffer::getFileOrSTDIN(File); if (std::error_code EC = FileOrErr.getError()) { if (DryRun) FileOrErr = MemoryBuffer::getMemBuffer(""); else return createFileError(File, EC); } OffloadingImage TheImage{}; TheImage.TheImageKind = IMG_Object; TheImage.TheOffloadKind = OFK_SYCL; TheImage.StringData["triple"] = Args.MakeArgString(Args.getLastArgValue(OPT_triple_EQ)); TheImage.StringData["arch"] = Args.MakeArgString(Args.getLastArgValue(OPT_arch_EQ)); TheImage.Image = std::move(*FileOrErr); llvm::SmallString<0> Buffer = OffloadBinary::write(TheImage); if (Buffer.size() % OffloadBinary::getAlignment() != 0) return createStringError("Offload binary has invalid size alignment"); FS << Buffer; } return Error::success(); } } // namespace int main(int argc, char **argv) { InitLLVM X(argc, argv); InitializeAllTargetInfos(); InitializeAllTargets(); InitializeAllTargetMCs(); InitializeAllAsmParsers(); InitializeAllAsmPrinters(); Executable = argv[0]; sys::PrintStackTraceOnErrorSignal(argv[0]); const OptTable &Tbl = getOptTable(); BumpPtrAllocator Alloc; StringSaver Saver(Alloc); auto Args = Tbl.parseArgs(argc, argv, OPT_INVALID, Saver, [&](StringRef Err) { reportError(createStringError(inconvertibleErrorCode(), Err)); }); if (Args.hasArg(OPT_help) || Args.hasArg(OPT_help_hidden)) { Tbl.printHelp( outs(), "clang-sycl-linker [options] ", "A utility that wraps around several steps required to link SYCL " "device files.\n" "This enables LLVM IR linking, post-linking and code generation for " "SYCL targets.", Args.hasArg(OPT_help_hidden), Args.hasArg(OPT_help_hidden)); return EXIT_SUCCESS; } if (Args.hasArg(OPT_version)) printVersion(outs()); Verbose = Args.hasArg(OPT_verbose); DryRun = Args.hasArg(OPT_dry_run); SaveTemps = Args.hasArg(OPT_save_temps); if (!Args.hasArg(OPT_o)) reportError(createStringError("Output file must be specified")); OutputFile = Args.getLastArgValue(OPT_o); if (!Args.hasArg(OPT_triple_EQ)) reportError(createStringError("Target triple must be specified")); if (Args.hasArg(OPT_spirv_dump_device_code_EQ)) { Arg *A = Args.getLastArg(OPT_spirv_dump_device_code_EQ); SmallString<128> Dir(A->getValue()); if (Dir.empty()) llvm::sys::path::native(Dir = "./"); else Dir.append(llvm::sys::path::get_separator()); SPIRVDumpDir = Dir; } // Get the input files to pass to the linking stage. auto FilesOrErr = getInput(Args); if (!FilesOrErr) reportError(FilesOrErr.takeError()); // Run SYCL linking process on the generated inputs. if (Error Err = runSYCLLink(*FilesOrErr, Args)) reportError(std::move(Err)); // Remove the temporary files created. if (!Args.hasArg(OPT_save_temps)) for (const auto &TempFile : TempFiles) if (std::error_code EC = sys::fs::remove(TempFile)) reportError(createFileError(TempFile, EC)); return EXIT_SUCCESS; }