From 4c910d347416c72369cf342b4426d2458cc5c2a0 Mon Sep 17 00:00:00 2001 From: Mikhail Borisov Date: Sun, 11 Oct 2026 22:17:57 +0500 Subject: [PATCH] fix(codegen): keep the artifact objects of rlibs out of the LTO input With `lto = "fat"` or `lto = "thin"`, the build of a binary that uses a library with kernels stopped with an error: "failed to get bitcode from object file for LTO (Can't find section .llvmbc)". rustc keeps a list of the Rust objects in each rlib. This list contains the objects of all modules. With LTO, rustc reads bitcode from these objects and removes them from the link. The artifact objects have no bitcode. If a session writes an rlib, the backend now gives each artifact object to rustc as the DWARF object of its module. This object is in the rlib, but it is not in the list. LTO does not read this object, and the linker gets it from the rlib. The system linker does not get the DWARF objects. If the session also writes a binary or a dynamic library, the backend gives the artifact objects to this linker as link arguments. With `-Zlink-native-libraries=no`, rustc does not give link arguments to the linker. Then the artifact objects of this session are Rust objects, as before. The book said that `join_codegen()` and `link()` only call the LLVM backend. Now it tells what they add to the outputs. CI builds `cuda_module_in_lib` with fat LTO. The library is an rlib, and then an rlib and a cdylib. After each build, CI makes sure that the binary has the PTX bundles of the library and of the binary. It also makes sure that the cdylib has the PTX bundle of the library. Fixes #1495 Signed-off-by: Mikhail Borisov Co-Authored-By: Claude Opus 5.5 --- .github/workflows/examples-compile.yml | 45 +++++ .../crates/rustc-codegen-cuda/src/lib.rs | 170 ++++++++++++++++-- .../compiler/rustc-codegen-cuda.md | 21 +-- 3 files changed, 212 insertions(+), 24 deletions(-) diff --git a/.github/workflows/examples-compile.yml b/.github/workflows/examples-compile.yml index 9efa9bd73..5f6a3675e 100644 --- a/.github/workflows/examples-compile.yml +++ b/.github/workflows/examples-compile.yml @@ -254,6 +254,51 @@ jobs: "${RUNNER_TEMP}/cross-crate-sm90-build.log" \ '^(kernel-lib|cross_crate_kernel)$' kernel-lib cross_crate_kernel + # With LTO, rustc reads bitcode from the Rust objects of each rlib and + # removes these objects from the link. The artifact objects of the + # libraries must not be Rust objects. + - name: LTO keeps the PTX bundles of libraries + timeout-minutes: 15 + working-directory: cuda-oxide + env: + CUDA_TOOLKIT_PATH: ${{ steps.cuda-toolkit.outputs.CUDA_PATH }} + CUDA_OXIDE_LLC: /usr/bin/llc-22 + CARGO_TARGET_DIR: ${{ runner.temp }}/examples-shared-target + CARGO_INCREMENTAL: "0" + CARGO_PROFILE_RELEASE_LTO: fat + run: | + set -euo pipefail + sysroot="$(rustc --print sysroot)" + export LIBRARY_PATH="${sysroot}/lib:${LIBRARY_PATH:-}" + export LD_LIBRARY_PATH="${CUDA_TOOLKIT_PATH}/lib64:${sysroot}/lib:${LD_LIBRARY_PATH:-}" + + assert_bundles() { + local file="$1" + shift + local section="${RUNNER_TEMP}/$(basename "${file}").oxart" + objcopy --dump-section ".oxart=${section}" "${file}" + local bundle + for bundle in "$@"; do + grep -aFq "${bundle}" "${section}" + done + } + binary="${CARGO_TARGET_DIR}/release/cuda_module_in_lib" + + cargo oxide build cuda_module_in_lib --arch sm_86 2>&1 \ + | tee "${RUNNER_TEMP}/cross-crate-lto-build.log" + assert_bundles "${binary}" cuda_module_in_lib module-kernels + + # If a session writes an rlib and a dynamic library, the system + # linker gets the artifact objects as link arguments. + manifest=crates/rustc-codegen-cuda/examples/cuda_module_in_lib/kernel-lib/Cargo.toml + printf '\n[lib]\ncrate-type = ["rlib", "cdylib"]\n' >> "${manifest}" + cargo oxide build cuda_module_in_lib --arch sm_86 2>&1 \ + | tee "${RUNNER_TEMP}/cross-crate-lto-cdylib-build.log" + git checkout -- "${manifest}" + assert_bundles "${binary}" cuda_module_in_lib module-kernels + assert_bundles \ + "$(find "${CARGO_TARGET_DIR}/release" -name libmodule_kernels.so)" module-kernels + - name: Upload failure logs if: failure() uses: actions/upload-artifact@v4 diff --git a/cuda-oxide/crates/rustc-codegen-cuda/src/lib.rs b/cuda-oxide/crates/rustc-codegen-cuda/src/lib.rs index e0e736684..d6232c4d9 100644 --- a/cuda-oxide/crates/rustc-codegen-cuda/src/lib.rs +++ b/cuda-oxide/crates/rustc-codegen-cuda/src/lib.rs @@ -316,6 +316,7 @@ extern crate rustc_metadata; extern crate rustc_middle; extern crate rustc_session; extern crate rustc_span; +extern crate rustc_structures; extern crate rustc_target; // rustc_public (stable MIR) and its bridge - for calling mir-importer @@ -331,13 +332,15 @@ mod generated_intrinsics; mod materialize; use rustc_codegen_ssa::traits::CodegenBackend; -use rustc_codegen_ssa::{CompiledModule, CompiledModules, CrateInfo, ModuleKind}; +use rustc_codegen_ssa::{CompiledModule, CompiledModules, CrateInfo, ModuleKind, NativeLib}; use rustc_metadata::EncodedMetadata; use rustc_middle::dep_graph::WorkProductMap; use rustc_middle::ty::TyCtxt; use rustc_middle::ty::print::with_no_trimmed_paths; use rustc_session::config::OutputFilenames; use rustc_session::{CodegenBackendInit, EarlySession, IncrCompSession, Session}; +use rustc_span::Symbol; +use rustc_structures::{CrateType, NativeLibKind}; use std::any::Any; use std::collections::BTreeSet; use std::path::{Path, PathBuf}; @@ -822,16 +825,12 @@ impl CodegenBackend for CudaCodegenBackend { crate_info, ); for (index, object) in ongoing.artifact_objects.into_iter().enumerate() { - compiled_modules.modules.push(CompiledModule { - name: format!("oxide_artifact_embed_{index}"), - kind: ModuleKind::Regular, - object: Some(object), - dwarf_object: None, - bytecode: None, - assembly: None, - llvm_ir: None, - global_asm_object: None, - }); + compiled_modules.modules.push(artifact_module( + index, + object, + &crate_info.crate_types, + sess.opts.unstable_opts.link_native_libraries, + )); } (compiled_modules, work_products) } @@ -840,15 +839,91 @@ impl CodegenBackend for CudaCodegenBackend { &self, sess: &Session, compiled_modules: CompiledModules, - crate_info: CrateInfo, + mut crate_info: CrateInfo, metadata: EncodedMetadata, outputs: &OutputFilenames, ) { + for object in artifact_link_args(&compiled_modules.modules, &crate_info.crate_types) { + let Some(object) = object.to_str() else { + sess.dcx().fatal(format!( + "[rustc_codegen_cuda] The path of the artifact object is not UTF-8: {}", + object.display() + )); + }; + crate_info.used_libraries.push(NativeLib { + kind: NativeLibKind::LinkArg, + name: Symbol::intern(object), + cfg: None, + verbatim: true, + dll_imports: Vec::new(), + }); + } self.llvm_backend .link(sess, compiled_modules, crate_info, metadata, outputs); } } +/// The prefix of the names of the artifact modules. +const ARTIFACT_MODULE_PREFIX: &str = "oxide_artifact_embed_"; + +/// The module of an artifact object. +/// +/// rustc keeps a list of the Rust objects in each rlib. This list contains the +/// objects of all modules. With LTO, rustc reads bitcode from these objects and +/// removes them from the link. The artifact objects have no bitcode. If the +/// session writes an rlib, the artifact object is the DWARF object of its +/// module. rustc puts the DWARF objects in the rlib, but not in the list. +/// +/// The system linker does not get the DWARF objects. `link` gives the artifact +/// objects to this linker as link arguments. With `-Zlink-native-libraries=no`, +/// rustc does not give link arguments to the linker. Then, if the session also +/// uses the system linker, the artifact objects are Rust objects. +fn artifact_module( + index: usize, + object: PathBuf, + crate_types: &[CrateType], + link_native_libraries: bool, +) -> CompiledModule { + let dwarf = crate_types.contains(&CrateType::Rlib) + && (link_native_libraries || !crate_types.iter().any(uses_system_linker)); + let (object, dwarf_object) = if dwarf { + (None, Some(object)) + } else { + (Some(object), None) + }; + CompiledModule { + name: format!("{ARTIFACT_MODULE_PREFIX}{index}"), + kind: ModuleKind::Regular, + object, + dwarf_object, + bytecode: None, + assembly: None, + llvm_ir: None, + global_asm_object: None, + } +} + +/// The artifact objects that `link` gives to the system linker. +fn artifact_link_args<'a>( + modules: &'a [CompiledModule], + crate_types: &[CrateType], +) -> Vec<&'a Path> { + if !crate_types.iter().any(uses_system_linker) { + return Vec::new(); + } + modules + .iter() + .filter(|module| module.name.starts_with(ARTIFACT_MODULE_PREFIX)) + .filter_map(|module| module.dwarf_object.as_deref()) + .collect() +} + +/// rustc writes rlibs and static libraries. The system linker makes the other +/// crate types. +fn uses_system_linker(crate_type: &CrateType) -> bool { + !matches!(crate_type, CrateType::Rlib | CrateType::StaticLib) +} + #[allow(clippy::too_many_arguments)] fn write_device_artifact_object( tcx: TyCtxt<'_>, @@ -1217,6 +1292,77 @@ pub(crate) fn host_target_supported( mod tests { use super::*; + #[test] + fn the_artifact_objects_of_an_rlib_are_not_rust_objects() { + let object = PathBuf::from("library.0.0.embed.o"); + let rlib_sessions: [&[CrateType]; 6] = [ + &[CrateType::Rlib], + &[CrateType::Rlib, CrateType::Cdylib], + &[CrateType::Rlib, CrateType::Dylib], + &[CrateType::Rlib, CrateType::StaticLib], + &[CrateType::Rlib, CrateType::StaticLib, CrateType::Cdylib], + &[CrateType::Rlib, CrateType::StaticLib, CrateType::Dylib], + ]; + for crate_types in rlib_sessions { + let module = artifact_module(0, object.clone(), crate_types, true); + assert_eq!(module.object, None, "{crate_types:?}"); + assert_eq!(module.dwarf_object, Some(object.clone()), "{crate_types:?}"); + } + + // Sessions without an rlib link the artifact objects. + let other_sessions: [&[CrateType]; 4] = [ + &[CrateType::Executable], + &[CrateType::Cdylib], + &[CrateType::Dylib], + &[CrateType::StaticLib], + ]; + for crate_types in other_sessions { + let module = artifact_module(0, object.clone(), crate_types, true); + assert_eq!(module.object, Some(object.clone()), "{crate_types:?}"); + assert_eq!(module.dwarf_object, None, "{crate_types:?}"); + } + + // With `-Zlink-native-libraries=no`, the system linker gets no link + // arguments. + let mixed = artifact_module( + 0, + object.clone(), + &[CrateType::Rlib, CrateType::Cdylib], + false, + ); + assert_eq!(mixed.object, Some(object.clone())); + let rlib = artifact_module(0, object.clone(), &[CrateType::Rlib], false); + assert_eq!(rlib.dwarf_object, Some(object)); + } + + #[test] + fn the_system_linker_gets_the_artifact_objects() { + let artifact = PathBuf::from("library.0.0.embed.o"); + let crate_types = [CrateType::Rlib, CrateType::Cdylib]; + let modules = [ + // A module of LLVM with split debug information. + CompiledModule { + name: "library.0".to_string(), + kind: ModuleKind::Regular, + object: Some(PathBuf::from("library.0.rcgu.o")), + dwarf_object: Some(PathBuf::from("library.0.rcgu.dwo")), + bytecode: None, + assembly: None, + llvm_ir: None, + global_asm_object: None, + }, + artifact_module(0, artifact.clone(), &crate_types, true), + ]; + assert_eq!( + artifact_link_args(&modules, &crate_types), + [artifact.as_path()] + ); + + // rustc puts the DWARF objects in rlibs and static libraries. + assert!(artifact_link_args(&modules, &[CrateType::Rlib]).is_empty()); + assert!(artifact_link_args(&modules, &[CrateType::Rlib, CrateType::StaticLib]).is_empty()); + } + #[test] fn host_targets_that_match_the_ptx_data_model_are_accepted() { use rustc_abi::Endian; diff --git a/cuda-oxide/cuda-oxide-book/compiler/rustc-codegen-cuda.md b/cuda-oxide/cuda-oxide-book/compiler/rustc-codegen-cuda.md index 683605591..daf3b6d91 100644 --- a/cuda-oxide/cuda-oxide-book/compiler/rustc-codegen-cuda.md +++ b/cuda-oxide/cuda-oxide-book/compiler/rustc-codegen-cuda.md @@ -122,20 +122,17 @@ the host pipeline always runs. The `CodegenBackend` trait also requires `join_codegen()` and `link()`. These handle waiting for parallel codegen threads and invoking the linker, -respectively. In cuda-oxide, both are pure delegation: +respectively. In cuda-oxide, both call the LLVM backend. The backend also +writes artifact objects for the embedded artifact bundles. `join_codegen()` and +`link()` make sure that the outputs contain these objects: -```rust -fn join_codegen(&self, ongoing: Box, sess: &Session) -> ... { - self.llvm_backend.join_codegen(ongoing, sess) -} - -fn link(&self, sess: &Session, codegen: ..., outputs: &OutputFilenames) -> ... { - self.llvm_backend.link(sess, codegen, outputs) -} -``` +- `join_codegen()` adds a module for each artifact object. +- `link()` can give artifact objects to the system linker as link arguments. -No interception, no modification. The LLVM backend handles linking exactly as -it normally would. +With LTO, rustc reads bitcode from the Rust objects of each rlib. The artifact +objects contain no bitcode. The doc comment of `artifact_module()` shows which +artifact objects are Rust objects and which `link()` gives to the system +linker. ```{note} For most crates in the dependency tree -- `core`, `cuda-core`, random