mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2025-02-25 18:55:26 -06:00
feat!: Intel support (#439)
* WIP: add initial intel xe/i915 support * feat: basic gpu clock configuration * wip i915 freq controls * fix: applying frequency settings on i915 * feat: intel DRM info * feat: show EUs and subslices in UI * refactor: exclude all system-dependent info in tests with conditional compilation * chore: ignore clippy errors on bindgen file * fix: more test fixes * feat: generate drm bindings for xe * wip * refactor: GPU controller initialization * fixes * chore: disable currently unused xe headers * feat: dynamically load libdrm for intel * chore: drop println * Merge with master * fix: tests * chore: avoid error in vulkan when running tests * feat: numerous i915 additions * feat: show fan speed * feat: hwmon monitoring on xe * fix: avoid crashing history graph on empty plot with throttling data * feat: report pstates * doc: update readme * chore: update API docs link
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -466,7 +466,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading 0.8.5",
|
||||
"libloading 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1500,6 +1500,7 @@ version = "0.6.1"
|
||||
dependencies = [
|
||||
"amdgpu-sysfs",
|
||||
"anyhow",
|
||||
"bindgen",
|
||||
"bitflags 2.6.0",
|
||||
"chrono",
|
||||
"copes",
|
||||
@@ -1511,13 +1512,13 @@ dependencies = [
|
||||
"lact-schema",
|
||||
"libdrm_amdgpu_sys",
|
||||
"libflate",
|
||||
"libloading 0.8.6",
|
||||
"nix",
|
||||
"notify",
|
||||
"nvml-wrapper",
|
||||
"os-release",
|
||||
"pciid-parser",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
@@ -1625,7 +1626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "239a084aa81eb01317fe32f41a9ba7d284acf13f079523e3b9406339f4ba7c0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libloading 0.8.5",
|
||||
"libloading 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1664,9 +1665,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
@@ -1872,7 +1873,7 @@ version = "0.10.0"
|
||||
source = "git+https://github.com/ilya-zlobintsev/nvml-wrapper?branch=lact#890581189516191428a8b8c7ba3b006adf03a3fc"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"libloading 0.8.5",
|
||||
"libloading 0.8.6",
|
||||
"nvml-wrapper-sys",
|
||||
"static_assertions",
|
||||
"thiserror",
|
||||
@@ -1884,7 +1885,7 @@ name = "nvml-wrapper-sys"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/ilya-zlobintsev/nvml-wrapper?branch=lact#890581189516191428a8b8c7ba3b006adf03a3fc"
|
||||
dependencies = [
|
||||
"libloading 0.8.5",
|
||||
"libloading 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2928,7 +2929,7 @@ dependencies = [
|
||||
"half",
|
||||
"heck 0.4.1",
|
||||
"indexmap",
|
||||
"libloading 0.8.5",
|
||||
"libloading 0.8.6",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
|
||||
33
README.md
33
README.md
@@ -1,8 +1,8 @@
|
||||
# Linux AMDGPU Control Application
|
||||
# Linux GPU Control Application
|
||||
|
||||
<img src="res/io.github.lact-linux.png" alt="icon" width="100"/>
|
||||
|
||||
This application allows you to control your AMD or Nvidia GPU on a Linux system.
|
||||
This application allows you to control your AMD, Nvidia or Intel GPU on a Linux system.
|
||||
|
||||
| GPU info | Overclocking | Fan control |
|
||||
|----------------------------------------------|----------------------------------------------|---------------------------------------------|
|
||||
@@ -14,11 +14,11 @@ Current features:
|
||||
|
||||
- Viewing information about the GPU
|
||||
- Power and thermals monitoring, power limit configuration
|
||||
- Fan curve control
|
||||
- Fan curve control (AMD and Nvidia)
|
||||
- Overclocking (GPU/VRAM clockspeed and voltage)
|
||||
- Power states configuration (AMD only)
|
||||
|
||||
Both AMD and Nvidia functionality works on X11, Wayland or even headless sessions.
|
||||
All of the functionality works regardless of the desktop session (there is no dependency on X11 extensions).
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -75,7 +75,7 @@ However the following table shows what functionality can be expected for a given
|
||||
| Vega | Supported | Supported | Supported | Supported | |
|
||||
| RDNA1 (RX 5000) | Supported | Supported | Supported | Supported | |
|
||||
| RDNA2 (RX 6000) | Supported | Supported | Supported | Supported | |
|
||||
| RDNA3 (RX 7000) | Supported | Limited | Supported | Limited | Fan zero RPM mode is enabled by default even with a custom fan curve, and requires kernel 6.13 to be disabled. The power cap is sometimes reported lower than it should be. See [#255](https://github.com/ilya-zlobintsev/LACT/issues/255) for more info. |
|
||||
| RDNA3 (RX 7000) | Supported | Supported | Supported | Supported | Fan zero RPM mode is enabled by default even with a custom fan curve, and requires kernel 6.13 to be disabled. The power cap is sometimes reported lower than it should be. See [#255](https://github.com/ilya-zlobintsev/LACT/issues/255) for more info. |
|
||||
|
||||
GPUs not listed here will still work, but might not have full functionality available.
|
||||
Monitoring/system info will be available everywhere. Integrated GPUs might also only have basic configuration available.
|
||||
@@ -84,6 +84,14 @@ Monitoring/system info will be available everywhere. Integrated GPUs might also
|
||||
|
||||
Anything Maxwell or newer should work, but generation support has not yet been tested thoroughly.
|
||||
|
||||
## Intel
|
||||
|
||||
Functionality status on Intel GPUs:
|
||||
- Clocks configuration - works on most devices, but there is no support for overclocking (clocks can only be adjusted within the default limits)
|
||||
- Power limit - works on ARC dGPUs. The maximum power limit might not be reported by the GPU, so the UI will change depending on the current limit
|
||||
- Monitoring - most values are shown on devices where they are applicable, dGPU temperature and fan speed reading might need a recent kernel version
|
||||
- Fan control - not supported by the driver
|
||||
|
||||
# Configuration
|
||||
|
||||
There is a configuration file available in `/etc/lact/config.yaml`. Most of the settings are accessible through the GUI, but some of them may be useful to be edited manually (like `admin_groups` to specify who has access to the daemon)
|
||||
@@ -103,11 +111,11 @@ To fix socket permissions in such configurations, edit `/etc/lact/config.yaml` a
|
||||
# Overclocking (AMD)
|
||||
|
||||
The overclocking functionality is disabled by default in the driver. There are two ways to enable it:
|
||||
- By using the "enable overclocking" option in the LACT GUI. This will create a file in `/etc/modprobe.d` that enables the required driver options. This is the easiest way and it should work for most people.
|
||||
- By using the "enable overclocking" option in the LACT GUI. This will create a file in `/etc/modprobe.d` that enables the required driver options. This is the easiest way and it should work for most people running standard distributions.
|
||||
|
||||
**Note:** This will attempt to automatically regenerate the initramfs to include the new settings. It does not cover all possible distro combinations. If you've enabled overclocking in LACT but it still doesn't work fter a reboot,
|
||||
**Note:** This will attempt to automatically regenerate the initramfs to include the new settings. It does not cover all possible distro combinations. If you've enabled overclocking in LACT but it still doesn't work after a reboot,
|
||||
you might need to check your distro's configuration to make sure the initramfs was updated. Updating the kernel version is a guaranteed way to trigger an initramfs update.
|
||||
- Specifying a boot parameter. You can manually specify the `amdgpu.ppfeaturemask=0xffffffff` kernel parameter in your bootloader to enable overclocking. See the [ArchWiki](https://wiki.archlinux.org/title/AMDGPU#Boot_parameter) for more details.
|
||||
- Specifying a boot parameter. This might be needed if your distro is not supported by the auto-enable functionality. You can manually specify the `amdgpu.ppfeaturemask=0xffffffff` kernel parameter in your bootloader to enable overclocking. See the [ArchWiki](https://wiki.archlinux.org/title/AMDGPU#Boot_parameter) for more details.
|
||||
|
||||
# Suspend/Resume
|
||||
|
||||
@@ -120,14 +128,15 @@ Dependencies:
|
||||
- gtk 4.6+
|
||||
- git
|
||||
- pkg-config
|
||||
- clang
|
||||
- make
|
||||
- hwdata
|
||||
- libdrm
|
||||
- blueprint-compiler 0.10.0+ (Ubuntu 22.04 in particular ships an older version in the repos, you can manually download a [deb file](http://de.archive.ubuntu.com/ubuntu/pool/universe/b/blueprint-compiler/blueprint-compiler_0.10.0-3_all.deb) of a new version)
|
||||
- blueprint-compiler 0.10.0+ (Ubuntu 22.04 in particular ships an older version in the repos, you can manually download a [deb file](http://de.archive.ubuntu.com/ubuntu/pool/universe/b/blueprint-compiler/blueprint-compiler_0.14.0-1_all.deb) of a new version)
|
||||
|
||||
Command to install all dependencies:
|
||||
- Fedora: `sudo dnf install rust cargo make git gtk4-devel libdrm-devel blueprint-compiler`
|
||||
- Arch: `sudo pacman -S --needed base-devel git make rust gtk4 hwdata blueprint-compiler`
|
||||
- Fedora: `sudo dnf install rust cargo make git clang gtk4-devel libdrm-devel blueprint-compiler`
|
||||
- Arch: `sudo pacman -S --needed base-devel git clang make rust gtk4 hwdata blueprint-compiler`
|
||||
|
||||
Steps:
|
||||
- `git clone https://github.com/ilya-zlobintsev/LACT && cd LACT`
|
||||
@@ -149,7 +158,7 @@ make build-release-libadwaita
|
||||
|
||||
# API
|
||||
|
||||
There is an API available over a unix or TCP socket. See [here](API.md) for more information.
|
||||
There is an API available over a unix or TCP socket. See [here](docs/API.md) for more information.
|
||||
|
||||
# Remote management
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ serde = { workspace = true, features = ["rc"] }
|
||||
serde_with = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
nix = { workspace = true, features = ["user", "fs"] }
|
||||
nix = { workspace = true, features = ["user", "fs", "ioctl"] }
|
||||
chrono = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"rt",
|
||||
@@ -37,13 +37,15 @@ pciid-parser = { version = "0.7", features = ["serde"] }
|
||||
serde_yaml = "0.9"
|
||||
vulkano = { version = "0.34.1", default-features = false }
|
||||
zbus = { version = "4.1.2", default-features = false, features = ["tokio"] }
|
||||
libdrm_amdgpu_sys = { version = "0.8.1", default-features = false, features = ["dynamic_loading"] }
|
||||
libdrm_amdgpu_sys = { version = "0.8.1", default-features = false, features = [
|
||||
"dynamic_loading",
|
||||
] }
|
||||
tar = "0.4.40"
|
||||
libflate = "2.0.0"
|
||||
os-release = "0.1.0"
|
||||
notify = { version = "6.1.1", default-features = false }
|
||||
regex = "1.11.0"
|
||||
copes = { git = "https://gitlab.com/corectrl/copes" }
|
||||
libloading = "0.8.6"
|
||||
|
||||
[dev-dependencies]
|
||||
divan = { workspace = true }
|
||||
@@ -51,6 +53,9 @@ pretty_assertions = { workspace = true }
|
||||
lact-daemon = { path = ".", features = ["bench"] }
|
||||
insta = { version = "1.41.1", features = ["json", "yaml"] }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.68"
|
||||
|
||||
[[bench]]
|
||||
name = "daemon"
|
||||
harness = false
|
||||
|
||||
17
lact-daemon/build.rs
Normal file
17
lact-daemon/build.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-changed=include/");
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
|
||||
bindgen::builder()
|
||||
.header("include/intel.h")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||
.dynamic_library_name("IntelDrm")
|
||||
.generate_comments(false)
|
||||
.generate()
|
||||
.expect("Unable to generate intel bindings")
|
||||
.write_to_file(out_path.join("intel_bindings.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
}
|
||||
1408
lact-daemon/include/drm/drm.h
Normal file
1408
lact-daemon/include/drm/drm.h
Normal file
File diff suppressed because it is too large
Load Diff
1362
lact-daemon/include/drm/drm_mode.h
Normal file
1362
lact-daemon/include/drm/drm_mode.h
Normal file
File diff suppressed because it is too large
Load Diff
1701
lact-daemon/include/drm/xe_drm.h
Normal file
1701
lact-daemon/include/drm/xe_drm.h
Normal file
File diff suppressed because it is too large
Load Diff
2
lact-daemon/include/intel.h
Normal file
2
lact-daemon/include/intel.h
Normal file
@@ -0,0 +1,2 @@
|
||||
#include "drm/xe_drm.h"
|
||||
#include <libdrm/intel_bufmgr.h>
|
||||
13
lact-daemon/src/bindings.rs
Normal file
13
lact-daemon/src/bindings.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
#![allow(
|
||||
non_upper_case_globals,
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
unused,
|
||||
clippy::too_many_arguments,
|
||||
clippy::pedantic,
|
||||
clippy::upper_case_acronyms
|
||||
)]
|
||||
|
||||
pub mod intel {
|
||||
include!(concat!(env!("OUT_DIR"), "/intel_bindings.rs"));
|
||||
}
|
||||
@@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashMap,
|
||||
env, fs,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
@@ -102,8 +101,8 @@ pub struct Gpu {
|
||||
/// Outer vector is for power profile components, inner vector is for the heuristics within a component
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub custom_power_profile_mode_hueristics: Vec<Vec<Option<i32>>>,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub power_states: HashMap<PowerLevelKind, Vec<u8>>,
|
||||
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
||||
pub power_states: IndexMap<PowerLevelKind, Vec<u8>>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
@@ -400,7 +399,6 @@ mod tests {
|
||||
use indexmap::IndexMap;
|
||||
use insta::assert_yaml_snapshot;
|
||||
use lact_schema::{FanControlMode, PmfwOptions};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn serde_de_full() {
|
||||
@@ -458,7 +456,7 @@ mod tests {
|
||||
clocks_configuration: ClocksConfiguration::default(),
|
||||
power_profile_mode_index: None,
|
||||
custom_power_profile_mode_hueristics: vec![],
|
||||
power_states: HashMap::new(),
|
||||
power_states: IndexMap::new(),
|
||||
};
|
||||
|
||||
assert!(!gpu.is_core_clocks_used());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::missing_panics_doc)]
|
||||
|
||||
mod bindings;
|
||||
mod config;
|
||||
mod server;
|
||||
mod socket;
|
||||
|
||||
199
lact-daemon/src/server/gpu_controller.rs
Normal file
199
lact-daemon/src/server/gpu_controller.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
mod amd;
|
||||
pub mod fan_control;
|
||||
mod intel;
|
||||
mod nvidia;
|
||||
|
||||
use amd::AmdGpuController;
|
||||
use intel::IntelGpuController;
|
||||
use nvidia::NvidiaGpuController;
|
||||
|
||||
pub const VENDOR_AMD: &str = "1002";
|
||||
pub const VENDOR_NVIDIA: &str = "10DE";
|
||||
|
||||
use crate::{
|
||||
bindings::intel::IntelDrm,
|
||||
config::{self},
|
||||
};
|
||||
use amdgpu_sysfs::gpu_handle::power_profile_mode::PowerProfileModesTable;
|
||||
use anyhow::Context;
|
||||
use futures::future::LocalBoxFuture;
|
||||
use lact_schema::{ClocksInfo, DeviceInfo, DeviceStats, GpuPciInfo, PciInfo, PowerStates};
|
||||
use libdrm_amdgpu_sys::LibDrmAmdgpu;
|
||||
use nvml_wrapper::Nvml;
|
||||
use std::{cell::LazyCell, collections::HashMap, fs, path::PathBuf, rc::Rc};
|
||||
use tokio::{sync::Notify, task::JoinHandle};
|
||||
use tracing::{error, warn};
|
||||
|
||||
type FanControlHandle = (Rc<Notify>, JoinHandle<()>);
|
||||
|
||||
pub trait GpuController {
|
||||
fn controller_info(&self) -> &CommonControllerInfo;
|
||||
|
||||
fn get_info(&self) -> DeviceInfo;
|
||||
|
||||
fn apply_config<'a>(
|
||||
&'a self,
|
||||
config: &'a config::Gpu,
|
||||
) -> LocalBoxFuture<'a, anyhow::Result<()>>;
|
||||
|
||||
fn get_stats(&self, gpu_config: Option<&config::Gpu>) -> DeviceStats;
|
||||
|
||||
fn get_clocks_info(&self) -> anyhow::Result<ClocksInfo>;
|
||||
|
||||
fn get_power_states(&self, gpu_config: Option<&config::Gpu>) -> PowerStates;
|
||||
|
||||
fn reset_pmfw_settings(&self);
|
||||
|
||||
fn cleanup_clocks(&self) -> anyhow::Result<()>;
|
||||
|
||||
fn get_power_profile_modes(&self) -> anyhow::Result<PowerProfileModesTable>;
|
||||
|
||||
fn vbios_dump(&self) -> anyhow::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CommonControllerInfo {
|
||||
pub sysfs_path: PathBuf,
|
||||
pub pci_info: GpuPciInfo,
|
||||
pub pci_slot_name: String,
|
||||
pub driver: String,
|
||||
}
|
||||
|
||||
impl CommonControllerInfo {
|
||||
pub fn build_id(&self) -> String {
|
||||
let GpuPciInfo {
|
||||
device_pci_info,
|
||||
subsystem_pci_info,
|
||||
} = &self.pci_info;
|
||||
|
||||
format!(
|
||||
"{}:{}-{}:{}-{}",
|
||||
device_pci_info.vendor_id,
|
||||
device_pci_info.model_id,
|
||||
subsystem_pci_info.vendor_id,
|
||||
subsystem_pci_info.model_id,
|
||||
self.pci_slot_name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init_controller(
|
||||
path: PathBuf,
|
||||
pci_db: &pciid_parser::Database,
|
||||
nvml: &LazyCell<Option<Rc<Nvml>>>,
|
||||
amd_drm: &LazyCell<Option<LibDrmAmdgpu>>,
|
||||
intel_drm: &LazyCell<Option<Rc<IntelDrm>>>,
|
||||
) -> anyhow::Result<Box<dyn GpuController>> {
|
||||
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);
|
||||
|
||||
let driver = uevent_map
|
||||
.remove("DRIVER")
|
||||
.context("DRIVER entry missing in 'uevent'")?
|
||||
.to_owned();
|
||||
let pci_slot_name = uevent_map
|
||||
.remove("PCI_SLOT_NAME")
|
||||
.context("PCI_SLOT_NAME entry missing in 'uevent'")?
|
||||
.to_owned();
|
||||
|
||||
let (vendor_id, device_id) = uevent_map
|
||||
.get("PCI_ID")
|
||||
.and_then(|id_line| id_line.split_once(':'))
|
||||
.context("PCI_ID entry missing in 'uevent'")?;
|
||||
|
||||
let subsystem_entry = uevent_map
|
||||
.get("PCI_SUBSYS_ID")
|
||||
.and_then(|id_line| id_line.split_once(':'));
|
||||
|
||||
let (subsystem_vendor_id, subsystem_device_id) = subsystem_entry
|
||||
.map(|(vendor, device)| (Some(vendor), Some(device)))
|
||||
.unwrap_or_default();
|
||||
|
||||
let subsystem_info = subsystem_entry
|
||||
.map(|(subsys_vendor_id, subsys_device_id)| {
|
||||
pci_db.get_device_info(vendor_id, device_id, subsys_vendor_id, subsys_device_id)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let vendor = pci_db.vendors.get(&vendor_id.to_ascii_lowercase());
|
||||
|
||||
let pci_info = GpuPciInfo {
|
||||
device_pci_info: PciInfo {
|
||||
vendor_id: vendor_id.to_owned(),
|
||||
vendor: vendor.map(|vendor| vendor.name.clone()),
|
||||
model_id: device_id.to_owned(),
|
||||
model: vendor.and_then(|vendor| {
|
||||
vendor
|
||||
.devices
|
||||
.get(&device_id.to_ascii_lowercase())
|
||||
.map(|device| device.name.clone())
|
||||
}),
|
||||
},
|
||||
subsystem_pci_info: PciInfo {
|
||||
vendor_id: subsystem_vendor_id.unwrap_or_default().to_owned(),
|
||||
vendor: subsystem_info.subvendor_name.map(str::to_owned),
|
||||
model_id: subsystem_device_id.unwrap_or_default().to_owned(),
|
||||
model: subsystem_info.subdevice_name.map(str::to_owned),
|
||||
},
|
||||
};
|
||||
|
||||
let common = CommonControllerInfo {
|
||||
sysfs_path: path,
|
||||
pci_info,
|
||||
pci_slot_name,
|
||||
driver,
|
||||
};
|
||||
|
||||
match common.driver.as_str() {
|
||||
"amdgpu" | "radeon" => {
|
||||
match AmdGpuController::new_from_path(common.clone(), amd_drm.as_ref()) {
|
||||
Ok(controller) => return Ok(Box::new(controller)),
|
||||
Err(err) => error!("could not initialize AMD controller: {err:#}"),
|
||||
}
|
||||
}
|
||||
"i915" | "xe" => {
|
||||
if let Some(drm) = intel_drm.as_ref().cloned() {
|
||||
match IntelGpuController::new(common.clone(), drm) {
|
||||
Ok(controller) => return Ok(Box::new(controller)),
|
||||
Err(err) => error!("could not initialize Intel controller: {err:#}"),
|
||||
}
|
||||
} else {
|
||||
error!("Intel DRM library missing, Intel controls will not be available");
|
||||
}
|
||||
}
|
||||
"nvidia" => {
|
||||
if let Some(nvml) = nvml.as_ref().cloned() {
|
||||
match NvidiaGpuController::new(common.clone(), nvml) {
|
||||
Ok(controller) => {
|
||||
return Ok(Box::new(controller));
|
||||
}
|
||||
Err(err) => error!("could not initialize Nvidia controller: {err:#}"),
|
||||
}
|
||||
} else {
|
||||
error!("NVML is missing, Nvidia controls will not be available");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
warn!(
|
||||
"GPU at '{}' has unsupported driver '{}', functionality will be limited",
|
||||
common.sysfs_path.display(),
|
||||
common.driver,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We use the AMD controller as the fallback even for non-AMD devices, it will at least
|
||||
// display basic device information from the SysFS
|
||||
Ok(Box::new(
|
||||
AmdGpuController::new_from_path(common, amd_drm.as_ref())
|
||||
.context("Could initialize fallback controller")?,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_uevent(data: &str) -> HashMap<&str, &str> {
|
||||
data.lines()
|
||||
.filter_map(|line| line.split_once('='))
|
||||
.collect()
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::{fan_control::FanCurve, FanControlHandle, GpuController, VENDOR_AMD};
|
||||
use super::{
|
||||
fan_control::FanCurve, CommonControllerInfo, FanControlHandle, GpuController, VENDOR_AMD,
|
||||
};
|
||||
use crate::{
|
||||
config::{self, ClocksConfiguration, FanControlSettings},
|
||||
server::vulkan::get_vulkan_info,
|
||||
@@ -12,22 +14,20 @@ use amdgpu_sysfs::{
|
||||
CommitHandle, GpuHandle, PerformanceLevel, PowerLevelKind, PowerLevels,
|
||||
},
|
||||
hw_mon::{FanControlMethod, HwMon},
|
||||
sysfs::SysFS,
|
||||
};
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::LocalBoxFuture;
|
||||
use lact_schema::{
|
||||
ClocksInfo, ClockspeedStats, DeviceInfo, DeviceStats, DrmInfo, FanStats, GpuPciInfo, LinkInfo,
|
||||
PciInfo, PmfwInfo, PowerState, PowerStates, PowerStats, VoltageStats, VramStats,
|
||||
ClocksInfo, ClockspeedStats, DeviceInfo, DeviceStats, DrmInfo, FanStats, IntelDrmInfo,
|
||||
LinkInfo, PmfwInfo, PowerState, PowerStates, PowerStats, VoltageStats, VramStats,
|
||||
};
|
||||
use libdrm_amdgpu_sys::LibDrmAmdgpu;
|
||||
use libdrm_amdgpu_sys::AMDGPU::{ThrottleStatus, ThrottlerBit};
|
||||
use pciid_parser::Database;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cmp,
|
||||
collections::{HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -42,7 +42,6 @@ use tracing::{debug, error, info, trace, warn};
|
||||
use {
|
||||
lact_schema::DrmMemoryInfo,
|
||||
libdrm_amdgpu_sys::AMDGPU::{DeviceHandle as DrmHandle, MetricsInfo, GPU_INFO},
|
||||
std::{fs::OpenOptions, os::fd::IntoRawFd},
|
||||
};
|
||||
|
||||
const GPU_CLOCKDOWN_TIMEOUT_SECS: u64 = 3;
|
||||
@@ -52,25 +51,23 @@ const STEAM_DECK_IDS: [&str; 2] = ["163F", "1435"];
|
||||
pub struct AmdGpuController {
|
||||
handle: GpuHandle,
|
||||
drm_handle: Option<DrmHandle>,
|
||||
pci_info: Option<GpuPciInfo>,
|
||||
common: CommonControllerInfo,
|
||||
fan_control_handle: RefCell<Option<FanControlHandle>>,
|
||||
}
|
||||
|
||||
impl AmdGpuController {
|
||||
#[allow(unused_variables)]
|
||||
pub fn new_from_path(
|
||||
sysfs_path: PathBuf,
|
||||
pci_db: &Database,
|
||||
skip_drm: bool,
|
||||
libdrm_amdgpu: Option<LibDrmAmdgpu>,
|
||||
common: CommonControllerInfo,
|
||||
libdrm_amdgpu: Option<&LibDrmAmdgpu>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let handle = GpuHandle::new_from_path(sysfs_path)
|
||||
let handle = GpuHandle::new_from_path(common.sysfs_path.clone())
|
||||
.map_err(|error| anyhow!("failed to initialize gpu handle: {error}"))?;
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut drm_handle = None;
|
||||
if matches!(handle.get_driver(), "amdgpu" | "radeon")
|
||||
&& !skip_drm
|
||||
&& libdrm_amdgpu.is_some()
|
||||
{
|
||||
#[cfg(not(test))]
|
||||
if matches!(handle.get_driver(), "amdgpu" | "radeon") && libdrm_amdgpu.is_some() {
|
||||
match get_drm_handle(&handle, libdrm_amdgpu.as_ref().unwrap()) {
|
||||
Ok(handle) => {
|
||||
drm_handle = Some(handle);
|
||||
@@ -81,47 +78,10 @@ impl AmdGpuController {
|
||||
}
|
||||
}
|
||||
|
||||
let mut device_pci_info = None;
|
||||
let mut subsystem_pci_info = None;
|
||||
|
||||
if let Some((vendor_id, model_id)) = handle.get_pci_id() {
|
||||
device_pci_info = Some(PciInfo {
|
||||
vendor_id: vendor_id.to_owned(),
|
||||
vendor: None,
|
||||
model_id: model_id.to_owned(),
|
||||
model: None,
|
||||
});
|
||||
|
||||
if let Some((subsys_vendor_id, subsys_model_id)) = handle.get_pci_subsys_id() {
|
||||
let pci_device_info =
|
||||
pci_db.get_device_info(vendor_id, model_id, subsys_vendor_id, subsys_model_id);
|
||||
|
||||
device_pci_info = Some(PciInfo {
|
||||
vendor_id: vendor_id.to_owned(),
|
||||
vendor: pci_device_info.vendor_name.map(str::to_owned),
|
||||
model_id: model_id.to_owned(),
|
||||
model: pci_device_info.device_name.map(str::to_owned),
|
||||
});
|
||||
subsystem_pci_info = Some(PciInfo {
|
||||
vendor_id: subsys_vendor_id.to_owned(),
|
||||
vendor: pci_device_info.subvendor_name.map(str::to_owned),
|
||||
model_id: subsys_model_id.to_owned(),
|
||||
model: pci_device_info.subdevice_name.map(str::to_owned),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
let pci_info = device_pci_info.and_then(|device_pci_info| {
|
||||
Some(GpuPciInfo {
|
||||
device_pci_info,
|
||||
subsystem_pci_info: subsystem_pci_info?,
|
||||
})
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
handle,
|
||||
drm_handle,
|
||||
pci_info,
|
||||
common,
|
||||
fan_control_handle: RefCell::new(None),
|
||||
})
|
||||
}
|
||||
@@ -417,11 +377,12 @@ impl AmdGpuController {
|
||||
.context("GPU has no hardware monitor")
|
||||
}
|
||||
|
||||
fn get_current_gfxclk(&self) -> Option<u16> {
|
||||
fn get_current_gfxclk(&self) -> Option<u64> {
|
||||
self.drm_handle
|
||||
.as_ref()
|
||||
.and_then(|drm_handle| drm_handle.get_gpu_metrics().ok())
|
||||
.and_then(|metrics| metrics.get_current_gfxclk())
|
||||
.map(u64::from)
|
||||
}
|
||||
|
||||
fn get_full_vbios_version(&self) -> Option<String> {
|
||||
@@ -470,6 +431,7 @@ impl AmdGpuController {
|
||||
l2_cache: Some(drm_info.calc_l2_cache_size()),
|
||||
l3_cache_mb: Some(drm_info.calc_l3_cache_size_mb()),
|
||||
memory_info: drm_memory_info,
|
||||
intel: IntelDrmInfo::default(),
|
||||
}),
|
||||
None => None,
|
||||
}
|
||||
@@ -544,52 +506,25 @@ impl AmdGpuController {
|
||||
}
|
||||
|
||||
fn is_steam_deck(&self) -> bool {
|
||||
self.pci_info.as_ref().is_some_and(|info| {
|
||||
info.device_pci_info.vendor_id == VENDOR_AMD
|
||||
&& STEAM_DECK_IDS.contains(&info.device_pci_info.model_id.as_str())
|
||||
})
|
||||
self.common.pci_info.device_pci_info.vendor_id == VENDOR_AMD
|
||||
&& STEAM_DECK_IDS.contains(&self.common.pci_info.device_pci_info.model_id.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuController for AmdGpuController {
|
||||
fn get_id(&self) -> anyhow::Result<String> {
|
||||
let handle = &self.handle;
|
||||
let pci_id = handle.get_pci_id().context("Device has no vendor id")?;
|
||||
let pci_subsys_id = handle
|
||||
.get_pci_subsys_id()
|
||||
.context("Device has no subsys id")?;
|
||||
let pci_slot_name = handle
|
||||
.get_pci_slot_name()
|
||||
.context("Device has no pci slot")?;
|
||||
|
||||
Ok(format!(
|
||||
"{}:{}-{}:{}-{}",
|
||||
pci_id.0, pci_id.1, pci_subsys_id.0, pci_subsys_id.1, pci_slot_name
|
||||
))
|
||||
}
|
||||
|
||||
fn get_pci_info(&self) -> Option<&GpuPciInfo> {
|
||||
self.pci_info.as_ref()
|
||||
}
|
||||
|
||||
fn get_path(&self) -> &Path {
|
||||
self.handle.get_path()
|
||||
fn controller_info(&self) -> &CommonControllerInfo {
|
||||
&self.common
|
||||
}
|
||||
|
||||
fn get_info(&self) -> DeviceInfo {
|
||||
let vulkan_info = self.pci_info.as_ref().and_then(|pci_info| {
|
||||
match get_vulkan_info(
|
||||
&pci_info.device_pci_info.vendor_id,
|
||||
&pci_info.device_pci_info.model_id,
|
||||
) {
|
||||
Ok(info) => Some(info),
|
||||
Err(err) => {
|
||||
warn!("could not load vulkan info: {err}");
|
||||
None
|
||||
}
|
||||
let vulkan_info = match get_vulkan_info(&self.common.pci_info) {
|
||||
Ok(info) => Some(info),
|
||||
Err(err) => {
|
||||
warn!("could not load vulkan info: {err}");
|
||||
None
|
||||
}
|
||||
});
|
||||
let pci_info = self.pci_info.clone();
|
||||
};
|
||||
let pci_info = Some(self.common.pci_info.clone());
|
||||
let driver = self.handle.get_driver().to_owned();
|
||||
let vbios_version = self.get_full_vbios_version();
|
||||
let link_info = self.get_link_info();
|
||||
@@ -605,14 +540,6 @@ impl GpuController for AmdGpuController {
|
||||
}
|
||||
}
|
||||
|
||||
fn hw_monitors(&self) -> &[HwMon] {
|
||||
&self.handle.hw_monitors
|
||||
}
|
||||
|
||||
fn get_pci_slot_name(&self) -> Option<String> {
|
||||
self.handle.get_pci_slot_name().map(str::to_owned)
|
||||
}
|
||||
|
||||
fn get_stats(&self, gpu_config: Option<&config::Gpu>) -> DeviceStats {
|
||||
let fan_settings = gpu_config.and_then(|config| config.fan_control_settings.as_ref());
|
||||
DeviceStats {
|
||||
@@ -1036,12 +963,15 @@ impl GpuController for AmdGpuController {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn get_drm_handle(handle: &GpuHandle, libdrm_amdgpu: &LibDrmAmdgpu) -> anyhow::Result<DrmHandle> {
|
||||
use std::os::unix::io::IntoRawFd;
|
||||
|
||||
let slot_name = handle
|
||||
.get_pci_slot_name()
|
||||
.context("Device has no PCI slot name")?;
|
||||
let path = format!("/dev/dri/by-path/pci-{slot_name}-render");
|
||||
let drm_file = OpenOptions::new()
|
||||
let drm_file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&path)
|
||||
|
||||
655
lact-daemon/src/server/gpu_controller/intel.rs
Normal file
655
lact-daemon/src/server/gpu_controller/intel.rs
Normal file
@@ -0,0 +1,655 @@
|
||||
use super::{CommonControllerInfo, GpuController};
|
||||
use crate::{bindings::intel::IntelDrm, config, server::vulkan::get_vulkan_info};
|
||||
use amdgpu_sysfs::{gpu_handle::power_profile_mode::PowerProfileModesTable, hw_mon::Temperature};
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::LocalBoxFuture;
|
||||
use lact_schema::{
|
||||
ClocksInfo, ClocksTable, ClockspeedStats, DeviceInfo, DeviceStats, DrmInfo, FanStats,
|
||||
IntelClocksTable, IntelDrmInfo, LinkInfo, PowerState, PowerStates, PowerStats, VoltageStats,
|
||||
VramStats,
|
||||
};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::{self, Display},
|
||||
fs,
|
||||
io::{BufRead, BufReader},
|
||||
os::{fd::AsRawFd, raw::c_int},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
time::Instant,
|
||||
};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum DriverType {
|
||||
I915,
|
||||
Xe,
|
||||
}
|
||||
|
||||
pub struct IntelGpuController {
|
||||
driver_type: DriverType,
|
||||
common: CommonControllerInfo,
|
||||
tile_gts: Vec<PathBuf>,
|
||||
hwmon_path: Option<PathBuf>,
|
||||
drm_file: fs::File,
|
||||
drm: Rc<IntelDrm>,
|
||||
last_gpu_busy: Cell<Option<(Instant, u64)>>,
|
||||
last_energy_value: Cell<Option<(Instant, u64)>>,
|
||||
initial_power_cap: Option<f64>,
|
||||
}
|
||||
|
||||
impl IntelGpuController {
|
||||
pub fn new(common: CommonControllerInfo, drm: Rc<IntelDrm>) -> anyhow::Result<Self> {
|
||||
let driver_type = match common.driver.as_str() {
|
||||
"xe" => DriverType::Xe,
|
||||
"i915" => DriverType::I915,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut tile_gts = vec![];
|
||||
|
||||
for entry in fs::read_dir(&common.sysfs_path)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
{
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.starts_with("tile") {
|
||||
for gt_entry in fs::read_dir(entry.path()).into_iter().flatten().flatten() {
|
||||
if let Some(gt_name) = gt_entry.file_name().to_str() {
|
||||
if gt_name.starts_with("gt") {
|
||||
let gt_path = gt_entry
|
||||
.path()
|
||||
.strip_prefix(&common.sysfs_path)
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
debug!("initialized GT at '{}'", gt_path.display());
|
||||
tile_gts.push(gt_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !tile_gts.is_empty() {
|
||||
info!(
|
||||
"initialized {} gt at '{}'",
|
||||
tile_gts.len(),
|
||||
common.sysfs_path.display()
|
||||
);
|
||||
}
|
||||
let drm_file = if cfg!(not(test)) {
|
||||
let drm_path = format!("/dev/dri/by-path/pci-{}-render", common.pci_slot_name);
|
||||
fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(drm_path)
|
||||
.context("Could not open DRM file")?
|
||||
} else {
|
||||
fs::File::open("/dev/null").unwrap()
|
||||
};
|
||||
|
||||
let hwmon_path = fs::read_dir(common.sysfs_path.join("hwmon"))
|
||||
.ok()
|
||||
.and_then(|mut read_dir| read_dir.next())
|
||||
.and_then(Result::ok)
|
||||
.map(|entry| entry.path());
|
||||
debug!("Initialized hwmon: {hwmon_path:?}");
|
||||
|
||||
let mut controller = Self {
|
||||
common,
|
||||
driver_type,
|
||||
tile_gts,
|
||||
hwmon_path,
|
||||
drm_file,
|
||||
drm,
|
||||
last_gpu_busy: Cell::new(None),
|
||||
last_energy_value: Cell::new(None),
|
||||
initial_power_cap: None,
|
||||
};
|
||||
|
||||
let stats = controller.get_stats(None);
|
||||
controller.initial_power_cap = stats.power.cap_current.filter(|cap| *cap != 0.0);
|
||||
|
||||
Ok(controller)
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuController for IntelGpuController {
|
||||
fn controller_info(&self) -> &CommonControllerInfo {
|
||||
&self.common
|
||||
}
|
||||
|
||||
fn get_info(&self) -> DeviceInfo {
|
||||
let vulkan_info = match get_vulkan_info(&self.common.pci_info) {
|
||||
Ok(info) => Some(info),
|
||||
Err(err) => {
|
||||
warn!("could not load vulkan info: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let drm_info = DrmInfo {
|
||||
intel: match self.driver_type {
|
||||
DriverType::I915 => self.get_drm_info_i915(),
|
||||
DriverType::Xe => self.get_drm_info_xe(),
|
||||
},
|
||||
vram_clock_ratio: 1.0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
DeviceInfo {
|
||||
pci_info: Some(self.common.pci_info.clone()),
|
||||
vulkan_info,
|
||||
driver: self.common.driver.clone(),
|
||||
vbios_version: None,
|
||||
link_info: LinkInfo::default(),
|
||||
drm_info: Some(drm_info),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
fn apply_config<'a>(
|
||||
&'a self,
|
||||
config: &'a config::Gpu,
|
||||
) -> LocalBoxFuture<'a, anyhow::Result<()>> {
|
||||
Box::pin(async {
|
||||
if let Some(max_clock) = config.clocks_configuration.max_core_clock {
|
||||
self.write_freq(FrequencyType::Max, max_clock)
|
||||
.context("Could not set max clock")?;
|
||||
}
|
||||
|
||||
if let Some(min_clock) = config.clocks_configuration.min_core_clock {
|
||||
self.write_freq(FrequencyType::Min, min_clock)
|
||||
.context("Could not set min clock")?;
|
||||
}
|
||||
|
||||
if let Some(cap) = config.power_cap {
|
||||
self.write_hwmon_file("power", "_max", &((cap * 1_000_000.0) as u64).to_string())
|
||||
.context("Could not set power cap")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn get_stats(&self, _gpu_config: Option<&config::Gpu>) -> DeviceStats {
|
||||
let current_gfxclk = self.read_freq(FrequencyType::Cur);
|
||||
let gpu_clockspeed = self
|
||||
.read_freq(FrequencyType::Act)
|
||||
.filter(|value| *value != 0)
|
||||
.or(current_gfxclk);
|
||||
|
||||
let clockspeed = ClockspeedStats {
|
||||
gpu_clockspeed,
|
||||
current_gfxclk,
|
||||
vram_clockspeed: None,
|
||||
};
|
||||
|
||||
let cap_current = self
|
||||
.read_hwmon_file("power", "_max")
|
||||
.map(|value: f64| value / 1_000_000.0)
|
||||
.map(|cap| if cap == 0.0 { 100.0 } else { cap }); // Placeholder max value
|
||||
|
||||
let power = PowerStats {
|
||||
average: None,
|
||||
current: self.get_power_usage(),
|
||||
cap_current,
|
||||
cap_min: Some(0.0),
|
||||
cap_max: self
|
||||
.read_hwmon_file::<f64>("power", "_rated_max")
|
||||
.filter(|max| *max != 0.0)
|
||||
.map(|cap| cap / 1_000_000.0)
|
||||
.or_else(|| cap_current.map(|current| current * 2.0)),
|
||||
cap_default: self.initial_power_cap,
|
||||
};
|
||||
|
||||
let voltage = VoltageStats {
|
||||
gpu: self.read_hwmon_file("in", "_input"),
|
||||
northbridge: None,
|
||||
};
|
||||
|
||||
let vram = VramStats {
|
||||
total: self
|
||||
.drm_try_2(IntelDrm::drm_intel_get_aperture_sizes)
|
||||
.map(|(_, total)| total as u64),
|
||||
used: None,
|
||||
};
|
||||
|
||||
let fan = FanStats {
|
||||
speed_current: self.read_hwmon_file("fan", "_input"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
DeviceStats {
|
||||
clockspeed,
|
||||
vram,
|
||||
busy_percent: self.get_busy_percent(),
|
||||
power,
|
||||
temps: self.get_temperatures(),
|
||||
voltage,
|
||||
throttle_info: self.get_throttle_info(),
|
||||
fan,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_clocks_info(&self) -> anyhow::Result<ClocksInfo> {
|
||||
let clocks_table = IntelClocksTable {
|
||||
gt_freq: self
|
||||
.read_freq(FrequencyType::Min)
|
||||
.zip(self.read_freq(FrequencyType::Max)),
|
||||
rp0_freq: self.read_freq(FrequencyType::Rp0),
|
||||
rpe_freq: self.read_freq(FrequencyType::Rpe),
|
||||
rpn_freq: self.read_freq(FrequencyType::Rpn),
|
||||
};
|
||||
|
||||
let table = if clocks_table == IntelClocksTable::default() {
|
||||
None
|
||||
} else {
|
||||
Some(ClocksTable::Intel(clocks_table))
|
||||
};
|
||||
|
||||
Ok(ClocksInfo {
|
||||
table,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn get_power_states(&self, _gpu_config: Option<&config::Gpu>) -> PowerStates {
|
||||
let core = [
|
||||
FrequencyType::Rpn,
|
||||
FrequencyType::Rpe,
|
||||
FrequencyType::Rp0,
|
||||
FrequencyType::Boost,
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|freq_type| {
|
||||
let value = self.read_freq(freq_type)?;
|
||||
Some(PowerState {
|
||||
enabled: true,
|
||||
min_value: None,
|
||||
value,
|
||||
index: None,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
PowerStates { core, vram: vec![] }
|
||||
}
|
||||
|
||||
fn reset_pmfw_settings(&self) {}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn cleanup_clocks(&self) -> anyhow::Result<()> {
|
||||
if let Some(rp0) = self.read_freq(FrequencyType::Rp0) {
|
||||
if let Err(err) = self.write_freq(FrequencyType::Max, rp0 as i32) {
|
||||
warn!("could not reset max clock: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rpn) = self.read_freq(FrequencyType::Rpn) {
|
||||
if let Err(err) = self.write_freq(FrequencyType::Min, rpn as i32) {
|
||||
warn!("could not reset min clock: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_power_profile_modes(&self) -> anyhow::Result<PowerProfileModesTable> {
|
||||
Err(anyhow!("Not supported"))
|
||||
}
|
||||
|
||||
fn vbios_dump(&self) -> anyhow::Result<Vec<u8>> {
|
||||
Err(anyhow!("Not supported"))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntelGpuController {
|
||||
#[allow(clippy::unused_self)]
|
||||
fn debugfs_path(&self) -> PathBuf {
|
||||
#[cfg(test)]
|
||||
return PathBuf::from("/dev/null");
|
||||
|
||||
#[cfg(not(test))]
|
||||
Path::new("/sys/kernel/debug/dri").join(&self.common.pci_slot_name)
|
||||
}
|
||||
|
||||
fn first_tile_gt(&self) -> Option<&Path> {
|
||||
self.tile_gts.first().map(PathBuf::as_ref)
|
||||
}
|
||||
|
||||
fn read_file<T>(&self, path: impl AsRef<Path>) -> Option<T>
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: Display,
|
||||
{
|
||||
let file_path = self.common.sysfs_path.join(path);
|
||||
|
||||
trace!("reading file from '{}'", file_path.display());
|
||||
|
||||
if file_path.exists() {
|
||||
match fs::read_to_string(&file_path) {
|
||||
Ok(contents) => match contents.trim().parse() {
|
||||
Ok(value) => return Some(value),
|
||||
Err(err) => {
|
||||
error!(
|
||||
"could not parse value from '{}': {err}",
|
||||
file_path.display()
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("could not read file at '{}': {err}", file_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn write_file(&self, path: impl AsRef<Path>, contents: &str) -> anyhow::Result<()> {
|
||||
let file_path = self.common.sysfs_path.join(path);
|
||||
|
||||
if file_path.exists() {
|
||||
fs::write(&file_path, contents)
|
||||
.with_context(|| format!("Could not write to '{}'", file_path.display()))?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("File '{}' does not exist", file_path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_hwmon_file<T>(&self, file_prefix: &str, file_suffix: &str) -> Option<T>
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: Display,
|
||||
{
|
||||
self.hwmon_path.as_ref().and_then(|hwmon_path| {
|
||||
let entries = fs::read_dir(hwmon_path).ok()?;
|
||||
for entry in entries.flatten() {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.starts_with(file_prefix) && name.ends_with(file_suffix) {
|
||||
return self.read_file(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn write_hwmon_file(
|
||||
&self,
|
||||
file_prefix: &str,
|
||||
file_suffix: &str,
|
||||
contents: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
debug!("writing value '{contents}' to '{file_prefix}*{file_suffix}'");
|
||||
|
||||
if let Some(hwmon_path) = &self.hwmon_path {
|
||||
let entries = fs::read_dir(hwmon_path)?;
|
||||
for entry in entries.flatten() {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.starts_with(file_prefix) && name.ends_with(file_suffix) {
|
||||
return self.write_file(entry.path(), contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("File not found"))
|
||||
} else {
|
||||
Err(anyhow!("No hwmon available"))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_drm_info_i915(&self) -> IntelDrmInfo {
|
||||
IntelDrmInfo {
|
||||
execution_units: self.drm_try(IntelDrm::drm_intel_get_eu_total),
|
||||
subslices: self.drm_try(IntelDrm::drm_intel_get_subslice_total),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
fn get_drm_info_xe(&self) -> IntelDrmInfo {
|
||||
IntelDrmInfo {
|
||||
execution_units: None,
|
||||
subslices: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, allow(unreachable_code, unused_variables))]
|
||||
fn drm_try<T: Default>(&self, f: unsafe fn(&IntelDrm, c_int, *mut T) -> c_int) -> Option<T> {
|
||||
#[cfg(test)]
|
||||
return None;
|
||||
|
||||
unsafe {
|
||||
let mut out = T::default();
|
||||
let result = f(&self.drm, self.drm_file.as_raw_fd(), &mut out);
|
||||
if result == 0 {
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, allow(unreachable_code, unused_variables))]
|
||||
fn drm_try_2<T: Default, O: Default>(
|
||||
&self,
|
||||
f: unsafe fn(&IntelDrm, c_int, *mut T, *mut O) -> c_int,
|
||||
) -> Option<(T, O)> {
|
||||
#[cfg(test)]
|
||||
return None;
|
||||
|
||||
unsafe {
|
||||
let mut a = T::default();
|
||||
let mut b = O::default();
|
||||
let result = f(&self.drm, self.drm_file.as_raw_fd(), &mut a, &mut b);
|
||||
if result == 0 {
|
||||
Some((a, b))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_possible_truncation
|
||||
)]
|
||||
fn get_busy_percent(&self) -> Option<u8> {
|
||||
let path = self.debugfs_path().join("gt0/rps_boost");
|
||||
let file = fs::File::open(path).ok()?;
|
||||
let mut lines = BufReader::new(file).lines();
|
||||
|
||||
while let Some(Ok(line)) = lines.next() {
|
||||
if let Some(contents) = line.strip_prefix("GPU busy?") {
|
||||
let raw_value = contents
|
||||
.split_ascii_whitespace()
|
||||
.last()?
|
||||
.strip_suffix("ms")?;
|
||||
let gpu_busy: u64 = raw_value.parse().ok()?;
|
||||
let timestamp = Instant::now();
|
||||
|
||||
if let Some((last_timestamp, last_gpu_busy)) =
|
||||
self.last_gpu_busy.replace(Some((timestamp, gpu_busy)))
|
||||
{
|
||||
let time_delta = timestamp - last_timestamp;
|
||||
let gpu_busy_delta = gpu_busy - last_gpu_busy;
|
||||
|
||||
let percentage =
|
||||
(gpu_busy_delta as f64 / time_delta.as_millis() as f64) * 100.0;
|
||||
return Some(percentage as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
|
||||
fn get_power_usage(&self) -> Option<f64> {
|
||||
self.read_hwmon_file::<u64>("power", "_input")
|
||||
.or_else(|| {
|
||||
let energy = self.read_hwmon_file("energy", "_input")?;
|
||||
let timestamp = Instant::now();
|
||||
|
||||
match self.last_energy_value.replace(Some((timestamp, energy))) {
|
||||
Some((last_timestamp, last_energy)) => {
|
||||
let time_delta = timestamp - last_timestamp;
|
||||
let energy_delta = energy - last_energy;
|
||||
|
||||
Some(energy_delta / time_delta.as_millis() as u64 * 1000)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
.map(|value| value as f64 / 1_000_000.0)
|
||||
}
|
||||
|
||||
fn get_temperatures(&self) -> HashMap<String, Temperature> {
|
||||
self.read_hwmon_file::<f32>("temp", "_input")
|
||||
.into_iter()
|
||||
.map(|temp| {
|
||||
let key = "gpu".to_owned();
|
||||
let temperature = Temperature {
|
||||
current: Some(temp / 1000.0),
|
||||
crit: None,
|
||||
crit_hyst: None,
|
||||
};
|
||||
(key, temperature)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_freq(&self, freq: FrequencyType) -> Option<u64> {
|
||||
self.freq_path(freq).and_then(|path| self.read_file(&path))
|
||||
}
|
||||
|
||||
fn write_freq(&self, freq: FrequencyType, value: i32) -> anyhow::Result<()> {
|
||||
let path = self.freq_path(freq).context("Frequency info not found")?;
|
||||
self.write_file(path, &value.to_string())
|
||||
.context("Could not write frequency")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn freq_path(&self, freq: FrequencyType) -> Option<PathBuf> {
|
||||
let path = &self.common.sysfs_path;
|
||||
|
||||
match self.driver_type {
|
||||
DriverType::I915 => {
|
||||
let card_path = path.parent().expect("Device has no parent path");
|
||||
|
||||
let infix = match freq {
|
||||
FrequencyType::Cur => "cur",
|
||||
FrequencyType::Act => "act",
|
||||
FrequencyType::Boost => "boost",
|
||||
FrequencyType::Min => "min",
|
||||
FrequencyType::Max => "max",
|
||||
FrequencyType::Rp0 => "RP0",
|
||||
FrequencyType::Rpe => "RP1",
|
||||
FrequencyType::Rpn => "RPn",
|
||||
};
|
||||
Some(card_path.join(format!("gt_{infix}_freq_mhz")))
|
||||
}
|
||||
DriverType::Xe => match self.first_tile_gt() {
|
||||
Some(gt_path) => {
|
||||
let prefix = match freq {
|
||||
FrequencyType::Cur => "cur",
|
||||
FrequencyType::Act => "act",
|
||||
FrequencyType::Boost => return None,
|
||||
FrequencyType::Min => "min",
|
||||
FrequencyType::Max => "max",
|
||||
FrequencyType::Rp0 => "rp0",
|
||||
FrequencyType::Rpe => "rpe",
|
||||
FrequencyType::Rpn => "rpn",
|
||||
};
|
||||
Some(gt_path.join("freq0").join(format!("{prefix}_freq")))
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_throttle_info(&self) -> Option<BTreeMap<String, Vec<String>>> {
|
||||
let mut reasons = BTreeMap::new();
|
||||
|
||||
match self.driver_type {
|
||||
DriverType::I915 => {
|
||||
let card_path = self
|
||||
.common
|
||||
.sysfs_path
|
||||
.parent()
|
||||
.expect("Device has no parent path");
|
||||
let gt_path = card_path.join("gt").join("gt0");
|
||||
let gt_files = fs::read_dir(gt_path).ok()?;
|
||||
for file in gt_files.flatten() {
|
||||
if let Some(name) = file.file_name().to_str() {
|
||||
if let Some(reason) = name.strip_prefix("throttle_reason_") {
|
||||
if reason == "status" {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = self.read_file::<i32>(file.path()) {
|
||||
if value != 0 {
|
||||
reasons.insert(reason.to_owned(), vec![]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DriverType::Xe => {
|
||||
if let Some(tile) = self.first_tile_gt() {
|
||||
let path = self.common.sysfs_path.join(tile).join("freq0/throttle");
|
||||
|
||||
let throttle_files = fs::read_dir(path).ok()?;
|
||||
for file in throttle_files.flatten() {
|
||||
if let Some(name) = file.file_name().to_str() {
|
||||
if let Some(reason) = name.strip_prefix("reason_") {
|
||||
if let Some(value) = self.read_file::<i32>(file.path()) {
|
||||
if value != 0 {
|
||||
reasons.insert(reason.to_owned(), vec![]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(reasons)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum FrequencyType {
|
||||
Cur,
|
||||
Act,
|
||||
Boost,
|
||||
Min,
|
||||
Max,
|
||||
Rp0,
|
||||
Rpe,
|
||||
Rpn,
|
||||
}
|
||||
|
||||
impl fmt::Display for FrequencyType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
FrequencyType::Cur => "Current",
|
||||
FrequencyType::Act => "Actual",
|
||||
FrequencyType::Boost => "Boost",
|
||||
FrequencyType::Min => "Minimum",
|
||||
FrequencyType::Max => "Maximum",
|
||||
FrequencyType::Rp0 => "Maximum (RP0)",
|
||||
FrequencyType::Rpe => "Efficient (RPe)",
|
||||
FrequencyType::Rpn => "Minimum (RPn)",
|
||||
};
|
||||
s.fmt(f)
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
mod amd;
|
||||
pub mod fan_control;
|
||||
mod nvidia;
|
||||
|
||||
pub const VENDOR_AMD: &str = "1002";
|
||||
pub const VENDOR_NVIDIA: &str = "10DE";
|
||||
|
||||
pub use amd::AmdGpuController;
|
||||
pub use nvidia::NvidiaGpuController;
|
||||
|
||||
use crate::config::{self};
|
||||
use amdgpu_sysfs::gpu_handle::power_profile_mode::PowerProfileModesTable;
|
||||
use amdgpu_sysfs::hw_mon::HwMon;
|
||||
use futures::future::LocalBoxFuture;
|
||||
use lact_schema::{ClocksInfo, DeviceInfo, DeviceStats, GpuPciInfo, PowerStates};
|
||||
use std::{path::Path, rc::Rc};
|
||||
use tokio::{sync::Notify, task::JoinHandle};
|
||||
|
||||
type FanControlHandle = (Rc<Notify>, JoinHandle<()>);
|
||||
|
||||
pub trait GpuController {
|
||||
fn get_id(&self) -> anyhow::Result<String>;
|
||||
|
||||
fn get_pci_info(&self) -> Option<&GpuPciInfo>;
|
||||
|
||||
fn get_path(&self) -> &Path;
|
||||
|
||||
fn get_info(&self) -> DeviceInfo;
|
||||
|
||||
fn get_pci_slot_name(&self) -> Option<String>;
|
||||
|
||||
fn apply_config<'a>(
|
||||
&'a self,
|
||||
config: &'a config::Gpu,
|
||||
) -> LocalBoxFuture<'a, anyhow::Result<()>>;
|
||||
|
||||
fn get_stats(&self, gpu_config: Option<&config::Gpu>) -> DeviceStats;
|
||||
|
||||
fn get_clocks_info(&self) -> anyhow::Result<ClocksInfo>;
|
||||
|
||||
fn get_power_states(&self, gpu_config: Option<&config::Gpu>) -> PowerStates;
|
||||
|
||||
fn reset_pmfw_settings(&self);
|
||||
|
||||
fn cleanup_clocks(&self) -> anyhow::Result<()>;
|
||||
|
||||
fn get_power_profile_modes(&self) -> anyhow::Result<PowerProfileModesTable>;
|
||||
|
||||
fn vbios_dump(&self) -> anyhow::Result<Vec<u8>>;
|
||||
|
||||
fn hw_monitors(&self) -> &[HwMon];
|
||||
}
|
||||
@@ -3,16 +3,13 @@ use crate::{
|
||||
server::vulkan::get_vulkan_info,
|
||||
};
|
||||
|
||||
use super::{fan_control::FanCurve, FanControlHandle, GpuController};
|
||||
use amdgpu_sysfs::{
|
||||
gpu_handle::power_profile_mode::PowerProfileModesTable,
|
||||
hw_mon::{HwMon, Temperature},
|
||||
};
|
||||
use super::{fan_control::FanCurve, CommonControllerInfo, FanControlHandle, GpuController};
|
||||
use amdgpu_sysfs::{gpu_handle::power_profile_mode::PowerProfileModesTable, hw_mon::Temperature};
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::LocalBoxFuture;
|
||||
use lact_schema::{
|
||||
ClocksInfo, ClocksTable, ClockspeedStats, DeviceInfo, DeviceStats, DrmInfo, DrmMemoryInfo,
|
||||
FanControlMode, FanStats, GpuPciInfo, LinkInfo, NvidiaClockInfo, NvidiaClocksTable, PmfwInfo,
|
||||
FanControlMode, FanStats, IntelDrmInfo, LinkInfo, NvidiaClockInfo, NvidiaClocksTable, PmfwInfo,
|
||||
PowerState, PowerStates, PowerStats, VoltageStats, VramStats,
|
||||
};
|
||||
use nvml_wrapper::{
|
||||
@@ -25,7 +22,6 @@ use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -33,37 +29,35 @@ use tokio::{select, sync::Notify, time::sleep};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
pub struct NvidiaGpuController {
|
||||
pub nvml: Rc<Nvml>,
|
||||
pub pci_slot_id: String,
|
||||
pub pci_info: GpuPciInfo,
|
||||
pub sysfs_path: PathBuf,
|
||||
pub fan_control_handle: RefCell<Option<FanControlHandle>>,
|
||||
nvml: Rc<Nvml>,
|
||||
common: CommonControllerInfo,
|
||||
fan_control_handle: RefCell<Option<FanControlHandle>>,
|
||||
|
||||
last_applied_gpc_offset: Cell<Option<i32>>,
|
||||
last_applied_mem_offset: Cell<Option<i32>>,
|
||||
}
|
||||
|
||||
impl NvidiaGpuController {
|
||||
pub fn new(
|
||||
nvml: Rc<Nvml>,
|
||||
pci_slot_id: String,
|
||||
pci_info: GpuPciInfo,
|
||||
sysfs_path: PathBuf,
|
||||
) -> Self {
|
||||
Self {
|
||||
pub fn new(common: CommonControllerInfo, nvml: Rc<Nvml>) -> anyhow::Result<Self> {
|
||||
nvml.device_by_pci_bus_id(common.pci_slot_name.as_str())
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not get PCI device '{}' from NVML",
|
||||
common.pci_slot_name
|
||||
)
|
||||
})?;
|
||||
Ok(Self {
|
||||
nvml,
|
||||
pci_slot_id,
|
||||
pci_info,
|
||||
sysfs_path,
|
||||
common,
|
||||
fan_control_handle: RefCell::new(None),
|
||||
last_applied_gpc_offset: Cell::new(None),
|
||||
last_applied_mem_offset: Cell::new(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn device(&self) -> Device<'_> {
|
||||
self.nvml
|
||||
.device_by_pci_bus_id(self.pci_slot_id.as_str())
|
||||
.device_by_pci_bus_id(self.common.pci_slot_name.as_str())
|
||||
.expect("Can no longer get device")
|
||||
}
|
||||
|
||||
@@ -94,7 +88,7 @@ impl NvidiaGpuController {
|
||||
let task_notify = notify.clone();
|
||||
|
||||
let nvml = self.nvml.clone();
|
||||
let pci_slot_id = self.pci_slot_id.clone();
|
||||
let pci_slot_id = self.common.pci_slot_name.clone();
|
||||
debug!("spawning new fan control task");
|
||||
|
||||
let handle = tokio::task::spawn_local(async move {
|
||||
@@ -266,37 +260,12 @@ impl NvidiaGpuController {
|
||||
}
|
||||
|
||||
impl GpuController for NvidiaGpuController {
|
||||
fn get_id(&self) -> anyhow::Result<String> {
|
||||
let GpuPciInfo {
|
||||
device_pci_info,
|
||||
subsystem_pci_info,
|
||||
} = &self.pci_info;
|
||||
|
||||
Ok(format!(
|
||||
"{}:{}-{}:{}-{}",
|
||||
device_pci_info.vendor_id,
|
||||
device_pci_info.model_id,
|
||||
subsystem_pci_info.vendor_id,
|
||||
subsystem_pci_info.model_id,
|
||||
self.pci_slot_id
|
||||
))
|
||||
}
|
||||
|
||||
fn get_pci_info(&self) -> Option<&GpuPciInfo> {
|
||||
Some(&self.pci_info)
|
||||
}
|
||||
|
||||
fn get_path(&self) -> &Path {
|
||||
&self.sysfs_path
|
||||
fn controller_info(&self) -> &CommonControllerInfo {
|
||||
&self.common
|
||||
}
|
||||
|
||||
fn get_info(&self) -> DeviceInfo {
|
||||
let device = self.device();
|
||||
|
||||
let vulkan_info = match get_vulkan_info(
|
||||
&self.pci_info.device_pci_info.vendor_id,
|
||||
&self.pci_info.device_pci_info.model_id,
|
||||
) {
|
||||
let vulkan_info = match get_vulkan_info(&self.common.pci_info) {
|
||||
Ok(info) => Some(info),
|
||||
Err(err) => {
|
||||
warn!("could not load vulkan info: {err}");
|
||||
@@ -304,8 +273,10 @@ impl GpuController for NvidiaGpuController {
|
||||
}
|
||||
};
|
||||
|
||||
let device = self.device();
|
||||
|
||||
DeviceInfo {
|
||||
pci_info: Some(self.pci_info.clone()),
|
||||
pci_info: Some(self.common.pci_info.clone()),
|
||||
vulkan_info,
|
||||
driver: format!(
|
||||
"nvidia {}",
|
||||
@@ -364,18 +335,11 @@ impl GpuController for NvidiaGpuController {
|
||||
resizeable_bar: None,
|
||||
})
|
||||
.ok(),
|
||||
intel: IntelDrmInfo::default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn hw_monitors(&self) -> &[HwMon] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn get_pci_slot_name(&self) -> Option<String> {
|
||||
Some(self.pci_slot_id.clone())
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
|
||||
@@ -4,15 +4,12 @@ use super::{
|
||||
system::{self, detect_initramfs_type, PP_FEATURE_MASK_PATH},
|
||||
};
|
||||
use crate::{
|
||||
bindings::intel::IntelDrm,
|
||||
config::{self, default_fan_static_speed, Config, FanControlSettings, Profile},
|
||||
server::{
|
||||
gpu_controller::{AmdGpuController, NvidiaGpuController},
|
||||
profiles,
|
||||
},
|
||||
server::{gpu_controller::init_controller, profiles},
|
||||
};
|
||||
use amdgpu_sysfs::{
|
||||
gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind},
|
||||
sysfs::SysFS,
|
||||
use amdgpu_sysfs::gpu_handle::{
|
||||
power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use lact_schema::{
|
||||
@@ -24,12 +21,12 @@ use lact_schema::{
|
||||
use libdrm_amdgpu_sys::LibDrmAmdgpu;
|
||||
use libflate::gzip;
|
||||
use nix::libc;
|
||||
use nvml_wrapper::{error::NvmlError, Nvml};
|
||||
use nvml_wrapper::Nvml;
|
||||
use os_release::OS_RELEASE;
|
||||
use pciid_parser::Database;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cell::{Cell, LazyCell, RefCell},
|
||||
collections::{BTreeMap, HashMap},
|
||||
env,
|
||||
fs::{self, File, Permissions},
|
||||
@@ -86,8 +83,6 @@ const SNAPSHOT_FAN_CTRL_FILES: &[&str] = &[
|
||||
"fan_zero_rpm_enable",
|
||||
"fan_zero_rpm_stop_temperature",
|
||||
];
|
||||
const SNAPSHOT_HWMON_FILE_PREFIXES: &[&str] =
|
||||
&["fan", "pwm", "power", "temp", "freq", "in", "name"];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Handler {
|
||||
@@ -105,21 +100,17 @@ impl<'a> Handler {
|
||||
Ok(custom_path) => PathBuf::from(custom_path),
|
||||
Err(_) => PathBuf::from("/sys/class/drm"),
|
||||
};
|
||||
Self::with_base_path(&base_path, config, false).await
|
||||
Self::with_base_path(&base_path, config).await
|
||||
}
|
||||
|
||||
pub(crate) async fn with_base_path(
|
||||
base_path: &Path,
|
||||
config: Config,
|
||||
sysfs_only: bool,
|
||||
) -> anyhow::Result<Self> {
|
||||
pub(crate) async fn with_base_path(base_path: &Path, config: Config) -> anyhow::Result<Self> {
|
||||
let mut controllers = BTreeMap::new();
|
||||
|
||||
// Sometimes LACT starts too early in the boot process, before the sysfs is initialized.
|
||||
// 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, sysfs_only)?;
|
||||
controllers = load_controllers(base_path)?;
|
||||
|
||||
let mut should_retry = false;
|
||||
if let Ok(devices) = fs::read_dir("/sys/bus/pci/devices") {
|
||||
@@ -133,7 +124,7 @@ impl<'a> Handler {
|
||||
.expect("pci file name should be valid unicode");
|
||||
|
||||
if controllers.values().any(|controller| {
|
||||
controller.get_pci_slot_name().as_ref() == Some(&slot_name)
|
||||
controller.controller_info().pci_slot_name == slot_name
|
||||
}) {
|
||||
debug!("found intialized drm entry for device {:?}", device.path());
|
||||
} else {
|
||||
@@ -200,7 +191,7 @@ impl<'a> Handler {
|
||||
error!("could not apply existing config for gpu {id}: {err}");
|
||||
}
|
||||
} else {
|
||||
info!("could not find GPU with id {id} defined in configuration");
|
||||
warn!("could not find GPU with id {id} defined in configuration");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,8 +336,11 @@ impl<'a> Handler {
|
||||
.iter()
|
||||
.map(|(id, controller)| {
|
||||
let name = controller
|
||||
.get_pci_info()
|
||||
.and_then(|pci_info| pci_info.device_pci_info.model.clone());
|
||||
.controller_info()
|
||||
.pci_info
|
||||
.device_pci_info
|
||||
.model
|
||||
.clone();
|
||||
DeviceListEntry {
|
||||
id: id.to_owned(),
|
||||
name,
|
||||
@@ -582,14 +576,14 @@ impl<'a> Handler {
|
||||
}
|
||||
|
||||
for controller in self.gpu_controllers.values() {
|
||||
let controller_path = controller.get_path();
|
||||
let controller_path = &controller.controller_info().sysfs_path;
|
||||
|
||||
for device_file in SNAPSHOT_DEVICE_FILES {
|
||||
let full_path = controller_path.join(device_file);
|
||||
add_path_to_archive(&mut archive, &full_path)?;
|
||||
}
|
||||
|
||||
let device_files = fs::read_dir(controller.get_path())
|
||||
let device_files = fs::read_dir(controller_path)
|
||||
.context("Could not read device dir")?
|
||||
.flatten();
|
||||
|
||||
@@ -599,40 +593,38 @@ impl<'a> Handler {
|
||||
.iter()
|
||||
.any(|prefix| entry_name.starts_with(prefix))
|
||||
{
|
||||
add_path_recursively(
|
||||
&mut archive,
|
||||
&device_entry.path(),
|
||||
controller.get_path(),
|
||||
)?;
|
||||
add_path_recursively(&mut archive, &device_entry.path(), controller_path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let card_path = controller_path.parent().unwrap();
|
||||
let card_files = fs::read_dir(card_path)
|
||||
.context("Could not read device dir")?
|
||||
.flatten();
|
||||
for card_entry in card_files {
|
||||
if let Ok(metadata) = card_entry.metadata() {
|
||||
if metadata.is_file() {
|
||||
let full_path = controller_path.join(card_entry.path());
|
||||
add_path_to_archive(&mut archive, &full_path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let gt_path = card_path.join("gt");
|
||||
if gt_path.exists() {
|
||||
add_path_recursively(&mut archive, >_path, card_path)?;
|
||||
}
|
||||
|
||||
let fan_ctrl_path = controller_path.join("gpu_od").join("fan_ctrl");
|
||||
for fan_ctrl_file in SNAPSHOT_FAN_CTRL_FILES {
|
||||
let full_path = fan_ctrl_path.join(fan_ctrl_file);
|
||||
add_path_to_archive(&mut archive, &full_path)?;
|
||||
}
|
||||
|
||||
for hw_mon in controller.hw_monitors() {
|
||||
let hw_mon_path = hw_mon.get_path();
|
||||
let hw_mon_entries =
|
||||
fs::read_dir(hw_mon_path).context("Could not read HwMon dir")?;
|
||||
|
||||
'entries: for entry in hw_mon_entries.flatten() {
|
||||
if !entry.metadata().is_ok_and(|metadata| metadata.is_file()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
for prefix in SNAPSHOT_HWMON_FILE_PREFIXES {
|
||||
if name.starts_with(prefix) {
|
||||
add_path_to_archive(&mut archive, &entry.path())?;
|
||||
continue 'entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hwmon_path = controller_path.join("hwmon");
|
||||
if hwmon_path.exists() {
|
||||
add_path_recursively(&mut archive, &hwmon_path, controller_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,11 +700,12 @@ impl<'a> Handler {
|
||||
.and_then(|config| config.gpus().ok()?.get(id));
|
||||
|
||||
let data = json!({
|
||||
"pci_info": controller.get_pci_info(),
|
||||
"pci_info": controller.controller_info().pci_info.clone(),
|
||||
"info": controller.get_info(),
|
||||
"stats": controller.get_stats(gpu_config),
|
||||
"clocks_info": controller.get_clocks_info().ok(),
|
||||
"power_profile_modes": controller.get_power_profile_modes().ok(),
|
||||
"power_states": controller.get_power_states(gpu_config),
|
||||
});
|
||||
(id.clone(), data)
|
||||
})
|
||||
@@ -919,10 +912,7 @@ impl<'a> Handler {
|
||||
}
|
||||
|
||||
/// `sysfs_only` disables initialization of any external data sources, such as libdrm and nvml
|
||||
fn load_controllers(
|
||||
base_path: &Path,
|
||||
sysfs_only: bool,
|
||||
) -> anyhow::Result<BTreeMap<String, Box<dyn GpuController>>> {
|
||||
fn load_controllers(base_path: &Path) -> anyhow::Result<BTreeMap<String, Box<dyn GpuController>>> {
|
||||
let mut controllers = BTreeMap::new();
|
||||
|
||||
let pci_db = Database::read().unwrap_or_else(|err| {
|
||||
@@ -933,34 +923,42 @@ fn load_controllers(
|
||||
}
|
||||
});
|
||||
|
||||
let libdrm_amdgpu = if sysfs_only {
|
||||
None
|
||||
} else {
|
||||
match LibDrmAmdgpu::new() {
|
||||
Ok(libdrm_amdgpu) => {
|
||||
info!("libdrm and libdrm_amdgpu initialized");
|
||||
Some(libdrm_amdgpu)
|
||||
}
|
||||
Err(err) => {
|
||||
info!("AMDGPU support disabled, {err}");
|
||||
None
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
let nvml: LazyCell<Option<Rc<Nvml>>> = LazyCell::new(|| match Nvml::init() {
|
||||
Ok(nvml) => {
|
||||
info!("Nvidia management library loaded");
|
||||
Some(Rc::new(nvml))
|
||||
}
|
||||
};
|
||||
Err(err) => {
|
||||
error!("could not load Nvidia management library: {err}");
|
||||
None
|
||||
}
|
||||
});
|
||||
#[cfg(test)]
|
||||
let nvml: LazyCell<Option<Rc<Nvml>>> = LazyCell::new(|| None);
|
||||
|
||||
let nvml = if sysfs_only {
|
||||
None
|
||||
} else {
|
||||
match Nvml::init() {
|
||||
Ok(nvml) => {
|
||||
info!("NVML initialized");
|
||||
Some(Rc::new(nvml))
|
||||
let amd_drm: LazyCell<Option<LibDrmAmdgpu>> = LazyCell::new(|| match LibDrmAmdgpu::new() {
|
||||
Ok(drm) => {
|
||||
info!("AMDGPU DRM initialized");
|
||||
Some(drm)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to initialize AMDGPU DRM: {err}, some functionality will be missing");
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let intel_drm: LazyCell<Option<Rc<IntelDrm>>> = unsafe {
|
||||
LazyCell::new(|| match IntelDrm::new("libdrm_intel.so.1") {
|
||||
Ok(drm) => {
|
||||
info!("Intel DRM initialized");
|
||||
Some(Rc::new(drm))
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Nvidia support disabled, {err}");
|
||||
error!("failed to initialize Intel DRM: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
for entry in base_path
|
||||
@@ -976,63 +974,24 @@ fn load_controllers(
|
||||
if name.starts_with("card") && !name.contains('-') {
|
||||
trace!("trying gpu controller at {:?}", entry.path());
|
||||
let device_path = entry.path().join("device");
|
||||
match AmdGpuController::new_from_path(
|
||||
device_path,
|
||||
&pci_db,
|
||||
sysfs_only,
|
||||
libdrm_amdgpu.clone(),
|
||||
) {
|
||||
Ok(controller) => match controller.get_id() {
|
||||
Ok(id) => {
|
||||
let path = controller.get_path();
|
||||
|
||||
if let Some(nvml) = nvml.clone() {
|
||||
if let Some(pci_slot_id) = controller.get_pci_slot_name() {
|
||||
match nvml.device_by_pci_bus_id(pci_slot_id.as_str()) {
|
||||
Ok(_) => {
|
||||
let controller = NvidiaGpuController::new(
|
||||
nvml,
|
||||
pci_slot_id,
|
||||
controller.get_pci_info().expect(
|
||||
"Initialized NVML device without PCI info somehow",
|
||||
).clone(),
|
||||
path.to_owned(),
|
||||
);
|
||||
match controller.get_id() {
|
||||
Ok(id) => {
|
||||
info!("initialized Nvidia GPU controller {id} for path {path:?}");
|
||||
controllers.insert(
|
||||
id,
|
||||
Box::new(controller) as Box<dyn GpuController>,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("could not get Nvidia GPU id: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(NvmlError::NotFound) => {
|
||||
debug!("PCI slot {pci_slot_id} not found in NVML");
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"could not initialize Nvidia GPU at {path:?}: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match init_controller(device_path.clone(), &pci_db, &nvml, &amd_drm, &intel_drm) {
|
||||
Ok(controller) => {
|
||||
let info = controller.controller_info();
|
||||
let id = info.build_id();
|
||||
|
||||
info!("initialized GPU controller {id} for path {path:?}");
|
||||
controllers.insert(id, Box::new(controller) as Box<dyn GpuController>);
|
||||
}
|
||||
Err(err) => warn!("could not initialize controller: {err:#}"),
|
||||
},
|
||||
Err(error) => {
|
||||
warn!(
|
||||
"failed to initialize controller at {:?}, {error}",
|
||||
entry.path()
|
||||
info!(
|
||||
"initialized {} controller for GPU {id} at '{}'",
|
||||
info.driver,
|
||||
info.sysfs_path.display()
|
||||
);
|
||||
|
||||
controllers.insert(id, controller);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"could not initialize GPU controller at '{}': {err:#}",
|
||||
device_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1103,6 +1062,8 @@ fn add_path_to_archive(
|
||||
warn!("file {full_path:?} exists, but could not be added to snapshot: {err}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trace!("{full_path:?} does not exist, not adding to snapshot");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use lact_schema::{VulkanDriverInfo, VulkanInfo};
|
||||
use lact_schema::{GpuPciInfo, VulkanDriverInfo, VulkanInfo};
|
||||
use std::borrow::Cow;
|
||||
use tracing::trace;
|
||||
use vulkano::{
|
||||
@@ -7,10 +7,14 @@ use vulkano::{
|
||||
VulkanLibrary,
|
||||
};
|
||||
|
||||
pub fn get_vulkan_info<'a>(vendor_id: &'a str, device_id: &'a str) -> anyhow::Result<VulkanInfo> {
|
||||
#[cfg_attr(test, allow(unreachable_code, unused_variables))]
|
||||
pub fn get_vulkan_info(pci_info: &GpuPciInfo) -> anyhow::Result<VulkanInfo> {
|
||||
#[cfg(test)]
|
||||
return Ok(VulkanInfo::default());
|
||||
|
||||
trace!("Reading vulkan info");
|
||||
let vendor_id = u32::from_str_radix(vendor_id, 16)?;
|
||||
let device_id = u32::from_str_radix(device_id, 16)?;
|
||||
let vendor_id = u32::from_str_radix(&pci_info.device_pci_info.vendor_id, 16)?;
|
||||
let device_id = u32::from_str_radix(&pci_info.device_pci_info.model_id, 16)?;
|
||||
|
||||
let library = VulkanLibrary::new().context("Could not create vulkan library")?;
|
||||
let instance = Instance::new(library, InstanceCreateInfo::default())
|
||||
|
||||
@@ -73,13 +73,13 @@ gpus:
|
||||
- -65536
|
||||
- 0
|
||||
power_states:
|
||||
memory_clock:
|
||||
- 0
|
||||
- 1
|
||||
core_clock:
|
||||
- 0
|
||||
- 2
|
||||
- 3
|
||||
memory_clock:
|
||||
- 0
|
||||
- 1
|
||||
profiles:
|
||||
vkcube:
|
||||
rule:
|
||||
|
||||
1
lact-daemon/src/tests/data/intel/a380-i915/card1/dev
Normal file
1
lact-daemon/src/tests/data/intel/a380-i915/card1/dev
Normal file
@@ -0,0 +1 @@
|
||||
226:1
|
||||
@@ -0,0 +1 @@
|
||||
2.5 GT/s PCIe
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
25637161804
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
603
|
||||
@@ -0,0 +1 @@
|
||||
i915
|
||||
@@ -0,0 +1 @@
|
||||
auto
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
unsupported
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
55000000
|
||||
@@ -0,0 +1 @@
|
||||
28000
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
55000
|
||||
@@ -0,0 +1,6 @@
|
||||
DRIVER=i915
|
||||
PCI_CLASS=30000
|
||||
PCI_ID=8086:56A5
|
||||
PCI_SUBSYS_ID=1849:6004
|
||||
PCI_SLOT_NAME=0000:0b:00.0
|
||||
MODALIAS=pci:v00008086d000056A5sv00001849sd00006004bc03sc00i00
|
||||
@@ -0,0 +1 @@
|
||||
0x8086
|
||||
1
lact-daemon/src/tests/data/intel/a380-i915/card1/error
Normal file
1
lact-daemon/src/tests/data/intel/a380-i915/card1/error
Normal file
@@ -0,0 +1 @@
|
||||
No error state collected
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
2450
|
||||
@@ -0,0 +1 @@
|
||||
300
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
1400
|
||||
@@ -0,0 +1 @@
|
||||
1400
|
||||
@@ -0,0 +1 @@
|
||||
128
|
||||
@@ -0,0 +1 @@
|
||||
0.00390625
|
||||
@@ -0,0 +1 @@
|
||||
600
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
6534
|
||||
@@ -0,0 +1 @@
|
||||
2450
|
||||
@@ -0,0 +1 @@
|
||||
600
|
||||
@@ -0,0 +1 @@
|
||||
300
|
||||
@@ -0,0 +1 @@
|
||||
600
|
||||
@@ -0,0 +1 @@
|
||||
2450
|
||||
@@ -0,0 +1 @@
|
||||
600
|
||||
@@ -0,0 +1 @@
|
||||
2450
|
||||
@@ -0,0 +1 @@
|
||||
300
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
2450
|
||||
@@ -0,0 +1 @@
|
||||
600
|
||||
@@ -0,0 +1 @@
|
||||
300
|
||||
@@ -0,0 +1 @@
|
||||
600
|
||||
@@ -0,0 +1 @@
|
||||
2450
|
||||
@@ -0,0 +1 @@
|
||||
600
|
||||
@@ -0,0 +1 @@
|
||||
2450
|
||||
@@ -0,0 +1 @@
|
||||
300
|
||||
4
lact-daemon/src/tests/data/intel/a380-i915/card1/uevent
Normal file
4
lact-daemon/src/tests/data/intel/a380-i915/card1/uevent
Normal file
@@ -0,0 +1,4 @@
|
||||
MAJOR=226
|
||||
MINOR=1
|
||||
DEVNAME=dri/card1
|
||||
DEVTYPE=drm_minor
|
||||
1
lact-daemon/src/tests/data/intel/a380-xe/card0/dev
Normal file
1
lact-daemon/src/tests/data/intel/a380-xe/card0/dev
Normal file
@@ -0,0 +1 @@
|
||||
226:0
|
||||
@@ -0,0 +1 @@
|
||||
2.5 GT/s PCIe
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
2850177917
|
||||
@@ -0,0 +1 @@
|
||||
pkg
|
||||
@@ -0,0 +1 @@
|
||||
638
|
||||
@@ -0,0 +1 @@
|
||||
pkg
|
||||
@@ -0,0 +1 @@
|
||||
xe
|
||||
@@ -0,0 +1 @@
|
||||
auto
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
unsupported
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
card
|
||||
@@ -0,0 +1 @@
|
||||
pkg
|
||||
@@ -0,0 +1 @@
|
||||
55000000
|
||||
@@ -0,0 +1 @@
|
||||
28000
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
10000
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
5000
|
||||
@@ -0,0 +1 @@
|
||||
10000000
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user