diff --git a/README.md b/README.md index 437beb89..93438a81 100644 --- a/README.md +++ b/README.md @@ -362,8 +362,20 @@ There is also a cli available. disabled ``` -The functionality of the CLI is quite limited. If you want to integrate LACT -with some application/script, you should use the [API](./docs/API.md) instead. + - Detach GPU (makes LACT temporarily ignore it): + + ``` + lact cli --gpu-id=10DE:2704-1462:5110-0000:01:00.0 detach + ``` + + - Reattach GPU: + + ``` + lact cli --gpu-id=10DE:2704-1462:5110-0000:01:00.0 reattach + ``` + +Note that not all functionality is exposed through the CLI. If you want to integrate LACT +with some application/script, you can use the [API](./docs/API.md) instead. # Reporting issues diff --git a/lact-cli/src/lib.rs b/lact-cli/src/lib.rs index 3869e4f3..1072a2e5 100644 --- a/lact-cli/src/lib.rs +++ b/lact-cli/src/lib.rs @@ -1,8 +1,8 @@ mod subcommands; use crate::subcommands::{ - current_auto_switch, current_profile, info, list_gpus, list_profiles, power_limit, - set_auto_switch, set_profile, snapshot, stats, + current_auto_switch, current_profile, detach, info, list_gpus, list_profiles, power_limit, + reattach, set_auto_switch, set_profile, snapshot, stats, }; use anyhow::{Context, Result, bail}; use lact_client::DaemonClient; @@ -57,6 +57,8 @@ pub fn run(args: CliArgs) -> Result<()> { } }, }, + CliCommand::Detach => detach(ctx).await, + CliCommand::Reattach => reattach(ctx).await, } }) } @@ -131,4 +133,14 @@ impl CliContext<'_> { Ok(()) } + + async fn name_for_id(&self, gpu_id: &str) -> anyhow::Result> { + Ok(self + .client + .list_devices() + .await? + .into_iter() + .find(|entry| entry.id == gpu_id) + .and_then(|entry| entry.name)) + } } diff --git a/lact-cli/src/subcommands.rs b/lact-cli/src/subcommands.rs index 1a8c96f3..156b242a 100644 --- a/lact-cli/src/subcommands.rs +++ b/lact-cli/src/subcommands.rs @@ -233,3 +233,40 @@ pub async fn set_auto_switch( } Ok(()) } + +pub async fn detach(ctx: CliContext<'_>) -> Result<()> { + let id = ctx + .args + .gpu_id + .as_deref() + .context("`--gpu-id` must be passed explicitly for reattaching")?; + let name = ctx.name_for_id(id).await?; + ctx.client.detach(id).await?; + + if let Some(name) = name { + println!("Detached GPU '{id}' ({name})"); + } else { + println!("Detached GPU '{id}'"); + } + + Ok(()) +} + +pub async fn reattach(ctx: CliContext<'_>) -> Result<()> { + let id = ctx + .args + .gpu_id + .as_deref() + .context("`--gpu-id` must be passed explicitly for reattaching")?; + ctx.client.reattach(id).await?; + + let name = ctx.name_for_id(id).await?; + + if let Some(name) = name { + println!("Reattached GPU '{id}' ({name})"); + } else { + println!("Reattached GPU '{id}'"); + } + + Ok(()) +} diff --git a/lact-client/src/lib.rs b/lact-client/src/lib.rs index 08a16c1f..1e167ae9 100644 --- a/lact-client/src/lib.rs +++ b/lact-client/src/lib.rs @@ -156,6 +156,8 @@ impl DaemonClient { request_with_id!(dump_vbios, VbiosDump, Vec); request_with_id!(get_process_list, ProcessList, ProcessList); request_with_id!(get_displays_info, DisplaysInfo, DisplaysInfo); + request_with_id!(detach, DetachGpu, ()); + request_with_id!(reattach, ReattachGpu, ()); pub async fn list_profiles(&self, include_state: bool) -> anyhow::Result { self.make_request(Request::ListProfiles { include_state }) diff --git a/lact-daemon/src/server.rs b/lact-daemon/src/server.rs index 99829ce6..dd0bd385 100644 --- a/lact-daemon/src/server.rs +++ b/lact-daemon/src/server.rs @@ -238,6 +238,8 @@ async fn handle_request<'a>( ), Request::ReleaseProfile { cookie } => ok_response(handler.release_profile(cookie).await?), Request::EvaluateProfileRule { rule } => ok_response(handler.evaluate_profile_rule(&rule)?), + Request::DetachGpu { id } => ok_response(handler.detach_gpu(id).await?), + Request::ReattachGpu { id } => ok_response(handler.reattach_gpu(id).await?), Request::SetProfileRule { name, rule, hooks } => { ok_response(handler.set_profile_rule(&name, rule, hooks, ctx).await?) } diff --git a/lact-daemon/src/server/gpu_controller.rs b/lact-daemon/src/server/gpu_controller.rs index 3b5ca687..7ec77d2f 100644 --- a/lact-daemon/src/server/gpu_controller.rs +++ b/lact-daemon/src/server/gpu_controller.rs @@ -17,7 +17,8 @@ use tokio::join; pub const VENDOR_AMD: &str = "1002"; pub const VENDOR_NVIDIA: &str = "10DE"; -use crate::server::handler::{AMD_DRM, INTEL_DRM, NVML}; +use crate::config::Config; +use crate::server::handler::{AMD_DRM, INTEL_DRM}; use crate::server::opencl::get_opencl_info; use crate::server::vulkan::get_vulkan_info; use amdgpu_sysfs::gpu_handle::power_profile_mode::PowerProfileModesTable; @@ -29,16 +30,12 @@ use lact_schema::{ }; use std::io; #[cfg(feature = "nvidia")] -use std::sync::Arc; +use std::sync::LazyLock; +use std::sync::Mutex; use std::{collections::HashMap, fs, path::PathBuf, rc::Rc}; use tokio::{sync::Notify, task::JoinHandle}; use tracing::{error, warn}; -#[cfg(feature = "nvidia")] -pub use nvidia::nvapi::NvApi; -#[cfg(feature = "nvidia")] -use nvml_wrapper::Nvml; - pub type DynGpuController = Box; type FanControlHandle = (Rc, JoinHandle<()>); @@ -166,18 +163,10 @@ pub struct PciSlotInfo { pub func: u16, } -#[cfg(feature = "nvidia")] -pub type NvidiaLibs = (Arc, Arc>); -#[cfg(not(feature = "nvidia"))] -pub type NvidiaLibs = (); - -pub(crate) fn init_controller( +pub(crate) fn build_controller_info( path: PathBuf, pci_db: &pciid_parser::Database, -) -> anyhow::Result> { - #[cfg(not(feature = "nvidia"))] - let _ = NVML; - +) -> anyhow::Result { let uevent_path = path.join("uevent"); let uevent = fs::read_to_string(uevent_path).context("Could not read 'uevent'")?; let mut uevent_map = parse_uevent(&uevent); @@ -239,12 +228,69 @@ pub(crate) fn init_controller( pci_info.subsystem_pci_info.model = get_embedded_device_name(&pci_info).or(pci_info.subsystem_pci_info.model); - let common = CommonControllerInfo { + Ok(CommonControllerInfo { sysfs_path: path, pci_info, pci_slot_name, driver, - }; + }) +} + +pub(crate) fn init_controller( + common: CommonControllerInfo, + config: &Config, +) -> anyhow::Result> { + static INIT_MUTEX: Mutex<()> = Mutex::new(()); + + #[cfg(not(feature = "nvidia"))] + let _ = config; + + // SAFETY: We use global LazyLock to make sure it's safe. + // Loading of shared libaries is unsafe + // https://docs.rs/libloading/0.8.8/libloading/struct.Library.html#method.new + let _guard = INIT_MUTEX.lock().unwrap(); + + #[cfg(feature = "nvidia")] + #[allow(unused_unsafe)] + let nvml = LazyLock::new(|| unsafe { + use nvml_wrapper::Nvml; + use tracing::info; + + Nvml::init() + .map(|nvml| { + info!( + "Nvidia management library {} loaded", + nvml.sys_nvml_version() + .unwrap_or_else(|err| err.to_string()) + ); + Rc::new(nvml) + }) + .inspect_err(|err| { + error!("could not load Nvidia management library: {err}"); + }) + .ok() + }); + + #[cfg(feature = "nvidia")] + let nvapi = LazyLock::new(|| { + use crate::server::gpu_controller::nvidia::nvapi::NvApi; + use tracing::info; + + if config.daemon.disable_nvapi == Some(true) { + info!("NvAPI support is disabled"); + None + } else { + NvApi::new() + .map(|nvapi| { + info!("NvAPI library loaded"); + Rc::new(nvapi) + }) + .inspect_err(|err| { + warn!("could not load NvAPI library: {err:#}"); + }) + .ok() + } + }); match common.driver.as_str() { "amdgpu" | "radeon" => { @@ -265,8 +311,8 @@ pub(crate) fn init_controller( } #[cfg(feature = "nvidia")] "nvidia" => { - if let Some((nvml, nvapi)) = NVML.as_ref() { - match NvidiaGpuController::new(common.clone(), nvml, nvapi.as_ref().as_ref()) { + if let Some(nvml) = nvml.clone() { + match NvidiaGpuController::new(common.clone(), nvml, nvapi.clone()) { Ok(controller) => { return Ok(Box::new(controller)); } diff --git a/lact-daemon/src/server/gpu_controller/nvidia.rs b/lact-daemon/src/server/gpu_controller/nvidia.rs index 1583c1f3..d332c3b0 100644 --- a/lact-daemon/src/server/gpu_controller/nvidia.rs +++ b/lact-daemon/src/server/gpu_controller/nvidia.rs @@ -5,7 +5,6 @@ use super::{CommonControllerInfo, FanControlHandle, GpuController}; use crate::{ bindings::nvidia::NvPhysicalGpuHandle, server::gpu_controller::{ - NvApi, common::{fan_control::FanCurveExt, resolve_process_name}, nvidia::nvapi::{CLOCK_CLIENT_CLK_VF_POINT_TYPE_PROG, ClockClientClkVfPointInfoV1}, }, @@ -26,6 +25,7 @@ use lact_schema::{ ProcessType, ProcessUtilizationType, TemperatureEntry, VoltageStats, VramStats, config::{CurvePoint, FanControlSettings, FanCurve, GpuConfig}, }; +use nvapi::NvApi; use nvml_wrapper::{ Device, Nvml, bitmasks::device::{PowerMizerModes, ThrottleReasons}, @@ -52,12 +52,12 @@ const SUPPORTED_UTIL_TYPES: &[ProcessUtilizationType] = &[ ]; pub struct NvidiaGpuController { - nvml: &'static Nvml, + nvml: Rc, common: CommonControllerInfo, fan_control_handle: RefCell>, initial_target_temp: Option, - nvapi: Option<(&'static NvApi, NvPhysicalGpuHandle)>, + nvapi: Option<(Rc, NvPhysicalGpuHandle)>, driver_handle: Option, nvapi_thermals_mask: Option, @@ -75,8 +75,8 @@ pub struct NvidiaGpuController { impl NvidiaGpuController { pub fn new( common: CommonControllerInfo, - nvml: &'static Nvml, - nvapi: Option<&'static NvApi>, + nvml: Rc, + nvapi: Option>, ) -> anyhow::Result { let device = nvml .device_by_pci_bus_id(common.pci_slot_name.as_str()) @@ -205,7 +205,7 @@ impl NvidiaGpuController { let notify = Rc::new(Notify::new()); let task_notify = notify.clone(); - let nvml = self.nvml; + let nvml = self.nvml.clone(); let pci_slot_id = self.common.pci_slot_name.clone(); debug!("spawning new fan control task"); diff --git a/lact-daemon/src/server/gpu_controller/nvidia/nvapi.rs b/lact-daemon/src/server/gpu_controller/nvidia/nvapi.rs index 125e1184..f4d2b787 100644 --- a/lact-daemon/src/server/gpu_controller/nvidia/nvapi.rs +++ b/lact-daemon/src/server/gpu_controller/nvidia/nvapi.rs @@ -16,6 +16,7 @@ use std::{ mem::{self, transmute}, ptr, }; +use tracing::error; const LIBARY_NAME: &str = "libnvidia-api.so.1"; const QUERY_INTERFACE_FN: &[u8] = b"nvapi_QueryInterface\0"; @@ -455,7 +456,10 @@ impl Drop for NvApi { unsafe { let unload = self.query_interface(QUERY_NVAPI_UNLOAD).unwrap(); let unload: unsafe extern "C" fn() -> NvAPI_Status = transmute(unload); - unload(); + let status = unload(); + if let Err(err) = self.handle_status(status) { + error!("could not unload NvAPI: {err}"); + } } } } diff --git a/lact-daemon/src/server/handler.rs b/lact-daemon/src/server/handler.rs index 03db1a18..eab9611e 100644 --- a/lact-daemon/src/server/handler.rs +++ b/lact-daemon/src/server/handler.rs @@ -5,12 +5,17 @@ use super::{ }; #[cfg(feature = "display-info")] use crate::server::display; +use crate::system::run_command; use crate::{ bindings::intel::IntelDrm, config::Config, - server::{ClientContext, gpu_controller::init_controller, profiles, system::DAEMON_VERSION}, + server::{ + ClientContext, + gpu_controller::{build_controller_info, init_controller}, + profiles, + system::DAEMON_VERSION, + }, }; -use crate::{server::gpu_controller::NvidiaLibs, system::run_command}; use amdgpu_sysfs::gpu_handle::{ PerformanceLevel, PowerLevelKind, power_profile_mode::PowerProfileModesTable, }; @@ -28,12 +33,8 @@ use lact_schema::{ use libdrm_amdgpu_sys::LibDrmAmdgpu; use libflate::gzip; use nix::libc; -#[cfg(all(not(test), feature = "nvidia"))] -use nvml_wrapper::Nvml; use pciid_parser::Database; use serde_json::json; -#[cfg(all(not(test), feature = "nvidia"))] -use std::sync::Arc; use std::{ cell::{Cell, RefCell}, collections::{BTreeMap, HashMap}, @@ -105,6 +106,7 @@ pub struct Handler { profile_hold_snapshot: ProfileHoldSnapshot, next_hold_cookie: Rc>, polkit_proxy: Option>, + ignored_gpu_ids: Rc>>, } impl<'a> Handler { @@ -126,7 +128,7 @@ impl<'a> Handler { // For such scenarios there is a retry logic when no GPUs were found, // or if some of the PCI devices don't have a drm entry yet. for i in 1..=CONTROLLERS_LOAD_RETRY_ATTEMPTS { - controllers = load_controllers(base_path, pci_db)?; + controllers = load_controllers(base_path, pci_db, &[], &config)?; let mut should_retry = false; @@ -201,6 +203,7 @@ impl<'a> Handler { profile_hold_snapshot: Rc::new(RefCell::new(None)), next_hold_cookie: Rc::new(Cell::new(1)), polkit_proxy, + ignored_gpu_ids: Rc::new(RwLock::new(Vec::new())), }; if let Err(err) = handler.apply_current_config().await { error!("could not apply config: {err:#}"); @@ -234,10 +237,11 @@ impl<'a> Handler { pub async fn reload_gpus(&self) { let mut controllers_guard = self.gpu_controllers.write().await; let config = self.config.read().await; + let detached_ids = self.ignored_gpu_ids.read().await; let base_path = drm_base_path(); let pci_db = read_pci_db(); - match load_controllers(&base_path, &pci_db) { + match load_controllers(&base_path, &pci_db, &detached_ids, &config) { Ok(new_controllers) => { info!( "GPU list reloaded with {} devices, reapplying configuration", @@ -1080,6 +1084,24 @@ impl<'a> Handler { } } + pub async fn detach_gpu(&self, gpu_id: &str) -> anyhow::Result<()> { + let _ = self.controller_by_id(gpu_id).await?; + + self.ignored_gpu_ids.write().await.push(gpu_id.to_owned()); + self.reload_gpus().await; + + Ok(()) + } + + pub async fn reattach_gpu(&self, gpu_id: &str) -> anyhow::Result<()> { + self.ignored_gpu_ids.write().await.retain(|id| id != gpu_id); + self.reload_gpus().await; + + let _ = self.controller_by_id(gpu_id).await?; + + Ok(()) + } + pub fn confirm_pending_config(&self, command: ConfirmCommand) -> anyhow::Result<()> { if let Some(tx) = self .confirm_config_tx @@ -1316,48 +1338,6 @@ pub(crate) fn read_pci_db() -> Database { }) } -#[cfg(any(test, not(feature = "nvidia")))] -pub(crate) static NVML: LazyLock> = LazyLock::new(|| None); - -#[cfg(all(not(test), feature = "nvidia"))] -// SAFETY: We use global LazyLock to make sure it's safe. -// Loading of shared libaries is unsafe -// https://docs.rs/libloading/0.8.8/libloading/struct.Library.html#method.new -#[allow(unused_unsafe)] -pub(crate) static NVML: LazyLock> = - LazyLock::new(|| match unsafe { Nvml::init() } { - Ok(nvml) => { - use crate::server::gpu_controller::NvApi; - - // The config has to be re-read here, because a LazyLock cannot capture external variables into the init closure - let disable_nvapi = Config::load() - .ok() - .flatten() - .and_then(|config| config.daemon.disable_nvapi); - - info!("Nvidia management library loaded"); - let nvapi = if disable_nvapi == Some(true) { - info!("NvAPI support is disabled"); - None - } else { - NvApi::new() - .inspect(|_| { - info!("NvAPI library loaded"); - }) - .inspect_err(|err| { - warn!("could not load NvAPI library: {err:#}"); - }) - .ok() - }; - - Some((Arc::new(nvml), Arc::new(nvapi))) - } - Err(err) => { - error!("could not load Nvidia management library: {err}"); - None - } - }); - pub(crate) static AMD_DRM: LazyLock> = LazyLock::new(|| { // SAFETY: We use global LazyLock to make sure it's safe. #[allow(unused_unsafe)] @@ -1394,6 +1374,8 @@ pub(crate) static INTEL_DRM: LazyLock> = LazyLock::new(|| { fn load_controllers( base_path: &Path, pci_db: &Database, + detached_ids: &[String], + config: &Config, ) -> anyhow::Result> { let mut controllers = BTreeMap::new(); @@ -1411,10 +1393,26 @@ fn load_controllers( trace!("trying gpu controller at {:?}", entry.path()); let device_path = entry.path().join("device"); - match init_controller(device_path.clone(), pci_db) { + let info = match build_controller_info(device_path.clone(), pci_db) { + Ok(info) => info, + Err(err) => { + error!( + "could not read GPU info at '{}': {err:#}", + device_path.display() + ); + continue; + } + }; + let id = info.build_id(); + + if detached_ids.contains(&id) { + info!("skipping GPU '{id}', as it is currently detached"); + continue; + } + + match init_controller(info, config) { Ok(controller) => { let info = controller.controller_info(); - let id = info.build_id(); info!( "initialized {} controller for GPU {id} at '{}'", diff --git a/lact-schema/src/args/cli.rs b/lact-schema/src/args/cli.rs index 677f4526..94b47e90 100644 --- a/lact-schema/src/args/cli.rs +++ b/lact-schema/src/args/cli.rs @@ -27,6 +27,10 @@ pub enum CliCommand { }, /// Manage profiles Profile(ProfileArgs), + /// Detach the GPU from LACT (temporarily ignore it) + Detach, + /// Reattach a previously detached GPU + Reattach, } #[derive(Parser, Clone, Copy)] diff --git a/lact-schema/src/request.rs b/lact-schema/src/request.rs index b91d5531..b4a38ed8 100644 --- a/lact-schema/src/request.rs +++ b/lact-schema/src/request.rs @@ -118,6 +118,12 @@ pub enum Request<'a> { ProcessList { id: &'a str, }, + DetachGpu { + id: &'a str, + }, + ReattachGpu { + id: &'a str, + }, EnableOverdrive, DisableOverdrive, GenerateSnapshot,