diff --git a/src/bootstrap/src/core/build_steps/compile.rs b/src/bootstrap/src/core/build_steps/compile.rs index 11308f613b877..9b4edf49f7c01 100644 --- a/src/bootstrap/src/core/build_steps/compile.rs +++ b/src/bootstrap/src/core/build_steps/compile.rs @@ -2303,6 +2303,17 @@ impl CommandLineStep for Assemble { let dst_lib = target_libdir.join(libname); builder.resolve_symlink_and_copy(&p, &dst_lib); } + + for &gpu_target in llvm::GPU_TARGETS { + let gpu_libc = + builder.ensure(llvm::GpuLibc { target: build_compiler.host, gpu_target }); + let dst_dir = target_libdir.join(gpu_target); + t!(fs::create_dir_all(&dst_dir)); + for p in gpu_libc.paths() { + let dst_lib = dst_dir.join(p.file_name().unwrap()); + builder.copy_link(p, &dst_lib, FileType::NativeLibrary); + } + } } } diff --git a/src/bootstrap/src/core/build_steps/dist.rs b/src/bootstrap/src/core/build_steps/dist.rs index 53e7746d220f5..42813fa6c0e97 100644 --- a/src/bootstrap/src/core/build_steps/dist.rs +++ b/src/bootstrap/src/core/build_steps/dist.rs @@ -2888,6 +2888,10 @@ impl CommandLineStep for Offload { let omp_offload = builder.ensure(llvm::OmpOffload { target }); let rust_offload = builder.ensure(llvm::RustOffload { target }); + let gpu_libcs: Vec<_> = llvm::GPU_TARGETS + .iter() + .map(|&gpu_target| (gpu_target, builder.ensure(llvm::GpuLibc { target, gpu_target }))) + .collect(); if builder.config.dry_run() { return None; @@ -2899,7 +2903,7 @@ impl CommandLineStep for Offload { tarball.set_overlay(OverlayKind::Offload); tarball.is_preview(true); - let omp_offload_libdir = builder.out.join(target).join("offload").join("lib"); + let omp_offload_libdir = builder.offload_out(target).join("lib"); for path in omp_offload.artifact_paths_with_symlink_targets() { let relative = t!(path.strip_prefix(&omp_offload_libdir)); @@ -2908,6 +2912,13 @@ impl CommandLineStep for Offload { tarball.add_file(path, destdir, FileType::NativeLibrary); } + for (gpu_target, gpu_libc) in &gpu_libcs { + let destdir = target_libdir.join(gpu_target); + for path in gpu_libc.paths() { + tarball.add_file(path, &destdir, FileType::NativeLibrary); + } + } + tarball.add_file(rust_offload.rust_offload_path(), target_libdir, FileType::NativeLibrary); Some(tarball.generate()) diff --git a/src/bootstrap/src/core/build_steps/llvm.rs b/src/bootstrap/src/core/build_steps/llvm.rs index 554b4b30d1d6f..a874a05e3694f 100644 --- a/src/bootstrap/src/core/build_steps/llvm.rs +++ b/src/bootstrap/src/core/build_steps/llvm.rs @@ -1141,10 +1141,7 @@ impl CommandLineStep for RustOffload { let profile = get_llvm_profile(&builder.config); - cfg.out_dir(&out_dir) - .profile(profile) - .env("LLVM_CONFIG_REAL", &llvm_output.host_llvm_config) - .define("LLVM_DIR", llvm_output.cmake_dir()); + cfg.out_dir(&out_dir).profile(profile).define("LLVM_DIR", llvm_output.cmake_dir()); cfg.build(); @@ -1229,40 +1226,14 @@ impl CommandLineStep for OmpOffload { let llvm_output = builder.ensure(Llvm { target: self.target }); - // Running cmake twice in the same folder is known to cause issues, like deleting existing - // binaries. We therefore write our offload artifacts into it's own folder, instead of - // using the llvm build dir. - let out_dir = builder.out.join(self.target.triple).join("offload"); + let out_dir = builder.offload_out(self.target); - let mut files = vec![]; let lib_ext = std::env::consts::DLL_EXTENSION; - files.push(out_dir.join("lib").join("libLLVMOffload").with_extension(lib_ext)); - files.push(out_dir.join("lib").join("libomp").with_extension(lib_ext)); - files.push(out_dir.join("lib").join("libomptarget").with_extension(lib_ext)); - files.push( - out_dir.join("lib").join("amdgcn-amd-amdhsa").join("libompdevice").with_extension("a"), - ); - files.push( - out_dir - .join("lib") - .join("amdgcn-amd-amdhsa") - .join("libomptarget-amdgpu") - .with_extension("bc"), - ); - files.push( - out_dir - .join("lib") - .join("nvptx64-nvidia-cuda") - .join("libompdevice") - .with_extension("a"), - ); - files.push( - out_dir - .join("lib") - .join("nvptx64-nvidia-cuda") - .join("libomptarget-nvptx") - .with_extension("bc"), - ); + let files = vec![ + out_dir.join("lib").join("libLLVMOffload").with_extension(lib_ext), + out_dir.join("lib").join("libomp").with_extension(lib_ext), + out_dir.join("lib").join("libomptarget").with_extension(lib_ext), + ]; // Offload/OpenMP are just subfolders of LLVM, so we can use the LLVM sha. static STAMP_HASH_MEMO: OnceLock = OnceLock::new(); @@ -1299,13 +1270,7 @@ impl CommandLineStep for OmpOffload { builder.config.update_submodule("src/llvm-project"); - let offload_clang_dir = if !builder.config.llvm_clang { - // We must have an external clang to use. - builder.build.config.offload_clang_dir.clone() - } else { - // No need to specify it, since we use the in-tree clang - None - }; + let offload_clang_dir = offload_clang_cmake_dir(builder); // We currently build libompdevice by accident. It includes bitcode for our amd/nvptx // targets, and only the latest clang compiler can build those. We could stop building those @@ -1315,27 +1280,7 @@ impl CommandLineStep for OmpOffload { // we built our own clang based on the llvm submodule first, this always works. The // alternative is that the user sets the offload_clang_dir path, in which case they hopefully point // to a suitable clang, otherwise the build will fail. - let clang_bin_dir = if builder.config.llvm_clang { - llvm_output.host_llvm_config.parent().map(Path::to_path_buf) - } else { - // We expect the following (default) structure of the offload_clang_dir: - // /lib/cmake/clang, with a ClangConfig.cmake inside. - // The clang binary is located in /bin, so we go up three levels to find it. - // This hardcodes the ClangConfig.cmake logic, which isn't great, so we filter for the - // binary and error if we can't find it (presumably because LLVM build layout changed?). - offload_clang_dir - .as_deref() - .and_then(|dir| dir.ancestors().nth(3)) - .map(|prefix| prefix.join("bin")) - } - .filter(|dir| dir.join(exe("clang", target)).exists()); - - let Some(clang_bin_dir) = clang_bin_dir else { - eprintln!( - "Building Offload requires a clang binary. Please either set `llvm.offload-clang-dir` or enable `llvm.clang` to build it." - ); - helpers::exit_process(1); - }; + let clang_bin_dir = offload_clang_bin_dir(builder, target, &llvm_output); let clang = clang_bin_dir.join(exe("clang", target)); let clangxx = clang_bin_dir.join(exe("clang++", target)); @@ -1357,79 +1302,61 @@ impl CommandLineStep for OmpOffload { libstdcxx.parent().map(Path::to_path_buf) }); - // In the context of OpenMP offload, some libraries must be compiled for the gpu target, - // some for the host, and others for both. We do not perform a full cross-compilation, since - // we don't want to run rustc on a GPU. - let omp_targets = vec![target.triple.as_ref(), "amdgcn-amd-amdhsa", "nvptx64-nvidia-cuda"]; - for omp_target in omp_targets { - let mut cfg = cmake::Config::new(builder.src.join("src/llvm-project/runtimes/")); - - // If we use an external clang as opposed to building our own llvm_clang, than that clang will - // come with it's own set of default include directories, which are based on a potentially older - // LLVM. This can cause issues, so we overwrite it to include headers based on our - // `src/llvm-project` submodule instead. - let mut cflags = CcFlags::default(); - if !builder.config.llvm_clang { - let base = builder.llvm_out(target).join("include"); - let inc_dir = base.display(); - cflags.push_all(format!(" -I {inc_dir}")); - } + let mut cfg = cmake::Config::new(builder.src.join("src/llvm-project/runtimes/")); - // Logic copied from `configure_llvm` - // ThinLTO is only available when building with LLVM, enabling LLD is required. - // Apple's linker ld64 supports ThinLTO out of the box though, so don't use LLD on Darwin. - let mut ldflags = LdFlags::default(); - if builder.config.llvm_thin_lto && !target.contains("apple") { - ldflags.push_all("-fuse-ld=lld"); - } - if *omp_target == *target.triple - && let Some(dir) = &cxx_lib_dir - { - ldflags.push_all(format!("-L{}", dir.display())); - } + // If we use an external clang as opposed to building our own llvm_clang, than that clang will + // come with it's own set of default include directories, which are based on a potentially older + // LLVM. This can cause issues, so we overwrite it to include headers based on our + // `src/llvm-project` submodule instead. + let mut cflags = CcFlags::default(); + if !builder.config.llvm_clang { + let base = builder.llvm_out(target).join("include"); + let inc_dir = base.display(); + cflags.push_all(format!(" -I {inc_dir}")); + } - configure_cmake(builder, target, &mut cfg, true, ldflags, cflags, &[]); - - cfg.define("CMAKE_C_COMPILER", &clang) - .define("CMAKE_CXX_COMPILER", &clangxx) - .define("CMAKE_ASM_COMPILER", &clang); - - // Re-use the same flags as llvm to control the level of debug information - // generated for offload. - let profile = get_llvm_profile(&builder.config); - trace!(?profile); - - // FIXME(offload): Once we move from OMP to Offload (Ol) APIs, we should drop the openmp - // runtime to simplify our build. So far, these are still under development. - cfg.out_dir(&out_dir) - .profile(profile) - .env("LLVM_CONFIG_REAL", &llvm_output.host_llvm_config) - .define("LLVM_ENABLE_ASSERTIONS", "ON") - .define("LLVM_INCLUDE_TESTS", "OFF") - .define("OFFLOAD_INCLUDE_TESTS", "OFF") - .define("LLVM_ROOT", builder.llvm_out(target).join("build")) - .define("LLVM_DIR", llvm_output.cmake_dir()) - .define("LLVM_DEFAULT_TARGET_TRIPLE", omp_target); - if let Some(p) = offload_clang_dir.clone() { - cfg.define("Clang_DIR", p); - } + // Logic copied from `configure_llvm` + // ThinLTO is only available when building with LLVM, enabling LLD is required. + // Apple's linker ld64 supports ThinLTO out of the box though, so don't use LLD on Darwin. + let mut ldflags = LdFlags::default(); + if builder.config.llvm_thin_lto && !target.contains("apple") { + ldflags.push_all("-fuse-ld=lld"); + } - // We don't perform a full cross-compilation of rustc, therefore our target.triple - // will still be a CPU target. - if *omp_target == *target.triple { - // The offload library provides functionality which only makes sense on the host. - cfg.define("LLVM_ENABLE_RUNTIMES", "openmp;offload"); - } else { - // OpenMP provides some device libraries, so we also compile it for all gpu targets. - cfg.define("OPENMP_INSTALL_LIBDIR", Path::new("lib").join(omp_target)); - cfg.define("LLVM_USE_LINKER", "lld"); - cfg.define("LLVM_ENABLE_RUNTIMES", "openmp"); - cfg.define("CMAKE_C_COMPILER_TARGET", omp_target); - cfg.define("CMAKE_CXX_COMPILER_TARGET", omp_target); - } - cfg.build(); + if let Some(dir) = &cxx_lib_dir { + ldflags.push_all(format!("-L{}", dir.display())); + } + + configure_cmake(builder, target, &mut cfg, true, ldflags, cflags, &[]); + + cfg.define("CMAKE_C_COMPILER", &clang) + .define("CMAKE_CXX_COMPILER", &clangxx) + .define("CMAKE_ASM_COMPILER", &clang); + + // Re-use the same flags as llvm to control the level of debug information + // generated for offload. + let profile = get_llvm_profile(&builder.config); + trace!(?profile); + + // FIXME(offload): Once we move from OMP to Offload (Ol) APIs, we should drop the openmp + // runtime to simplify our build. So far, these are still under development. + cfg.out_dir(&out_dir) + .profile(profile) + .define("LLVM_ENABLE_ASSERTIONS", "ON") + .define("LLVM_INCLUDE_TESTS", "OFF") + .define("OFFLOAD_INCLUDE_TESTS", "OFF") + .define("LLVM_ROOT", builder.llvm_out(target).join("build")) + .define("LLVM_DIR", llvm_output.cmake_dir()) + .define("LLVM_DEFAULT_TARGET_TRIPLE", &*target.triple); + if let Some(p) = offload_clang_dir { + cfg.define("Clang_DIR", p); } + // The offload library provides functionality which only makes sense on the host. + cfg.define("LLVM_ENABLE_RUNTIMES", "openmp;offload"); + + cfg.build(); + t!(stamp.write()); for p in &files { @@ -1447,6 +1374,199 @@ impl CommandLineStep for OmpOffload { } } +fn offload_clang_cmake_dir(builder: &Builder<'_>) -> Option { + if !builder.config.llvm_clang { + // We must have an external clang to use. + builder.config.offload_clang_dir.clone() + } else { + // No need to specify it, since we use the in-tree clang + None + } +} + +fn offload_clang_bin_dir( + builder: &Builder<'_>, + target: TargetSelection, + llvm_output: &LlvmOutput, +) -> PathBuf { + let clang_bin_dir = if builder.config.llvm_clang { + llvm_output.host_llvm_config.parent().map(Path::to_path_buf) + } else { + // We expect the following (default) structure of the offload_clang_dir: + // /lib/cmake/clang, with a ClangConfig.cmake inside. + // The clang binary is located in /bin, so we go up three levels to find it. + // This hardcodes the ClangConfig.cmake logic, which isn't great, so we filter for the + // binary and error if we can't find it (presumably because LLVM build layout changed?). + builder + .config + .offload_clang_dir + .as_deref() + .and_then(|dir| dir.ancestors().nth(3)) + .map(|prefix| prefix.join("bin")) + } + .filter(|dir| dir.join(exe("clang", target)).exists()); + + let Some(clang_bin_dir) = clang_bin_dir else { + eprintln!( + "Building Offload requires a clang binary. Please either set `llvm.offload-clang-dir` or enable `llvm.clang` to build it." + ); + helpers::exit_process(1); + }; + clang_bin_dir +} + +// FIXME(offload): add intel-spirv target once there is a rustc target for it. +pub const GPU_TARGETS: &[&str] = &["amdgcn-amd-amdhsa", "nvptx64-nvidia-cuda"]; + +#[derive(Clone)] +pub struct BuiltGpuLibc { + libs: Vec, +} + +impl BuiltGpuLibc { + pub fn paths(&self) -> &[PathBuf] { + &self.libs + } +} + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub struct GpuLibc { + pub target: TargetSelection, + pub gpu_target: &'static str, +} + +impl CommandLineStep for GpuLibc { + type Output = BuiltGpuLibc; + const IS_HOST: bool = true; + + fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> { + run.alias("gpu-libc") + } + + fn make_run(run: RunConfig<'_>) { + for &gpu_target in GPU_TARGETS { + run.builder.ensure(GpuLibc { target: run.target, gpu_target }); + } + } + + fn run(self, builder: &Builder<'_>) -> Self::Output { + let target = self.target; + let gpu_target = self.gpu_target; + + let out_dir = builder.offload_out(target).join(gpu_target); + let lib_dir = out_dir.join("lib").join(gpu_target); + let files = vec![lib_dir.join("libc.a"), lib_dir.join("libm.a"), lib_dir.join("crt1.o")]; + + if builder.config.dry_run() { + return BuiltGpuLibc { libs: files }; + } + + let llvm_output = builder.ensure(Llvm { target }); + + static STAMP_HASH_MEMO: OnceLock = OnceLock::new(); + let smart_stamp_hash = STAMP_HASH_MEMO.get_or_init(|| { + generate_smart_stamp_hash( + builder, + &builder.config.src.join("src/llvm-project/libc"), + builder.in_tree_llvm_info.sha().unwrap_or_default(), + ) + }); + let stamp = BuildStamp::new(&out_dir).with_prefix("gpu-libc").add_stamp(smart_stamp_hash); + + trace!("checking build stamp to see if we need to rebuild the gpu libc"); + if stamp.is_up_to_date() { + trace!(?out_dir, "gpu libc build artifacts are up to date"); + return BuiltGpuLibc { libs: files }; + } + + trace!(?target, ?gpu_target, "(re)building the gpu libc"); + let _guard = builder.msg_unstaged(Kind::Build, "gpu-libc", target); + t!(stamp.remove()); + let _time = helpers::timeit(builder); + t!(fs::create_dir_all(&out_dir)); + + builder.config.update_submodule("src/llvm-project"); + + let clang_bin_dir = offload_clang_bin_dir(builder, target, &llvm_output); + let clang = clang_bin_dir.join(exe("clang", target)); + let clangxx = clang_bin_dir.join(exe("clang++", target)); + + let mut program_dirs = vec![clang_bin_dir.clone()]; + if !clang_bin_dir.join(exe("ld.lld", target)).exists() { + program_dirs.push(builder.ensure(Lld { target }).join("bin")); + } + let program_flags = + program_dirs.iter().map(|d| format!("-B{}", d.display())).collect::>().join(" "); + + let mut cfg = cmake::Config::new(builder.src.join("src/llvm-project/runtimes/")); + + configure_cmake( + builder, + target, + &mut cfg, + true, + LdFlags::default(), + CcFlags::default(), + &[], + ); + + let profile = get_llvm_profile(&builder.config); + trace!(?profile); + + let llvm_version_major = get_llvm_version_major(builder, &llvm_output.host_llvm_config); + + if llvm_output.link_shared() { + let mut dylib_path = vec![builder.llvm_out(target).join("lib")]; + dylib_path.extend(helpers::dylib_path()); + cfg.env(helpers::dylib_path_var(), t!(env::join_paths(dylib_path))); + } + + cfg.out_dir(&out_dir) + .profile(profile) + .define("LLVM_ENABLE_RUNTIMES", "libc") + .define("LLVM_LIBC_FULL_BUILD", "ON") + .define("LLVM_DEFAULT_TARGET_TRIPLE", gpu_target) + .define("LLVM_RUNTIMES_TARGET", gpu_target) + .define("LLVM_ENABLE_PER_TARGET_RUNTIME_DIR", "ON") + .define("LLVM_ENABLE_ASSERTIONS", "ON") + .define("LLVM_INCLUDE_TESTS", "OFF") + .define("LLVM_ROOT", builder.llvm_out(target).join("build")) + .define("LLVM_DIR", llvm_output.cmake_dir()) + .define("LIBC_NAMESPACE", format!("__llvm_libc_{llvm_version_major}")) + .define("CMAKE_C_COMPILER", &clang) + .define("CMAKE_CXX_COMPILER", &clangxx) + // We already require a suitable clang to build this step. Every working toolchain + // should build ar/ranlib along with clang, so we just directly specify them, to avoid + // relying on a (potentially incompatible) host ar/ranlib. + .define("CMAKE_AR", clang_bin_dir.join(exe("llvm-ar", target))) + .define("CMAKE_RANLIB", clang_bin_dir.join(exe("llvm-ranlib", target))) + .define("CMAKE_C_COMPILER_TARGET", gpu_target) + .define("CMAKE_CXX_COMPILER_TARGET", gpu_target) + .define("CMAKE_C_COMPILER_WORKS", "ON") + .define("CMAKE_CXX_COMPILER_WORKS", "ON") + .define("CMAKE_ASM_COMPILER_WORKS", "ON") + .define("CMAKE_C_FLAGS", &program_flags) + .define("CMAKE_CXX_FLAGS", &program_flags); + + if let Some(p) = offload_clang_cmake_dir(builder) { + cfg.define("Clang_DIR", p); + } + + cfg.build(); + + for p in &files { + if !p.exists() { + eprintln!("Failed to build libc-for-gpu file: {p:?}, {}", out_dir.display()); + helpers::exit_process(1); + } + } + + t!(stamp.write()); + + BuiltGpuLibc { libs: files } + } +} + #[derive(Clone)] pub struct BuiltEnzyme { /// Path to the libEnzyme dylib. @@ -1573,7 +1693,6 @@ impl CommandLineStep for Enzyme { cfg.out_dir(&out_dir) .profile(profile) - .env("LLVM_CONFIG_REAL", &llvm_output.host_llvm_config) .define("LLVM_ENABLE_ASSERTIONS", "ON") .define("ENZYME_EXTERNAL_SHARED_LIB", "ON") .define("ENZYME_BC_LOADER", "OFF") diff --git a/src/bootstrap/src/core/builder/mod.rs b/src/bootstrap/src/core/builder/mod.rs index 98fceeae9df5c..dd2867d2bb4be 100644 --- a/src/bootstrap/src/core/builder/mod.rs +++ b/src/bootstrap/src/core/builder/mod.rs @@ -780,6 +780,7 @@ impl<'a> Builder<'a> { llvm::Lld, llvm::Enzyme, llvm::RustOffload, + llvm::GpuLibc, llvm::CrtBeginEnd, tool::RustdocGUITest, tool::OptimizedDist, @@ -1547,6 +1548,14 @@ Alternatively, you can set `build.local-rebuild=true` and use a stage0 compiler } } + /// Root output directory of the OpenMP/Offload runtimes for `target` + /// + /// Deliberately not below [`Self::llvm_out`], since running cmake twice in the same folder is + /// known to cause issues, like deleting existing binaries. + pub fn offload_out(&self, target: TargetSelection) -> PathBuf { + self.out.join(target).join("offload") + } + /// Updates all submodules, and exits with an error if submodule /// management is disabled and the submodule does not exist. pub fn require_and_update_all_submodules(&self) { diff --git a/src/doc/rustc-dev-guide/src/offload/installation.md b/src/doc/rustc-dev-guide/src/offload/installation.md index ab8e7984d5b4a..8422d072bca3b 100644 --- a/src/doc/rustc-dev-guide/src/offload/installation.md +++ b/src/doc/rustc-dev-guide/src/offload/installation.md @@ -12,6 +12,10 @@ cd rust ./configure --enable-llvm-link-shared --release-channel=nightly --enable-llvm-assertions --enable-llvm-offload --enable-llvm-enzyme --enable-clang --enable-lld --enable-option-checking --enable-ninja --disable-docs ``` +If you would rather reuse an existing clang than build one, drop `--enable-clang` and pass +`--enable-llvm-offload-clang-dir=` +instead. It should match the (major version of the) LLVM in `src/llvm-project`. + Afterwards you can build rustc using: ```console ./x build --stage 1 library