use super::{CudaArchInfo, CudaDetectionMethod, CudaInfo, CudaInfoSources, DetectedCudaInfo};
use rattler_conda_types::Version;
use serde::{Deserialize, Serialize};
use std::{
io::Write,
path::{Path, PathBuf},
str::FromStr,
};
const CACHE_FILE_NAME: &str = "cuda-info-v1.json";
const FULL_TTL_SECS: u64 = 24 * 60 * 60;
const ARCH_MISSING_TTL_SECS: u64 = 10 * 60;
const MAX_CLOCK_SKEW_SECS: u64 = 5 * 60;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum BootId {
Uuid(String),
BootCount(u32),
BootTime(u64),
}
impl BootId {
pub(super) fn current() -> Option<Self> {
#[cfg(target_os = "linux")]
{
let id = std::fs::read_to_string("/proc/sys/kernel/random/boot_id").ok()?;
Some(Self::Uuid(id.trim().to_owned()))
}
#[cfg(target_os = "windows")]
{
if let Some(count) = windows_boot_count() {
return Some(Self::BootCount(count));
}
let uptime_secs =
unsafe { windows_sys::Win32::System::SystemInformation::GetTickCount64() } / 1000;
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
Some(Self::BootTime(now_secs.checked_sub(uptime_secs)?))
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
None
}
}
pub(super) fn matches(&self, other: &Self) -> bool {
match (self, other) {
(Self::BootTime(a), Self::BootTime(b)) => boot_times_within_tolerance(*a, *b),
_ => self == other,
}
}
}
pub(super) fn boot_times_within_tolerance(a: u64, b: u64) -> bool {
const BOOT_TIME_TOLERANCE_SECS: u64 = 120;
a.abs_diff(b) <= BOOT_TIME_TOLERANCE_SECS
}
#[cfg(target_os = "windows")]
fn windows_boot_count() -> Option<u32> {
use windows_sys::Win32::System::Registry::{
HKEY_LOCAL_MACHINE, RRF_RT_REG_DWORD, RegGetValueW,
};
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
let subkey = wide(
"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Memory Management\\PrefetchParameters",
);
let value = wide("BootId");
let mut data: u32 = 0;
let mut data_size: u32 = std::mem::size_of::<u32>() as u32;
let status = unsafe {
RegGetValueW(
HKEY_LOCAL_MACHINE,
subkey.as_ptr(),
value.as_ptr(),
RRF_RT_REG_DWORD,
std::ptr::null_mut(),
std::ptr::addr_of_mut!(data).cast::<std::ffi::c_void>(),
&mut data_size,
)
};
if status == 0 { Some(data) } else { None }
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum DriverFingerprint {
Module { version: String },
File {
path: PathBuf,
mtime_secs: u64,
len: u64,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub(super) struct DeviceFingerprint {
pub(super) gpus: Vec<String>,
pub(super) device_nodes: Vec<String>,
}
#[derive(Serialize, Deserialize)]
struct CacheFile {
boot_id: BootId,
driver_fingerprint: DriverFingerprint,
device_fingerprint: Option<DeviceFingerprint>,
written_at: u64,
version: String,
arch: Option<(u32, u32)>,
#[serde(default)]
version_source: Option<CudaDetectionMethod>,
#[serde(default)]
arch_source: Option<CudaDetectionMethod>,
}
pub(super) struct CacheEnv {
pub(super) boot_id: Option<BootId>,
pub(super) driver_fingerprint: Option<DriverFingerprint>,
pub(super) device_fingerprint: Option<DeviceFingerprint>,
pub(super) now: u64,
}
impl CacheEnv {
pub(super) fn current() -> Self {
Self {
boot_id: BootId::current(),
driver_fingerprint: driver_fingerprint(),
device_fingerprint: device_fingerprint(),
now: now_unix_secs(),
}
}
}
fn now_unix_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
#[cfg(target_os = "linux")]
const LIBNVIDIA_ML_ABSOLUTE_PATHS: &[&str] = &[
"/usr/lib64/nvidia/libnvidia-ml.so.1", "/usr/lib64/nvidia/libnvidia-ml.so",
"/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1", "/usr/lib/x86_64-linux-gnu/libnvidia-ml.so",
"/usr/lib/wsl/lib/libnvidia-ml.so.1", "/usr/lib/wsl/lib/libnvidia-ml.so",
];
#[cfg(any(target_os = "linux", target_os = "windows"))]
fn file_fingerprint(path: &Path) -> Option<DriverFingerprint> {
let metadata = std::fs::metadata(path).ok()?;
let mtime = metadata
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?;
Some(DriverFingerprint::File {
path: path.to_path_buf(),
mtime_secs: mtime.as_secs(),
len: metadata.len(),
})
}
fn driver_fingerprint() -> Option<DriverFingerprint> {
#[cfg(target_os = "linux")]
{
if let Ok(version) = std::fs::read_to_string("/sys/module/nvidia/version") {
return Some(DriverFingerprint::Module {
version: version.trim().to_owned(),
});
}
for path in LIBNVIDIA_ML_ABSOLUTE_PATHS {
if let Some(fingerprint) = file_fingerprint(Path::new(path)) {
return Some(fingerprint);
}
}
None
}
#[cfg(target_os = "windows")]
{
let mut candidates = Vec::new();
if let Some(windir) = std::env::var_os("WINDIR") {
candidates.push(Path::new(&windir).join("System32").join("nvml.dll"));
}
if let Some(program_files) = std::env::var_os("ProgramFiles") {
candidates.push(
Path::new(&program_files)
.join("NVIDIA Corporation")
.join("NVSMI")
.join("nvml.dll"),
);
}
for path in candidates {
if let Some(fingerprint) = file_fingerprint(&path) {
return Some(fingerprint);
}
}
None
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
None
}
}
#[cfg(target_os = "linux")]
fn is_nvidia_device_node(name: &str) -> bool {
name.strip_prefix("nvidia")
.is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()))
}
fn device_fingerprint() -> Option<DeviceFingerprint> {
#[cfg(target_os = "linux")]
{
let gpus_dir = std::fs::read_dir("/proc/driver/nvidia/gpus").ok();
let has_gpus_dir = gpus_dir.is_some();
let mut gpus: Vec<String> = gpus_dir
.map(|entries| {
entries
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect()
})
.unwrap_or_default();
gpus.sort();
let mut devices: Vec<String> = std::fs::read_dir("/dev")
.map(|entries| {
entries
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.filter(|name| is_nvidia_device_node(name))
.collect()
})
.unwrap_or_default();
devices.sort();
if !has_gpus_dir && devices.is_empty() {
return None;
}
Some(DeviceFingerprint {
gpus,
device_nodes: devices,
})
}
#[cfg(target_os = "windows")]
{
let mut gpus = windows_nvidia_device_ids()?;
gpus.sort();
Some(DeviceFingerprint {
gpus,
device_nodes: Vec::new(),
})
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
None
}
}
#[cfg(target_os = "windows")]
fn windows_nvidia_device_ids() -> Option<Vec<String>> {
use windows_sys::Win32::Devices::DeviceAndDriverInstallation::{
CM_GETIDLIST_FILTER_ENUMERATOR, CM_GETIDLIST_FILTER_PRESENT, CM_Get_Device_ID_List_SizeW,
CM_Get_Device_ID_ListW, CR_SUCCESS,
};
let filter: Vec<u16> = "PCI".encode_utf16().chain(std::iter::once(0)).collect();
let flags = CM_GETIDLIST_FILTER_ENUMERATOR | CM_GETIDLIST_FILTER_PRESENT;
let mut len: u32 = 0;
if unsafe { CM_Get_Device_ID_List_SizeW(&mut len, filter.as_ptr(), flags) } != CR_SUCCESS {
return None;
}
let mut buffer = vec![0u16; len as usize];
if unsafe { CM_Get_Device_ID_ListW(filter.as_ptr(), buffer.as_mut_ptr(), len, flags) }
!= CR_SUCCESS
{
return None;
}
Some(
buffer
.split(|&c| c == 0)
.filter(|segment| !segment.is_empty())
.map(String::from_utf16_lossy)
.filter(|id| is_nvidia_pci_device_id(id))
.collect(),
)
}
#[cfg(any(target_os = "windows", test))]
pub(super) fn is_nvidia_pci_device_id(id: &str) -> bool {
id.to_ascii_uppercase().contains("VEN_10DE")
}
pub(super) fn read_with_env(env: &CacheEnv, cache_dir: &Path) -> Option<DetectedCudaInfo> {
let path = cache_dir.join(CACHE_FILE_NAME);
let Ok(content) = std::fs::read_to_string(&path) else {
tracing::trace!("no CUDA info cache found at {}", path.display());
return None;
};
let Ok(cached) = serde_json::from_str::<CacheFile>(&content) else {
tracing::debug!("ignoring invalid CUDA info cache at {}", path.display());
return None;
};
let Some(current_boot_id) = env.boot_id.as_ref() else {
tracing::debug!(
"ignoring CUDA info cache because the current boot id could not be determined"
);
return None;
};
if !cached.boot_id.matches(current_boot_id) {
tracing::info!(
cache_path = %path.display(),
cached_boot_id = ?cached.boot_id,
current_boot_id = ?current_boot_id,
"invalidating CUDA info cache from a previous boot session"
);
return None;
}
let Some(current_driver_fingerprint) = env.driver_fingerprint.as_ref() else {
tracing::debug!(
"ignoring CUDA info cache because the current driver fingerprint could not be determined"
);
return None;
};
if &cached.driver_fingerprint != current_driver_fingerprint {
tracing::info!(
cache_path = %path.display(),
cached_driver_fingerprint = ?cached.driver_fingerprint,
current_driver_fingerprint = ?current_driver_fingerprint,
"invalidating CUDA info cache because the driver changed"
);
return None;
}
if cached.device_fingerprint != env.device_fingerprint {
tracing::info!(
cache_path = %path.display(),
cached_device_fingerprint = ?cached.device_fingerprint,
current_device_fingerprint = ?env.device_fingerprint,
"invalidating CUDA info cache because the visible GPUs changed"
);
return None;
}
if cached.written_at.saturating_sub(env.now) > MAX_CLOCK_SKEW_SECS {
tracing::debug!(
cache_path = %path.display(),
written_at = cached.written_at,
now = env.now,
"ignoring CUDA info cache written in the future"
);
return None;
}
let ttl = if cached.arch.is_some() {
FULL_TTL_SECS
} else {
ARCH_MISSING_TTL_SECS
};
if env.now.saturating_sub(cached.written_at) > ttl {
tracing::info!(
cache_path = %path.display(),
written_at = cached.written_at,
now = env.now,
ttl,
"invalidating expired CUDA info cache"
);
return None;
}
let version = match Version::from_str(&cached.version) {
Ok(version) => version,
Err(err) => {
tracing::debug!(
version = cached.version,
error = %err,
"ignoring CUDA info cache with invalid version"
);
return None;
}
};
tracing::trace!("using CUDA info cached at {}", path.display());
Some(DetectedCudaInfo {
info: CudaInfo {
version: Some(version),
arch_info: cached
.arch
.map(|(major, minor)| CudaArchInfo { major, minor }),
},
sources: CudaInfoSources {
version: cached.version_source,
arch: cached.arch_source,
},
})
}
pub(super) fn write_with_env(
env: &CacheEnv,
cache_dir: &Path,
info: &CudaInfo,
sources: CudaInfoSources,
) {
let Some(version) = &info.version else {
tracing::trace!("not caching CUDA info because no CUDA driver version was detected");
return;
};
let Some(boot_id) = env.boot_id.clone() else {
tracing::debug!(
"not caching CUDA info because the current boot id could not be determined"
);
return;
};
let Some(driver_fingerprint) = env.driver_fingerprint.clone() else {
tracing::debug!(
"not caching CUDA info because the current driver fingerprint could not be determined"
);
return;
};
if let Err(err) = std::fs::create_dir_all(cache_dir) {
tracing::debug!(
cache_dir = %cache_dir.display(),
error = %err,
"failed to create CUDA info cache directory"
);
return;
}
let cached = CacheFile {
boot_id,
driver_fingerprint,
device_fingerprint: env.device_fingerprint.clone(),
written_at: env.now,
version: version.to_string(),
arch: info.arch_info.as_ref().map(|arch| (arch.major, arch.minor)),
version_source: sources.version,
arch_source: sources.arch,
};
let Ok(content) = serde_json::to_string(&cached) else {
tracing::debug!("failed to serialize CUDA info cache entry");
return;
};
let path = cache_dir.join(CACHE_FILE_NAME);
let mut tmp = match tempfile::NamedTempFile::new_in(cache_dir) {
Ok(tmp) => tmp,
Err(err) => {
tracing::debug!(
cache_dir = %cache_dir.display(),
error = %err,
"failed to create temporary CUDA info cache file"
);
return;
}
};
if let Err(err) = tmp.write_all(content.as_bytes()) {
tracing::debug!(
cache_path = %path.display(),
error = %err,
"failed to write temporary CUDA info cache file"
);
return;
}
match tmp.persist(&path) {
Ok(_) => tracing::trace!("cached CUDA info at {}", path.display()),
Err(err) => {
tracing::debug!(
cache_path = %path.display(),
error = %err.error,
"failed to persist CUDA info cache"
);
}
}
}