Rust in Everything
基于 tenferro-runtime 0.8.0

Unable to dynamically load the "cuda" shared library

主线位置:直接计算主线换成 CUDA 引擎之前的第一步:发现设备、打开引擎。CUDA 引擎的结构和没有驱动时三个入口各自的行为见 CUDA(只读源码) 的「没有驱动时」。本机没有 CUDA,下面只复现「没有驱动」这一种情形;有驱动时的行为只读了源码。

flowchart TB
  start["选引擎"] --> dev["cuda_devices"]
  dev --> drv{"有 libcuda"}
  drv -->|有| ok["Ok 设备列表"]
  drv -->|没有| panic["cudarc panic"]
  panic --> exit["exit 101"]
  start --> probe["gpu_available"]
  probe -->|false| cpu["用 CpuEngine"]
  probe -->|true| dev

实验 crate exp-tenferro8/pit2-cuda(从 gpu-cuda 拷来,依赖 tenferro-gpu 的 cuda feature),debug,M2 Max、macOS 27、rustc 1.99.0,本机没有 CUDA 驱动。panic = "abort" 的对照用 CARGO_PROFILE_DEV_PANIC=abort 另编了一份。

现象

照上游例子 cuda_quickstart(用户指南 devices-and-gpu.md 的「CUDA Quickstart」一节原样收录)写,第一行就是 cuda_devices()?:

fn quickstart() -> Result<(), Box<dyn std::error::Error>> {
  let Some(device) = cuda_devices()?.into_iter().next() else {
    println!("no CUDA device");
    return Ok(());
  };
  let _engine = CudaEngine::open(device.id())?;
  Ok(())
}

thread 'main' (9769242) panicked at .../cudarc-0.19.10/src/lib.rs:200:5:
Unable to dynamically load the "cuda" shared library - searched for library names: ["libcuda.dylib", "libcuda64.dylib", "libcuda64_12.dylib", ...]. Ensure that `LD_LIBRARY_PATH` has the correct path to the installed library. If the shared library is present on the system under a different name than one of those listed above, please open a GitHub issue.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101

(库名列表有 34 项,中间省略。)函数签名是 Result,调用方写了 ?,期望在没有 CUDA 的机器上拿到一个错误、退回 CPU;实际是 panic,? 根本没机会起作用,连 no CUDA device 那个分支也走不到。程序要是想「有 GPU 用 GPU、没有用 CPU」,在 Mac 和没装驱动的 CI 机器上会直接退出。CudaEngine::open 在没有驱动时同样 panic(CUDA(只读源码) 用 RUST_BACKTRACE=1 追过它的路径:open 先调 cuda_devices)。

出处

cuda_devices 的文档写的是返回错误:

/// # Errors
///
/// Returns [`CudaDeviceError::Discovery`] for driver initialization, device
/// enumeration, or device-name lookup failures.
// ...
pub fn cuda_devices() -> Result<Vec<CudaDeviceInfo>, CudaDeviceError> {
    discover_with(&CudaDriverApi)
}

discover_with 的第一步是 driver.initialize(),即 cudarc::driver::result::init()。cudarc 0.19.10 开了动态加载,第一次调驱动函数时才去找 libcuda,找不到就在它自己的 src/lib.rs 第 200 行 panic,map_err 根本拿不到一个 Err。

gpu_available 走的是另一条路:

pub fn gpu_available() -> bool {
    let available = t4a_cuda_runtime::device_count()
        .map(|count| count > 0)
        .unwrap_or(false);

t4a-cuda-runtime 0.1.0 先用 is_culib_present() 探测驱动库,不在就返回类型化的 DriverUnavailable,这里当成 false。同一个 crate 里,两个入口一个走 t4a-cuda-runtime、一个直接调 cudarc,行为不一样。上游知道 cudarc 会 panic:CudaDeviceInfo 几个方法的文档示例都写着「cudarc panics when the CUDA driver library is absent, so the call is guarded」,用 catch_unwind(cuda_devices) 包起来;但 cuda_devices 自己的文档和 cuda_quickstart 都直接 ?。用户指南后面另一段 CUDA 代码先 if !gpu_available() 再 cuda_devices()?,两段写法不一致。函数文档承诺返回错误、实际 panic,这是 0.8.0 仍在的上游 bug(0.7.1 一样,那时的 CUDA 后端还是 CubeCL)。

修法

先探测,再发现设备:

fn guarded() -> Result<(), Box<dyn std::error::Error>> {
  if !gpu_available() {
    println!("gpu_available() = false, using the CPU engine");
    return Ok(());
  }
  quickstart()
}
gpu_available() = false, using the CPU engine
exit=0

或者照 CudaDeviceInfo 文档示例的写法,用 catch_unwind 包住:

match catch_unwind(cuda_devices) {
  Ok(r) => println!("cuda_devices() returned {:?}", r.map(|d| d.len())),
  Err(_) => println!("cuda_devices() panicked; treating as no CUDA"),
}

thread 'main' (9769254) panicked at .../cudarc-0.19.10/src/lib.rs:200:5:
Unable to dynamically load the "cuda" shared library - searched for library names: [...]. ...
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
cuda_devices() panicked; treating as no CUDA
exit=0
  • gpu_available() 更干净:不打印 panic 信息。它在 0.8.0 标了 #[doc(hidden)],文档里看不到,但它是公开函数,用户指南也在用。设了环境变量 TENFERRO_REQUIRE_GPU 时它找不到设备会故意 panic(给 GPU 测试机用的),普通程序别设。
  • catch_unwind 版本能工作,但默认的 panic hook 仍然把整段消息打到 stderr。用 panic = "abort" 编译的程序根本接不住:同一个 catch 分支以 134 退出(SIGABRT),而 gpu_available() 那个分支照样打印 false 并以 0 退出。
  • 有驱动时 gpu_available() 会初始化驱动(cuInit)再数设备;启动时调一次、记住结果即可。

怎么确认没中招

  • 在一台没有 CUDA 的机器(Mac 就行)上把选引擎的那段代码跑一次,进程应当以 0 退出并走 CPU 分支。
  • 搜 cuda_devices( 和 CudaEngine::open(,确认每一处之前都有 gpu_available() 或 catch_unwind。
  • 检查 profile 里有没有 panic = "abort";有的话只能用 gpu_available()。

小结

  • 没有 CUDA 驱动时,cuda_devices() 和 CudaEngine::open 在 cudarc 里 panic(Unable to dynamically load the "cuda" shared library),进程以 101 退出,不返回文档承诺的 CudaDeviceError。0.8.0 仍在的上游 bug。
  • 上游例子 cuda_quickstart 直接 cuda_devices()?,在没有驱动的机器上同样会崩;gpu_available() 走 t4a-cuda-runtime 的探测,返回 false。
  • 先调 gpu_available(),或用 catch_unwind 包住;panic = "abort" 时只有前者可用。

💬 关联讨论

发起讨论
加载中...