feat: add detach/reattach option to temporarily ignore a GPU

This commit is contained in:
Ilya Zlobintsev committed 2026-07-21 09:23:03 +03:00
1 parent bc2d598eab
commit a956ada1c8
9 files changed
+134 -16

No files matched your search

+14 -2
View File
@@ -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
+14 -2
View File
@@ -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<Option<String>> {
Ok(self
.client
.list_devices()
.await?
.into_iter()
.find(|entry| entry.id == gpu_id)
.and_then(|entry| entry.name))
}
}
+33
View File
@@ -233,3 +233,36 @@ pub async fn set_auto_switch(
}
Ok(())
}
pub async fn detach(ctx: CliContext<'_>) -> Result<()> {
let id = ctx.current_gpu_id().await?;
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(())
}
+2
View File
@@ -156,6 +156,8 @@ impl DaemonClient {
request_with_id!(dump_vbios, VbiosDump, Vec<u8>);
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<ProfilesInfo> {
self.make_request(Request::ListProfiles { include_state })
+2
View File
@@ -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?)
}
+11 -7
View File
@@ -171,13 +171,10 @@ pub type NvidiaLibs = (Arc<Nvml>, Arc<Option<NvApi>>);
#[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<Box<dyn GpuController>> {
#[cfg(not(feature = "nvidia"))]
let _ = NVML;
) -> anyhow::Result<CommonControllerInfo> {
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 +236,19 @@ 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,
) -> anyhow::Result<Box<dyn GpuController>> {
#[cfg(not(feature = "nvidia"))]
let _ = NVML;
match common.driver.as_str() {
"amdgpu" | "radeon" => {
+48 -5
View File
@@ -8,7 +8,12 @@ use crate::server::display;
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::{
@@ -105,6 +110,7 @@ pub struct Handler {
profile_hold_snapshot: ProfileHoldSnapshot,
next_hold_cookie: Rc<Cell<u64>>,
polkit_proxy: Option<AuthorityProxy<'static>>,
ignored_gpu_ids: Rc<RwLock<Vec<String>>>,
}
impl<'a> Handler {
@@ -126,7 +132,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, &[])?;
let mut should_retry = false;
@@ -201,6 +207,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 +241,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) {
Ok(new_controllers) => {
info!(
"GPU list reloaded with {} devices, reapplying configuration",
@@ -1080,6 +1088,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
@@ -1394,6 +1420,7 @@ pub(crate) static INTEL_DRM: LazyLock<Option<IntelDrm>> = LazyLock::new(|| {
fn load_controllers(
base_path: &Path,
pci_db: &Database,
detached_ids: &[String],
) -> anyhow::Result<BTreeMap<String, DynGpuController>> {
let mut controllers = BTreeMap::new();
@@ -1411,10 +1438,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) {
Ok(controller) => {
let info = controller.controller_info();
let id = info.build_id();
info!(
"initialized {} controller for GPU {id} at '{}'",
+4
View File
@@ -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)]
+6
View File
@@ -118,6 +118,12 @@ pub enum Request<'a> {
ProcessList {
id: &'a str,
},
DetachGpu {
id: &'a str,
},
ReattachGpu {
id: &'a str,
},
EnableOverdrive,
DisableOverdrive,
GenerateSnapshot,