mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2026-08-17 16:34:54 -05:00
1723 lines
65 KiB
Rust
1723 lines
65 KiB
Rust
use super::{CommonControllerInfo, FanControlHandle, GpuController, VENDOR_AMD};
|
|
use crate::server::gpu_controller::common::{
|
|
fan_control::FanCurveExt,
|
|
fdinfo::{self, DrmUtilMap},
|
|
};
|
|
use amdgpu_sysfs::{
|
|
error::Error,
|
|
gpu_handle::{
|
|
CommitHandle, GpuHandle, PerformanceLevel, PowerLevelId, PowerLevelKind,
|
|
fan_control::FanCurve as PmfwCurve,
|
|
overdrive::{ClocksTable, ClocksTableGen},
|
|
power_profile_mode::PowerProfileModesTable,
|
|
},
|
|
hw_mon::{FanControlMethod, HwMon, Temperature},
|
|
sysfs::SysFS,
|
|
};
|
|
use anyhow::{Context, anyhow, bail};
|
|
use futures::{FutureExt, future::LocalBoxFuture};
|
|
use lact_schema::{
|
|
ActivePowerStates, AmdCacheInstance, AmdIpInfo, CacheInfo, CacheType, ClocksInfo,
|
|
ClockspeedStats, DeviceApiInfo, DeviceFlag, DeviceInfo, DeviceStats, DeviceType, DrmInfo,
|
|
FanControlMode, FanStats, IntelDrmInfo, LinkInfo, PmfwInfo, PowerState, PowerStates,
|
|
PowerStats, ProcessList, ProcessUtilizationType, RopInfo, TemperatureEntry, VoltageStats,
|
|
VramStats,
|
|
config::{ClocksConfiguration, FanControlSettings, FanCurve, GpuConfig},
|
|
};
|
|
#[cfg(feature = "display-info")]
|
|
use lact_schema::{DisplayConnector, DisplaysInfo};
|
|
use libdrm_amdgpu_sys::AMDGPU::{GpuMetrics, HW_IP::HW_IP_TYPE, ThrottlerBit, ThrottlerType};
|
|
use libdrm_amdgpu_sys::{AMDGPU::SENSOR_INFO::SENSOR_TYPE, LibDrmAmdgpu, PCI};
|
|
use std::{
|
|
cell::RefCell,
|
|
cmp,
|
|
collections::{HashMap, HashSet, VecDeque},
|
|
path::{Path, PathBuf},
|
|
rc::Rc,
|
|
time::Duration,
|
|
};
|
|
use std::{collections::BTreeMap, fs, time::Instant};
|
|
use tokio::{select, sync::Notify, time::sleep};
|
|
use tracing::{debug, error, info, trace, warn};
|
|
|
|
use {
|
|
lact_schema::DrmMemoryInfo,
|
|
libdrm_amdgpu_sys::AMDGPU::{DeviceHandle as DrmHandle, GPU_INFO, MetricsInfo},
|
|
};
|
|
|
|
/// RDNA3 - minimum family with PMFW
|
|
const AMDGPU_FAMILY_GC_11_0_0: u32 = 145;
|
|
|
|
const FAN_CONTROL_RETRIES: u32 = 10;
|
|
const MAX_PSTATE_READ_ATTEMPTS: u32 = 5;
|
|
const REQUIRE_MANUAL_DEVICE_IDS: [&str; 3] = ["163F", "1435", "15BF"];
|
|
const AMDGPU_IDS_FLAGS_FUSION: u64 = 0x1;
|
|
const HSA_CACHE_TYPE_DATA: u32 = 0x0000_0001;
|
|
const HSA_CACHE_TYPE_INSTRUCTION: u32 = 0x0000_0002;
|
|
const HSA_CACHE_TYPE_CPU: u32 = 0x0000_0004;
|
|
// const HSA_CACHE_TYPE_HSACU: u32 = 0x0000_0008;
|
|
|
|
const DRM_VRAM_KEYS: &[&str] = &["drm-memory-vram"];
|
|
const DRM_ENGINES: &[(&str, ProcessUtilizationType)] = &[
|
|
("gfx", ProcessUtilizationType::Graphics),
|
|
("compute", ProcessUtilizationType::Compute),
|
|
("dec", ProcessUtilizationType::Decode),
|
|
("enc", ProcessUtilizationType::Encode),
|
|
("enc_1", ProcessUtilizationType::Encode),
|
|
];
|
|
|
|
const ALL_HW_IP: &[HW_IP_TYPE] = &[
|
|
HW_IP_TYPE::GFX,
|
|
HW_IP_TYPE::COMPUTE,
|
|
HW_IP_TYPE::DMA,
|
|
HW_IP_TYPE::UVD,
|
|
HW_IP_TYPE::VCE,
|
|
HW_IP_TYPE::UVD_ENC,
|
|
HW_IP_TYPE::VCN_DEC,
|
|
HW_IP_TYPE::VCN_ENC,
|
|
HW_IP_TYPE::VCN_JPEG,
|
|
HW_IP_TYPE::VPE,
|
|
];
|
|
|
|
pub struct AmdGpuController {
|
|
handle: GpuHandle,
|
|
drm_handle: Option<DrmHandle>,
|
|
common: CommonControllerInfo,
|
|
fan_control_handle: RefCell<Option<FanControlHandle>>,
|
|
last_drm_util: RefCell<Option<DrmUtilMap>>,
|
|
}
|
|
|
|
impl AmdGpuController {
|
|
#[allow(unused_variables)]
|
|
pub fn new_from_path(
|
|
common: CommonControllerInfo,
|
|
libdrm_amdgpu: Option<&'static LibDrmAmdgpu>,
|
|
) -> anyhow::Result<Self> {
|
|
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;
|
|
#[cfg(not(test))]
|
|
if let Some(libdrm_amdgpu) = libdrm_amdgpu
|
|
&& handle.get_driver() == "amdgpu"
|
|
{
|
|
drm_handle = Some(
|
|
get_drm_handle(&common, libdrm_amdgpu).context("Could not get AMD DRM handle")?,
|
|
);
|
|
}
|
|
|
|
Ok(Self {
|
|
handle,
|
|
drm_handle,
|
|
common,
|
|
fan_control_handle: RefCell::new(None),
|
|
last_drm_util: RefCell::new(None),
|
|
})
|
|
}
|
|
|
|
fn hw_mon_and_then<U>(&self, f: fn(&HwMon) -> Result<U, Error>) -> Option<U> {
|
|
self.handle.hw_monitors.first().and_then(|mon| f(mon).ok())
|
|
}
|
|
|
|
async fn set_static_fan_control(&self, static_speed: f32) -> anyhow::Result<Vec<CommitHandle>> {
|
|
// Stop existing task to set static speed
|
|
self.stop_fan_control(false).await?;
|
|
|
|
let mut commit_handles = Vec::new();
|
|
|
|
// Use PMFW curve functionality for static speed when it is available
|
|
if let Ok(current_curve) = self.handle.get_fan_curve() {
|
|
if let Ok(true) = self.handle.get_fan_zero_rpm_enable() {
|
|
match self.handle.set_fan_zero_rpm_enable(false) {
|
|
Ok(zero_rpm_commit) => {
|
|
commit_handles.push(zero_rpm_commit);
|
|
}
|
|
Err(err) => {
|
|
error!("could not disable zero RPM mode for static fan control: {err}");
|
|
}
|
|
}
|
|
}
|
|
|
|
let allowed_ranges = current_curve.allowed_ranges.clone().ok_or_else(|| {
|
|
anyhow!("The GPU does not allow setting custom fan values (is overdrive enabled?)")
|
|
})?;
|
|
let min_temperature = allowed_ranges.temperature_range.start();
|
|
let max_temperature = allowed_ranges.temperature_range.end();
|
|
|
|
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
|
let custom_pwm = (f32::from(*allowed_ranges.speed_range.end()) * static_speed) as u8;
|
|
let static_pwm = cmp::max(*allowed_ranges.speed_range.start(), custom_pwm);
|
|
|
|
let mut points = vec![(*min_temperature, static_pwm)];
|
|
for _ in 1..current_curve.points.len() {
|
|
points.push((*max_temperature, static_pwm));
|
|
}
|
|
|
|
let new_curve = PmfwCurve {
|
|
points: points.into_boxed_slice(),
|
|
allowed_ranges: Some(allowed_ranges),
|
|
};
|
|
|
|
debug!("setting static curve {new_curve:?}");
|
|
|
|
let curve_commit = self
|
|
.handle
|
|
.set_fan_curve(&new_curve)
|
|
.context("Could not set fan curve")?;
|
|
commit_handles.push(curve_commit);
|
|
|
|
Ok(commit_handles)
|
|
} else {
|
|
let hw_mon = self
|
|
.handle
|
|
.hw_monitors
|
|
.first()
|
|
.cloned()
|
|
.context("This GPU has no monitor")?;
|
|
|
|
hw_mon
|
|
.set_fan_control_method(FanControlMethod::Manual)
|
|
.context("Could not set fan control method")?;
|
|
|
|
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
|
let static_pwm = (f32::from(u8::MAX) * static_speed) as u8;
|
|
|
|
hw_mon
|
|
.set_fan_pwm(static_pwm)
|
|
.context("could not set fan speed")?;
|
|
|
|
debug!("set fan speed to {}", static_speed);
|
|
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
async fn start_curve_fan_control(
|
|
&self,
|
|
curve: FanCurve,
|
|
settings: FanControlSettings,
|
|
) -> anyhow::Result<Option<CommitHandle>> {
|
|
// Use the PMFW curve functionality when it is available
|
|
// Otherwise, fall back to manual fan control via a task
|
|
if let Ok(current_curve) = self.handle.get_fan_curve() {
|
|
let new_curve = curve
|
|
.into_pmfw_curve(current_curve.clone())
|
|
.context("Invalid fan curve")?;
|
|
|
|
debug!("setting pmfw curve {new_curve:?}");
|
|
|
|
let commit_handle = self
|
|
.handle
|
|
.set_fan_curve(&new_curve)
|
|
.context("Could not set fan curve")?;
|
|
|
|
Ok(Some(commit_handle))
|
|
} else {
|
|
self.start_curve_fan_control_task(curve, settings).await?;
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
async fn start_curve_fan_control_task(
|
|
&self,
|
|
curve: FanCurve,
|
|
settings: FanControlSettings,
|
|
) -> anyhow::Result<()> {
|
|
// Stop existing task to re-apply new curve
|
|
self.stop_fan_control(false).await?;
|
|
|
|
let hw_mon = self
|
|
.handle
|
|
.hw_monitors
|
|
.first()
|
|
.cloned()
|
|
.context("This GPU has no monitor")?;
|
|
|
|
let temps = hw_mon.get_temps();
|
|
match temps.len() {
|
|
0 => return Err(anyhow!("GPU has no temperature reporting")),
|
|
1 => {
|
|
warn!(
|
|
"GPU has only one temperature sensor, 'temperature_key' setting will be ignored"
|
|
);
|
|
}
|
|
_ => {
|
|
if !temps.contains_key(&settings.temperature_key) {
|
|
return Err(anyhow!(
|
|
"Sensor with name {} not found, available sensors: {}",
|
|
settings.temperature_key,
|
|
temps
|
|
.keys()
|
|
.map(String::as_str)
|
|
.collect::<Vec<&str>>()
|
|
.join(",")
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
hw_mon
|
|
.set_fan_control_method(FanControlMethod::Manual)
|
|
.context("Could not set fan control method")?;
|
|
|
|
let mut notify_guard = self
|
|
.fan_control_handle
|
|
.try_borrow_mut()
|
|
.map_err(|err| anyhow!("Lock error: {err}"))?;
|
|
|
|
let notify = Rc::new(Notify::new());
|
|
let task_notify = notify.clone();
|
|
|
|
debug!("spawning new fan control task");
|
|
let handle = tokio::task::spawn_local(async move {
|
|
let mut last_pwm = (None, Instant::now());
|
|
let mut last_temp = 0.0;
|
|
|
|
// If the fan speed could was able to be set at least once
|
|
let mut control_available = false;
|
|
|
|
let temp_key = settings.temperature_key.clone();
|
|
let interval = Duration::from_millis(settings.interval_ms);
|
|
let spindown_delay = Duration::from_millis(settings.spindown_delay_ms.unwrap_or(0));
|
|
#[allow(clippy::cast_precision_loss)]
|
|
let change_threshold = settings.change_threshold.unwrap_or(0) as f32;
|
|
|
|
let mut retries = 0;
|
|
|
|
loop {
|
|
select! {
|
|
() = sleep(interval) => (),
|
|
() = task_notify.notified() => break,
|
|
}
|
|
|
|
let mut temps = hw_mon.get_temps();
|
|
let temp = if temps.len() == 1 {
|
|
temps.into_values().next().unwrap()
|
|
} else if let Some(value) = temps.remove(&temp_key) {
|
|
value
|
|
} else {
|
|
retries += 1;
|
|
|
|
if retries == FAN_CONTROL_RETRIES {
|
|
error!(
|
|
"could not get temperature sensor {temp_key}, exiting fan control (reached max attempts)"
|
|
);
|
|
break;
|
|
}
|
|
error!(
|
|
"could not get temperature sensor {temp_key} from {} sensors (assuming error is temporary, attempt {retries}/{FAN_CONTROL_RETRIES})",
|
|
temps.len()
|
|
);
|
|
continue;
|
|
};
|
|
|
|
let current_temp = temp.current.expect("Missing temp");
|
|
|
|
if (last_temp - current_temp).abs() < change_threshold {
|
|
trace!(
|
|
"temperature changed from {last_temp}°C to {current_temp}°C, which is less than the {change_threshold}°C threshold, skipping speed adjustment"
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let target_pwm = curve.pwm_at_temp(temp);
|
|
let now = Instant::now();
|
|
|
|
if let (Some(previous_pwm), previous_timestamp) = last_pwm {
|
|
let diff = now - previous_timestamp;
|
|
if target_pwm < previous_pwm && diff < spindown_delay {
|
|
trace!(
|
|
"delaying fan spindown ({}ms left)",
|
|
spindown_delay.checked_sub(diff).unwrap().as_millis()
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
last_pwm = (Some(target_pwm), now);
|
|
last_temp = current_temp;
|
|
|
|
trace!("fan control tick: setting pwm to {target_pwm}");
|
|
|
|
match hw_mon.set_fan_pwm(target_pwm) {
|
|
Ok(()) => control_available = true,
|
|
Err(err) => {
|
|
error!("could not set fan speed: {err}");
|
|
if control_available {
|
|
retries += 1;
|
|
|
|
if retries == FAN_CONTROL_RETRIES {
|
|
error!(
|
|
"could not set fan speed after {retries} attempts, exiting fan control (reached max attempts)"
|
|
);
|
|
break;
|
|
}
|
|
|
|
info!(
|
|
"fan control was previously available, assuming the error is temporary (attempt {retries}/{FAN_CONTROL_RETRIES})"
|
|
);
|
|
|
|
if !matches!(
|
|
hw_mon.get_fan_control_method(),
|
|
Ok(FanControlMethod::Manual),
|
|
) {
|
|
info!(
|
|
"fan control method was changed externally, setting back to manual"
|
|
);
|
|
if let Err(err) =
|
|
hw_mon.set_fan_control_method(FanControlMethod::Manual)
|
|
{
|
|
error!("could not set fan control back to manual: {err}");
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
info!("disabling fan control");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
retries = 0;
|
|
}
|
|
debug!("exited fan control task");
|
|
|
|
if let Err(err) = hw_mon.set_fan_control_method(FanControlMethod::Auto) {
|
|
error!("could not reset fan control back to auto: {err}");
|
|
}
|
|
});
|
|
|
|
*notify_guard = Some((notify, handle));
|
|
|
|
debug!(
|
|
"started fan control with interval {}ms",
|
|
settings.interval_ms
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn stop_fan_control(&self, reset_mode: bool) -> anyhow::Result<()> {
|
|
let maybe_notify = self
|
|
.fan_control_handle
|
|
.try_borrow_mut()
|
|
.map_err(|err| anyhow!("Lock error: {err}"))?
|
|
.take();
|
|
if let Some((notify, handle)) = maybe_notify {
|
|
notify.notify_one();
|
|
handle.await?;
|
|
}
|
|
|
|
if reset_mode {
|
|
if self.handle.get_fan_curve().is_ok()
|
|
&& let Err(err) = self.handle.reset_fan_curve()
|
|
{
|
|
warn!("could not reset fan curve: {err:#}");
|
|
}
|
|
|
|
if let Some(hw_mon) = self.handle.hw_monitors.first().cloned()
|
|
&& let Ok(current_control) = hw_mon.get_fan_control_method()
|
|
&& !matches!(current_control, FanControlMethod::Auto)
|
|
{
|
|
hw_mon
|
|
.set_fan_control_method(FanControlMethod::Auto)
|
|
.context("Could not set fan control back to automatic")?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_power_states_kind(
|
|
&self,
|
|
gpu_config: Option<&GpuConfig>,
|
|
kind: PowerLevelKind,
|
|
attempt: u32,
|
|
) -> Vec<PowerState> {
|
|
let enabled_states = gpu_config.and_then(|gpu| gpu.power_states.get(&kind));
|
|
let levels = self
|
|
.handle
|
|
.get_clock_levels(kind)
|
|
.inspect(|power_levels| trace!("{kind:?} power states: {power_levels:?}"))
|
|
.inspect_err(|err| debug!("could not get {kind:?} power states: {err:#}"))
|
|
.map(|power_levels| power_levels.levels)
|
|
.unwrap_or_default();
|
|
|
|
if attempt < MAX_PSTATE_READ_ATTEMPTS
|
|
&& levels
|
|
.iter()
|
|
.any(|level| level.value >= u64::from(u16::MAX))
|
|
{
|
|
debug!("GPU reported nonsensical {kind:?} power state values, retrying: {levels:?}");
|
|
return self.get_power_states_kind(gpu_config, kind, attempt + 1);
|
|
}
|
|
|
|
levels
|
|
.into_iter()
|
|
.map(|level| {
|
|
let enabled = match level.id {
|
|
PowerLevelId::Index(index) => {
|
|
enabled_states.is_none_or(|enabled| enabled.contains(&index))
|
|
}
|
|
PowerLevelId::Sleep => true,
|
|
};
|
|
PowerState {
|
|
enabled,
|
|
min_value: None,
|
|
value: level.value,
|
|
id: Some(level.id),
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn first_hw_mon(&self) -> anyhow::Result<&HwMon> {
|
|
self.handle
|
|
.hw_monitors
|
|
.first()
|
|
.context("GPU has no hardware monitor")
|
|
}
|
|
|
|
fn get_clockspeed(&self, metrics: Option<&GpuMetrics>) -> ClockspeedStats {
|
|
let vram_clockspeed = self
|
|
.drm_handle
|
|
.as_ref()
|
|
.and_then(|handle| handle.sensor_info(SENSOR_TYPE::GFX_MCLK).ok())
|
|
.map(u64::from)
|
|
.or_else(|| self.hw_mon_and_then(HwMon::get_vram_clockspeed));
|
|
|
|
let mut sensors = HashMap::new();
|
|
|
|
let mut target_gpu_clockspeed = None;
|
|
|
|
if let Some(metrics) = metrics {
|
|
for (label, value) in [
|
|
("DCLK", metrics.get_current_dclk()),
|
|
("DCLK1", metrics.get_current_dclk1()),
|
|
("FCLK", metrics.get_current_fclk()),
|
|
("VCLK", metrics.get_current_vclk()),
|
|
("VCLK1", metrics.get_current_vclk()),
|
|
("UCLK", metrics.get_current_uclk()),
|
|
("SOCCLK", metrics.get_current_socclk()),
|
|
] {
|
|
if let Some(value) = value
|
|
&& value != 0
|
|
&& value != u16::MAX
|
|
{
|
|
sensors.insert(label.to_owned(), u64::from(value));
|
|
}
|
|
}
|
|
|
|
target_gpu_clockspeed = metrics
|
|
.get_current_gfxclk()
|
|
.filter(|value| *value != u16::MAX)
|
|
.map(u64::from);
|
|
}
|
|
|
|
ClockspeedStats {
|
|
gpu_clockspeed: self.hw_mon_and_then(HwMon::get_gpu_clockspeed),
|
|
target_gpu_clockspeed,
|
|
vram_clockspeed,
|
|
sensors,
|
|
}
|
|
}
|
|
|
|
fn get_full_vbios_version(&self) -> Option<String> {
|
|
if let Some(drm_handle) = &self.drm_handle
|
|
&& let Ok(vbios_info) = drm_handle.get_vbios_info()
|
|
{
|
|
return Some(format!("{} [{}]", vbios_info.ver, vbios_info.date));
|
|
}
|
|
|
|
self.handle.get_vbios_version().ok()
|
|
}
|
|
|
|
fn get_drm_info(&self) -> Option<DrmInfo> {
|
|
use libdrm_amdgpu_sys::AMDGPU::VRAM_TYPE;
|
|
|
|
let cache_info = self
|
|
.kfd_node_path()
|
|
.inspect(|path| debug!("found KFD node path at '{}'", path.display()))
|
|
.and_then(|path| match read_cache_info_from_kfd(&path) {
|
|
Ok(info) => Some(info),
|
|
Err(err) => {
|
|
warn!("could not read cache info from kfd sysfs: {err:#}");
|
|
None
|
|
}
|
|
});
|
|
|
|
trace!("Reading DRM info");
|
|
let drm_handle = self.drm_handle.as_ref();
|
|
|
|
let drm_memory_info =
|
|
drm_handle
|
|
.and_then(|handle| handle.memory_info().ok())
|
|
.map(|memory_info| DrmMemoryInfo {
|
|
resizeable_bar: Some(memory_info.check_resizable_bar()),
|
|
cpu_accessible_used: memory_info.cpu_accessible_vram.heap_usage,
|
|
cpu_accessible_total: memory_info.cpu_accessible_vram.total_heap_size,
|
|
});
|
|
|
|
match drm_handle {
|
|
Some(handle) => handle.device_info().ok().map(|drm_info| DrmInfo {
|
|
device_name: drm_info.find_device_name(),
|
|
pci_revision_id: Some(drm_info.pci_rev_id()),
|
|
family_name: Some(drm_info.get_family_name().to_string()),
|
|
family_id: Some(drm_info.family_id()),
|
|
asic_name: Some(drm_info.get_asic_name().to_string()),
|
|
chip_class: Some(drm_info.get_chip_class().to_string()),
|
|
compute_units: Some(drm_info.cu_active_number),
|
|
isa: drm_info
|
|
.get_gfx_target_version()
|
|
.map(|version| version.to_string()),
|
|
streaming_multiprocessors: None,
|
|
cuda_cores: None,
|
|
vram_type: Some(drm_info.get_vram_type().to_string()),
|
|
vram_vendor: self.handle.get_vram_vendor().ok(),
|
|
vram_clock_ratio: match drm_info.get_vram_type() {
|
|
VRAM_TYPE::GDDR6 => 2.0,
|
|
_ => 1.0,
|
|
},
|
|
amd_ip_info: ALL_HW_IP
|
|
.iter()
|
|
.filter_map(|ip_type| {
|
|
let ip_info = handle.get_hw_ip_info(*ip_type).ok()?;
|
|
|
|
Some(AmdIpInfo {
|
|
ip_type: ip_info.ip_type.to_string(),
|
|
version_major: ip_info.info.hw_ip_version_major,
|
|
version_minor: ip_info.info.hw_ip_version_minor,
|
|
queues: ip_info.info.num_queues(),
|
|
count: ip_info.count,
|
|
})
|
|
})
|
|
.collect(),
|
|
vram_bit_width: Some(drm_info.vram_bit_width),
|
|
vram_max_bw: Some(drm_info.peak_memory_bw_gb().to_string()),
|
|
cache_info,
|
|
memory_info: drm_memory_info,
|
|
rop_info: Some(RopInfo {
|
|
unit_count: drm_info.rb_pipes(),
|
|
operations_factor: if drm_info.get_asic_name().rbplus_allowed() {
|
|
8
|
|
} else {
|
|
4
|
|
},
|
|
operations_count: drm_info.calc_rop_count(),
|
|
}),
|
|
intel: IntelDrmInfo::default(),
|
|
}),
|
|
None => Some(DrmInfo {
|
|
cache_info,
|
|
vram_clock_ratio: 1.0,
|
|
..Default::default()
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn get_link_info(&self) -> LinkInfo {
|
|
#[cfg(not(test))]
|
|
let gpu_pcie_port_bus = self
|
|
.handle
|
|
.get_pci_slot_name()
|
|
.and_then(|name| name.parse::<PCI::BUS_INFO>().ok())
|
|
.map(|bus_info| bus_info.get_gpu_pcie_port_bus());
|
|
#[cfg(test)]
|
|
let gpu_pcie_port_bus: Option<PCI::BUS_INFO> = None;
|
|
let current_link = gpu_pcie_port_bus.and_then(|bus| bus.get_current_link_info());
|
|
let max_pcie_link = gpu_pcie_port_bus.and_then(|bus| bus.get_max_link_info());
|
|
let max_system_link = gpu_pcie_port_bus.and_then(|bus| bus.get_max_system_link());
|
|
|
|
let max_link = if let (Some(max_pcie_link), Some(max_system_link)) =
|
|
(max_pcie_link, max_system_link)
|
|
{
|
|
Some(PCI::LINK {
|
|
r#gen: cmp::min(max_pcie_link.r#gen, max_system_link.r#gen),
|
|
width: cmp::min(max_pcie_link.width, max_system_link.width),
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
|
|
LinkInfo {
|
|
current_width: current_link
|
|
.map(|link| link.width.to_string())
|
|
.or_else(|| self.handle.get_current_link_width().ok()),
|
|
current_speed: current_link
|
|
.map(|link| format!("Gen {}", link.r#gen))
|
|
.or_else(|| self.handle.get_current_link_speed().ok()),
|
|
max_width: max_link
|
|
.map(|link| link.width.to_string())
|
|
.or_else(|| self.handle.get_max_link_width().ok()),
|
|
max_speed: max_link
|
|
.map(|link| format!("Gen {}", link.r#gen))
|
|
.or_else(|| self.handle.get_max_link_speed().ok()),
|
|
}
|
|
}
|
|
|
|
fn debugfs_path(&self) -> Option<PathBuf> {
|
|
let slot_id = self.handle.get_pci_slot_name()?;
|
|
let name_search_term = format!("dev={slot_id}");
|
|
|
|
for entry in fs::read_dir("/sys/kernel/debug/dri").ok()?.flatten() {
|
|
let debugfs_path = entry.path();
|
|
let name_file_path = debugfs_path.join("name");
|
|
|
|
if name_file_path.exists()
|
|
&& let Ok(contents) = fs::read_to_string(&name_file_path)
|
|
&& contents.contains(&name_search_term)
|
|
{
|
|
return Some(debugfs_path);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn apply_clocks_config_require_manual_proformance_level(&self) -> bool {
|
|
self.common.pci_info.device_pci_info.vendor_id == VENDOR_AMD
|
|
&& REQUIRE_MANUAL_DEVICE_IDS
|
|
.contains(&self.common.pci_info.device_pci_info.model_id.as_str())
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
fn kfd_node_path(&self) -> Option<PathBuf> {
|
|
self.drm_handle
|
|
.as_ref()
|
|
.and_then(|handle| handle.get_pci_bus_info().ok())
|
|
.and_then(|bus_info| bus_info.get_drm_render_path().ok())
|
|
.and_then(|path| {
|
|
path.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.map(str::to_owned)
|
|
})
|
|
.and_then(|name| name.strip_prefix("renderD").map(str::to_owned))
|
|
.and_then(|render_minor| {
|
|
let nodes_dir = fs::read_dir("/sys/class/kfd/kfd/topology/nodes").ok()?;
|
|
nodes_dir.into_iter().flatten().find_map(|entry| {
|
|
fs::read_to_string(entry.path().join("properties"))
|
|
.ok()
|
|
.filter(|node_properties| {
|
|
node_properties
|
|
.lines()
|
|
.filter_map(|line| line.split_once(' '))
|
|
.any(|(key, value)| {
|
|
key == "drm_render_minor" && value == render_minor
|
|
})
|
|
})
|
|
.map(|_| entry.path())
|
|
})
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn kfd_node_path(&self) -> Option<PathBuf> {
|
|
self.handle
|
|
.get_path()
|
|
.parent()
|
|
.and_then(|path| path.parent())
|
|
.map(|path| path.join("kfd/topology/nodes/1"))
|
|
}
|
|
}
|
|
|
|
impl GpuController for AmdGpuController {
|
|
fn controller_info(&self) -> &CommonControllerInfo {
|
|
&self.common
|
|
}
|
|
|
|
fn device_type(&self) -> DeviceType {
|
|
self.drm_handle
|
|
.as_ref()
|
|
.and_then(|drm| drm.device_info().ok())
|
|
.map_or(DeviceType::Dedicated, |info| {
|
|
if (info.ids_flags & AMDGPU_IDS_FLAGS_FUSION) > 0 {
|
|
DeviceType::Integrated
|
|
} else {
|
|
DeviceType::Dedicated
|
|
}
|
|
})
|
|
}
|
|
|
|
fn get_info(
|
|
&self,
|
|
unique_vendor: bool,
|
|
include_api_info: bool,
|
|
) -> LocalBoxFuture<'_, DeviceInfo> {
|
|
Box::pin(async move {
|
|
let api_info = if include_api_info {
|
|
self.get_api_info(unique_vendor).await
|
|
} else {
|
|
DeviceApiInfo::default()
|
|
};
|
|
|
|
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();
|
|
let drm_info = self.get_drm_info();
|
|
|
|
let mut flags = vec![DeviceFlag::DumpableVBios];
|
|
|
|
if drm_info
|
|
.as_ref()
|
|
.and_then(|info| info.family_id)
|
|
.is_some_and(|family| family >= AMDGPU_FAMILY_GC_11_0_0)
|
|
{
|
|
flags.push(DeviceFlag::HasPmfw);
|
|
}
|
|
|
|
// Custom fan control can only be used if:
|
|
// - There is a fan with a readable speed
|
|
// - The device does not use PMFW (older than RDNA3) OR has a readable PMFW fan curve (with overdrive on RDNA3+)
|
|
if self
|
|
.hw_mon_and_then(HwMon::get_fan_current)
|
|
.or_else(|| self.hw_mon_and_then(HwMon::get_fan_pwm).map(u32::from))
|
|
.or_else(|| {
|
|
let metrics = GpuMetrics::get_from_sysfs_path(self.handle.get_path()).ok();
|
|
metrics
|
|
.as_ref()
|
|
.and_then(MetricsInfo::get_current_fan_speed)
|
|
.map(u32::from)
|
|
})
|
|
.is_some()
|
|
&& (!flags.contains(&DeviceFlag::HasPmfw) || self.handle.get_fan_curve().is_ok())
|
|
{
|
|
flags.push(DeviceFlag::ConfigurableFanControl);
|
|
}
|
|
|
|
DeviceInfo {
|
|
pci_info,
|
|
api_info,
|
|
driver,
|
|
vbios_version,
|
|
link_info,
|
|
drm_info,
|
|
flags,
|
|
}
|
|
})
|
|
}
|
|
|
|
fn friendly_name(&self) -> Option<String> {
|
|
self.drm_handle
|
|
.as_ref()
|
|
.and_then(|handle| handle.device_info().ok())
|
|
.and_then(|info| info.find_device_name())
|
|
.or_else(|| self.common.pci_info.device_pci_info.model.clone())
|
|
}
|
|
|
|
#[allow(
|
|
clippy::cast_sign_loss,
|
|
clippy::cast_possible_truncation,
|
|
clippy::too_many_lines
|
|
)]
|
|
fn get_stats(&self, gpu_config: Option<&GpuConfig>) -> DeviceStats {
|
|
let metrics = GpuMetrics::get_from_sysfs_path(self.handle.get_path()).ok();
|
|
let metrics = metrics.as_ref();
|
|
|
|
let pmfw_curve = self.handle.get_fan_curve().ok();
|
|
let pmfw_curve_range = pmfw_curve
|
|
.as_ref()
|
|
.and_then(|curve| curve.allowed_ranges.as_ref())
|
|
.map(|range| &range.speed_range);
|
|
|
|
let pwm_max = pmfw_curve_range
|
|
.map(|range| *range.end())
|
|
.map(|percent| (f64::from(percent) * 2.55) as u32)
|
|
.or_else(|| self.hw_mon_and_then(HwMon::get_fan_max_pwm).map(u32::from));
|
|
let pwm_min = pmfw_curve_range
|
|
.map(|range| *range.start())
|
|
.map(|percent| (f64::from(percent) * 2.55) as u32)
|
|
.or_else(|| self.hw_mon_and_then(HwMon::get_fan_min_pwm).map(u32::from));
|
|
|
|
let mut temps: HashMap<String, TemperatureEntry> = HashMap::new();
|
|
|
|
if let Ok(hwmon) = self.first_hw_mon() {
|
|
temps = hwmon
|
|
.get_temps()
|
|
.into_iter()
|
|
.map(|(name, value)| {
|
|
let entry = TemperatureEntry {
|
|
value,
|
|
primary: true,
|
|
display_only: false,
|
|
};
|
|
(name, entry)
|
|
})
|
|
.collect();
|
|
}
|
|
|
|
let mut power_sensors = HashMap::new();
|
|
|
|
let mut throttle_info = None;
|
|
|
|
if let Some(metrics) = metrics {
|
|
let extra_temp_sensors = [
|
|
("soc", metrics.get_temperature_soc().map(|temp| temp / 100)),
|
|
("vrgfx", metrics.get_temperature_vrgfx()),
|
|
("vrmem", metrics.get_temperature_vrmem()),
|
|
("vrsoc", metrics.get_temperature_vrsoc()),
|
|
(
|
|
"skin",
|
|
metrics.get_temperature_skin().map(|temp| temp / 100),
|
|
),
|
|
];
|
|
|
|
for (label, value) in extra_temp_sensors {
|
|
if let Some(value) = value
|
|
&& value != 0
|
|
&& value != u16::MAX
|
|
{
|
|
temps.insert(
|
|
label.to_owned(),
|
|
TemperatureEntry {
|
|
value: Temperature {
|
|
current: Some(f32::from(value)),
|
|
crit: None,
|
|
crit_hyst: None,
|
|
},
|
|
primary: false,
|
|
display_only: true,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
let extra_power_sensors = [
|
|
("apu", metrics.get_average_apu_power()),
|
|
("core", metrics.get_average_all_core_power()),
|
|
("gfx", metrics.get_average_gfx_power_u32()),
|
|
]
|
|
.into_iter()
|
|
.filter(|(_, value)| value.is_some_and(|value| value != 0 && value != u32::MAX))
|
|
.chain(
|
|
[
|
|
("soc", metrics.get_average_soc_power()),
|
|
("cpu", metrics.get_average_cpu_power()),
|
|
]
|
|
.into_iter()
|
|
.filter(|(_, value)| value.is_some_and(|value| value != 0 && value != u16::MAX))
|
|
.map(|(label, value)| (label, value.map(u32::from))),
|
|
);
|
|
|
|
for (label, value) in extra_power_sensors {
|
|
if let Some(value) = value {
|
|
power_sensors.insert(label.to_owned(), f64::from(value) / 1000.0);
|
|
}
|
|
}
|
|
|
|
throttle_info = metrics.get_throttle_status_info().map(|throttle| {
|
|
let mut grouped_bits: HashMap<ThrottlerType, HashSet<u8>> = HashMap::new();
|
|
|
|
for bit in throttle.get_all_throttler() {
|
|
let throttle_type = ThrottlerType::from(bit);
|
|
grouped_bits
|
|
.entry(throttle_type)
|
|
.or_default()
|
|
.insert(bit as u8);
|
|
}
|
|
|
|
grouped_bits
|
|
.into_iter()
|
|
.map(|(throttle_type, bits)| {
|
|
let mut names: Vec<String> = bits
|
|
.into_iter()
|
|
.map(|bit| ThrottlerBit::from(bit).to_string())
|
|
.collect();
|
|
names.sort_unstable();
|
|
(throttle_type.to_string(), names)
|
|
})
|
|
.collect()
|
|
});
|
|
}
|
|
|
|
let mut voltages = HashMap::new();
|
|
|
|
for (label, value) in [
|
|
(
|
|
"northbridge",
|
|
self.hw_mon_and_then(HwMon::get_northbridge_voltage),
|
|
),
|
|
(
|
|
"mem",
|
|
metrics
|
|
.and_then(MetricsInfo::get_voltage_mem)
|
|
.map(u64::from),
|
|
),
|
|
(
|
|
"soc",
|
|
metrics
|
|
.and_then(MetricsInfo::get_voltage_soc)
|
|
.map(u64::from),
|
|
),
|
|
] {
|
|
if let Some(value) = value {
|
|
voltages.insert(label.to_owned(), value);
|
|
}
|
|
}
|
|
|
|
let fan_settings = gpu_config.and_then(|config| config.fan_control_settings.as_ref());
|
|
DeviceStats {
|
|
fan: FanStats {
|
|
control_enabled: gpu_config.is_some_and(|config| config.fan_control_enabled),
|
|
control_mode: fan_settings.map(|settings| settings.mode),
|
|
static_speed: fan_settings.map(|settings| settings.static_speed),
|
|
curve: fan_settings.map(|settings| settings.curve.0.clone()),
|
|
spindown_delay_ms: fan_settings.and_then(|settings| settings.spindown_delay_ms),
|
|
change_threshold: fan_settings.and_then(|settings| settings.change_threshold),
|
|
temperature_key: fan_settings.map(|settings| settings.temperature_key.clone()),
|
|
auto_threshold: None,
|
|
speed_current: self.hw_mon_and_then(HwMon::get_fan_current).or_else(|| {
|
|
metrics
|
|
.and_then(MetricsInfo::get_current_fan_speed)
|
|
.map(u32::from)
|
|
}),
|
|
speed_max: self.hw_mon_and_then(HwMon::get_fan_max),
|
|
speed_min: self.hw_mon_and_then(HwMon::get_fan_min),
|
|
pwm_current: self.hw_mon_and_then(HwMon::get_fan_pwm).or_else(|| {
|
|
metrics
|
|
.and_then(MetricsInfo::get_fan_pwm)
|
|
.and_then(|pwm| u8::try_from(pwm).ok())
|
|
}),
|
|
pwm_max,
|
|
pwm_min,
|
|
temperature_range: pmfw_curve
|
|
.as_ref()
|
|
.and_then(|curve| curve.allowed_ranges.as_ref())
|
|
.map(|range| {
|
|
(
|
|
*range.temperature_range.start(),
|
|
*range.temperature_range.end(),
|
|
)
|
|
}),
|
|
pmfw_info: PmfwInfo {
|
|
acoustic_limit: self.handle.get_fan_acoustic_limit().ok(),
|
|
acoustic_target: self.handle.get_fan_acoustic_target().ok(),
|
|
target_temp: self.handle.get_fan_target_temperature().ok(),
|
|
minimum_pwm: self.handle.get_fan_minimum_pwm().ok(),
|
|
zero_rpm_enable: self.handle.get_fan_zero_rpm_enable().ok(),
|
|
zero_rpm_temperature: self.handle.get_fan_zero_rpm_stop_temperature().ok(),
|
|
},
|
|
},
|
|
clockspeed: self.get_clockspeed(metrics),
|
|
voltage: VoltageStats {
|
|
gpu: self.hw_mon_and_then(HwMon::get_gpu_voltage),
|
|
sensors: voltages,
|
|
},
|
|
vram: VramStats {
|
|
total: self.handle.get_total_vram().ok(),
|
|
used: self.handle.get_used_vram().ok(),
|
|
},
|
|
power: PowerStats {
|
|
average: self.hw_mon_and_then(HwMon::get_power_average),
|
|
current: self.hw_mon_and_then(HwMon::get_power_input),
|
|
cap_current: self.hw_mon_and_then(HwMon::get_power_cap),
|
|
cap_max: self.hw_mon_and_then(HwMon::get_power_cap_max),
|
|
cap_min: self.hw_mon_and_then(HwMon::get_power_cap_min),
|
|
cap_default: self.hw_mon_and_then(HwMon::get_power_cap_default),
|
|
sensors: power_sensors,
|
|
},
|
|
temps,
|
|
busy_percent: self.handle.get_busy_percent().ok(),
|
|
performance_level: self.handle.get_power_force_performance_level().ok(),
|
|
active_power_states: {
|
|
let active_power_states = ActivePowerStates {
|
|
core: self
|
|
.handle
|
|
.get_core_clock_levels()
|
|
.inspect_err(|err| debug!("could not get active core power state: {err:#}"))
|
|
.ok()
|
|
.and_then(|levels| levels.active),
|
|
memory: self
|
|
.handle
|
|
.get_memory_clock_levels()
|
|
.inspect_err(|err| {
|
|
debug!("could not get active memory power state: {err:#}");
|
|
})
|
|
.ok()
|
|
.and_then(|levels| levels.active),
|
|
pcie: self
|
|
.handle
|
|
.get_pcie_clock_levels()
|
|
.inspect_err(|err| debug!("could not get active PCIe power state: {err:#}"))
|
|
.ok()
|
|
.and_then(|levels| levels.active),
|
|
};
|
|
(active_power_states.core.is_some()
|
|
|| active_power_states.memory.is_some()
|
|
|| active_power_states.pcie.is_some())
|
|
.then_some(active_power_states)
|
|
},
|
|
throttle_info,
|
|
}
|
|
}
|
|
|
|
fn get_clocks_info(&self, _gpu_config: Option<&GpuConfig>) -> anyhow::Result<ClocksInfo> {
|
|
let clocks_table = self
|
|
.handle
|
|
.get_clocks_table()
|
|
.context("Clocks table not available")?;
|
|
|
|
Ok(clocks_table.into())
|
|
}
|
|
|
|
fn get_power_states(&self, gpu_config: Option<&GpuConfig>) -> PowerStates {
|
|
let core = self.get_power_states_kind(gpu_config, PowerLevelKind::CoreClock, 0);
|
|
let vram = self.get_power_states_kind(gpu_config, PowerLevelKind::MemoryClock, 0);
|
|
PowerStates { core, vram }
|
|
}
|
|
|
|
fn get_power_profile_modes(&self) -> anyhow::Result<PowerProfileModesTable> {
|
|
Ok(self.handle.get_power_profile_modes()?)
|
|
}
|
|
|
|
fn reset_pmfw_settings(&self) {
|
|
let handle = &self.handle;
|
|
if self.handle.get_fan_target_temperature().is_ok()
|
|
&& let Err(err) = handle.reset_fan_target_temperature()
|
|
{
|
|
warn!("Could not reset target temperature: {err:#}");
|
|
}
|
|
|
|
if self.handle.get_fan_acoustic_target().is_ok()
|
|
&& let Err(err) = handle.reset_fan_acoustic_target()
|
|
{
|
|
warn!("Could not reset acoustic target: {err:#}");
|
|
}
|
|
|
|
if self.handle.get_fan_acoustic_limit().is_ok()
|
|
&& let Err(err) = handle.reset_fan_acoustic_limit()
|
|
{
|
|
warn!("Could not reset acoustic limit: {err:#}");
|
|
}
|
|
|
|
if self.handle.get_fan_minimum_pwm().is_ok()
|
|
&& let Err(err) = handle.reset_fan_minimum_pwm()
|
|
{
|
|
warn!("Could not reset minimum pwm: {err:#}");
|
|
}
|
|
}
|
|
|
|
fn vbios_dump(&self) -> anyhow::Result<Vec<u8>> {
|
|
let debugfs = self.debugfs_path().context("DebugFS not found")?;
|
|
fs::read(debugfs.join("amdgpu_vbios")).context("Could not read VBIOS file")
|
|
}
|
|
|
|
#[allow(clippy::too_many_lines)]
|
|
fn apply_config<'a>(&'a self, config: &'a GpuConfig) -> LocalBoxFuture<'a, anyhow::Result<()>> {
|
|
Box::pin(async {
|
|
let mut commit_handles = VecDeque::new();
|
|
|
|
// Reset the clocks table in case the settings get reverted back to not having a clocks value configured
|
|
self.handle.reset_clocks_table().ok();
|
|
|
|
if !config.fan_control_enabled {
|
|
self.stop_fan_control(true)
|
|
.await
|
|
.context("Failed to stop fan control")?;
|
|
}
|
|
|
|
if let Some(PerformanceLevel::High | PerformanceLevel::Low) = config.performance_level {
|
|
// Reset to auto first
|
|
self.handle
|
|
.set_power_force_performance_level(PerformanceLevel::Auto)
|
|
.context("Failed to set power performance level")?;
|
|
}
|
|
|
|
if self.apply_clocks_config_require_manual_proformance_level() {
|
|
// Van Gogh/Sephiroth only allow clock settings to be used with manual performance mode
|
|
self.handle
|
|
.set_power_force_performance_level(PerformanceLevel::Manual)
|
|
.ok();
|
|
}
|
|
|
|
if config.is_core_clocks_used() {
|
|
match self.handle.get_clocks_table() {
|
|
Ok(original_table) => {
|
|
let mut table = original_table.clone();
|
|
apply_clocks_config_to_table(&config.clocks_configuration, &mut table)
|
|
.context("Failed to apply clocks configuration to table")?;
|
|
|
|
debug!(
|
|
"writing clocks commands: {:#?}",
|
|
table
|
|
.get_commands(&original_table)
|
|
.context("Failed to get table commands")?
|
|
);
|
|
|
|
let handle = self
|
|
.handle
|
|
.set_clocks_table(&table)
|
|
.context("Could not write clocks table")
|
|
.with_context(|| {
|
|
format!(
|
|
"Clocks table commands: {:?}",
|
|
table.get_commands(&original_table)
|
|
)
|
|
})?;
|
|
commit_handles.push_back(handle);
|
|
}
|
|
Err(err) => {
|
|
error!(
|
|
"custom clock settings are present but will be ignored, could not get clocks table: {err}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut deferred_performance_level = None;
|
|
match self.handle.get_power_force_performance_level() {
|
|
Ok(_) => {
|
|
let performance_level =
|
|
config.performance_level.unwrap_or(PerformanceLevel::Auto);
|
|
|
|
match performance_level {
|
|
PerformanceLevel::Auto | PerformanceLevel::Manual => {
|
|
self.handle
|
|
.set_power_force_performance_level(performance_level)
|
|
.context("Failed to set power performance level")?;
|
|
}
|
|
_ => {
|
|
deferred_performance_level = Some(performance_level);
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
error!("could not get current performance level: {err}");
|
|
}
|
|
}
|
|
|
|
if let Some(mode_index) = config.power_profile_mode_index {
|
|
if config.performance_level != Some(PerformanceLevel::Manual) {
|
|
return Err(anyhow!(
|
|
"Performance level has to be set to `manual` to use power profile modes"
|
|
));
|
|
}
|
|
|
|
if config.custom_power_profile_mode_hueristics.is_empty() {
|
|
self.handle
|
|
.set_active_power_profile_mode(mode_index)
|
|
.context("Failed to set active power profile mode")?;
|
|
} else {
|
|
self.handle
|
|
.set_custom_power_profile_mode_heuristics(
|
|
&config.custom_power_profile_mode_hueristics,
|
|
)
|
|
.context("Failed to set custom power profile mode heuristics")?;
|
|
}
|
|
}
|
|
|
|
if config.fan_control_enabled {
|
|
if let Some(ref settings) = config.fan_control_settings {
|
|
match settings.mode {
|
|
lact_schema::FanControlMode::Static => {
|
|
let fan_handles = self
|
|
.set_static_fan_control(settings.static_speed)
|
|
.await
|
|
.context("Failed to set static fan control")?;
|
|
|
|
for handle in fan_handles {
|
|
commit_handles.push_front(handle);
|
|
}
|
|
}
|
|
lact_schema::FanControlMode::Curve => {
|
|
if settings.curve.0.is_empty() {
|
|
return Err(anyhow!("Cannot use empty fan curve"));
|
|
}
|
|
|
|
if let Some(commit_handle) = self
|
|
.start_curve_fan_control(settings.curve.clone(), settings.clone())
|
|
.await
|
|
.context("Failed to set curve fan control")?
|
|
{
|
|
commit_handles.push_front(commit_handle);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return Err(anyhow!(
|
|
"Trying to enable fan control with no settings provided"
|
|
));
|
|
}
|
|
} else {
|
|
let pmfw = &config.pmfw_options;
|
|
if let Some(acoustic_limit) = pmfw.acoustic_limit
|
|
&& self
|
|
.handle
|
|
.get_fan_acoustic_limit()
|
|
.context("Could not get acoustic limit")?
|
|
.current
|
|
!= acoustic_limit
|
|
{
|
|
let commit_handle = self
|
|
.handle
|
|
.set_fan_acoustic_limit(acoustic_limit)
|
|
.context("Could not set acoustic limit")?;
|
|
commit_handles.push_front(commit_handle);
|
|
}
|
|
|
|
if let Some(acoustic_target) = pmfw.acoustic_target
|
|
&& self
|
|
.handle
|
|
.get_fan_acoustic_target()
|
|
.context("Could not get acoustic target")?
|
|
.current
|
|
!= acoustic_target
|
|
{
|
|
let commit_handle = self
|
|
.handle
|
|
.set_fan_acoustic_target(acoustic_target)
|
|
.context("Could not set acoustic target")?;
|
|
commit_handles.push_front(commit_handle);
|
|
}
|
|
|
|
if let Some(target_temperature) = pmfw.target_temperature
|
|
&& self
|
|
.handle
|
|
.get_fan_target_temperature()
|
|
.context("Could not get target temperature")?
|
|
.current
|
|
!= target_temperature
|
|
{
|
|
let commit_handle = self
|
|
.handle
|
|
.set_fan_target_temperature(target_temperature)
|
|
.context("Could not set target temperature")?;
|
|
commit_handles.push_front(commit_handle);
|
|
}
|
|
|
|
if let Some(minimum_pwm) = pmfw.minimum_pwm
|
|
&& self
|
|
.handle
|
|
.get_fan_minimum_pwm()
|
|
.context("Could not get minimum pwm")?
|
|
.current
|
|
!= minimum_pwm
|
|
{
|
|
let commit_handle = self
|
|
.handle
|
|
.set_fan_minimum_pwm(minimum_pwm)
|
|
.context("Could not set minimum pwm")?;
|
|
commit_handles.push_front(commit_handle);
|
|
}
|
|
}
|
|
|
|
// Unlike the other PMFW options, zero rpm should be applied with a custom curve as well (but not in static mode)
|
|
if !(config.fan_control_enabled
|
|
&& config
|
|
.fan_control_settings
|
|
.as_ref()
|
|
.is_some_and(|fan| fan.mode == FanControlMode::Static))
|
|
{
|
|
if let Some(zero_rpm) = config.pmfw_options.zero_rpm {
|
|
match self.handle.get_fan_zero_rpm_enable() {
|
|
Ok(current_zero_rpm) => {
|
|
if current_zero_rpm != zero_rpm {
|
|
let commit_handle = self
|
|
.handle
|
|
.set_fan_zero_rpm_enable(zero_rpm)
|
|
.context("Could not set zero RPM mode")?;
|
|
commit_handles.push_front(commit_handle);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
error!(
|
|
"zero RPM is present in the config, but not available on the GPU: {err}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(zero_rpm_threshold) = config.pmfw_options.zero_rpm_threshold {
|
|
match self.handle.get_fan_zero_rpm_stop_temperature() {
|
|
Ok(current_threshold) => {
|
|
if current_threshold.current != zero_rpm_threshold {
|
|
let commit_handle = self
|
|
.handle
|
|
.set_fan_zero_rpm_stop_temperature(zero_rpm_threshold)
|
|
.context("Could not set zero RPM temperature")?;
|
|
commit_handles.push_front(commit_handle);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
error!(
|
|
"zero RPM threshold is present in the config, but not available on the GPU: {err}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(configured_cap) = config.power_cap {
|
|
let hw_mon = self.first_hw_mon()?;
|
|
|
|
match (hw_mon.get_power_cap_min(), hw_mon.get_power_cap_max()) {
|
|
(Ok(min), Ok(max)) => {
|
|
let clamped_cap = configured_cap.clamp(min, max);
|
|
|
|
if clamped_cap != configured_cap {
|
|
warn!(
|
|
"Power cap {configured_cap}W was outside of the allowed range, clamped to {clamped_cap}W"
|
|
);
|
|
}
|
|
|
|
hw_mon.set_power_cap(clamped_cap).with_context(|| {
|
|
format!("Failed to set power cap: {configured_cap}")
|
|
})?;
|
|
}
|
|
(Err(err), _) | (_, Err(err)) => {
|
|
bail!("Could not read allowed power cap range: {err:#}");
|
|
}
|
|
}
|
|
} else if let Ok(hw_mon) = self.first_hw_mon()
|
|
&& let Ok(default_cap) = hw_mon.get_power_cap_default()
|
|
&& Ok(default_cap) != hw_mon.get_power_cap()
|
|
{
|
|
hw_mon.set_power_cap(default_cap).with_context(|| {
|
|
format!("Failed to set power cap to default cap: {default_cap}")
|
|
})?;
|
|
}
|
|
|
|
for handle in commit_handles {
|
|
handle.commit()?;
|
|
}
|
|
|
|
if let Some(performance_level) = deferred_performance_level {
|
|
self.handle
|
|
.set_power_force_performance_level(performance_level)
|
|
.context("Failed to set power performance level")?;
|
|
}
|
|
|
|
for (kind, states) in &config.power_states {
|
|
if config.performance_level != Some(PerformanceLevel::Manual) {
|
|
return Err(anyhow!(
|
|
"Performance level has to be set to `manual` to configure power states"
|
|
));
|
|
}
|
|
|
|
self.handle
|
|
.set_enabled_power_levels(*kind, states)
|
|
.with_context(|| format!("Could not set {kind:?} power states"))?;
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn reset_clocks(&self) -> anyhow::Result<()> {
|
|
if self.handle.get_clocks_table().is_err() {
|
|
return Ok(());
|
|
}
|
|
|
|
self.handle.reset_clocks_table()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn cleanup(&self) -> LocalBoxFuture<'_, ()> {
|
|
async {
|
|
if let Some((fan_notify, fan_handle)) = self.fan_control_handle.take() {
|
|
debug!("sending stop notification to old fan control task");
|
|
fan_notify.notify_one();
|
|
fan_handle.await.unwrap();
|
|
debug!("finished controller cleanup");
|
|
}
|
|
}
|
|
.boxed_local()
|
|
}
|
|
|
|
fn process_list(&self) -> anyhow::Result<ProcessList> {
|
|
let mut last_total_time_map = self.last_drm_util.borrow_mut();
|
|
fdinfo::read_process_list(
|
|
&self.common,
|
|
DRM_VRAM_KEYS,
|
|
DRM_ENGINES,
|
|
&mut last_total_time_map,
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "display-info")]
|
|
fn populate_displays_info(&self, info: &mut DisplaysInfo) -> anyhow::Result<()> {
|
|
let debugfs = self.debugfs_path().context("Could not get debugfs")?;
|
|
|
|
for (connector, info) in &mut info.displays {
|
|
if let DisplayConnector::DisplayPort {
|
|
lanes,
|
|
bandwidth,
|
|
embedded: _,
|
|
} = &mut info.connector_type
|
|
{
|
|
let link_settings_path = debugfs.join(connector).join("link_settings");
|
|
let link_settings = fs::read_to_string(link_settings_path)?;
|
|
|
|
let mut parts = link_settings.split_ascii_whitespace().skip(1);
|
|
|
|
*lanes = parts
|
|
.next()
|
|
.context("Missing lane count")?
|
|
.parse::<u16>()
|
|
.context("Invalid lane count")?;
|
|
|
|
let bw_enum = parts
|
|
.next()
|
|
.context("Missing bandwidth")?
|
|
.strip_prefix("0x")
|
|
.and_then(|value| u32::from_str_radix(value, 16).ok())
|
|
.context("Invalid bandwidth value")?;
|
|
|
|
*bandwidth = crate::server::display::dp_rate_to_bandwidth(bw_enum);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
fn get_drm_handle(
|
|
common: &CommonControllerInfo,
|
|
libdrm_amdgpu: &LibDrmAmdgpu,
|
|
) -> anyhow::Result<DrmHandle> {
|
|
let path = common.get_drm_render()?;
|
|
let drm_file = fs::OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.open(&path)
|
|
.with_context(|| format!("Could not open drm file at {}", path.display()))?;
|
|
let (handle, _, _) = libdrm_amdgpu
|
|
.init_device_handle_with_fd(drm_file)
|
|
.map_err(|err| anyhow!("Could not open drm handle, error code {err}"))?;
|
|
Ok(handle)
|
|
}
|
|
|
|
fn apply_clocks_config_to_table(
|
|
config: &ClocksConfiguration,
|
|
table: &mut ClocksTableGen,
|
|
) -> anyhow::Result<()> {
|
|
if let ClocksTableGen::Rdna(table) = table {
|
|
// Avoid writing settings to the clocks table except the user-specified ones
|
|
// There is an issue on some GPU models where the default values are actually outside of the allowed range
|
|
// See https://github.com/sibradzic/amdgpu-clocks/issues/32#issuecomment-829953519 (part 2) for an example
|
|
table.clear();
|
|
|
|
// Normalize the VDDC curve - make sure all of the values are within the allowed range
|
|
table.normalize_vddc_curve();
|
|
|
|
match config.voltage_offset {
|
|
Some(offset) => table.set_voltage_offset(offset)?,
|
|
None => table.voltage_offset = None,
|
|
}
|
|
|
|
if let Some(offset) = config.gpu_clock_offsets.get(&0) {
|
|
table.sclk_offset = Some(*offset);
|
|
}
|
|
}
|
|
|
|
if let Some(min_clockspeed) = config.min_core_clock {
|
|
table.set_min_sclk(min_clockspeed)?;
|
|
}
|
|
if let Some(min_clockspeed) = config.min_memory_clock {
|
|
table.set_min_mclk(min_clockspeed)?;
|
|
}
|
|
if let Some(min_voltage) = config.min_voltage {
|
|
table.set_min_voltage(min_voltage)?;
|
|
}
|
|
|
|
if let Some(clockspeed) = config.max_core_clock {
|
|
table.set_max_sclk(clockspeed)?;
|
|
}
|
|
if let Some(clockspeed) = config.max_memory_clock {
|
|
table.set_max_mclk(clockspeed)?;
|
|
}
|
|
if let Some(voltage) = config.max_voltage {
|
|
table.set_max_voltage(voltage)?;
|
|
}
|
|
|
|
if !config.gpu_vf_curve.is_empty() {
|
|
// Validate first
|
|
for (i, point) in &config.gpu_vf_curve {
|
|
if let Some(clockspeed) = point.clockspeed {
|
|
let range = match table {
|
|
ClocksTableGen::Gcn(table) => table.od_range.sclk,
|
|
ClocksTableGen::Rdna(table) => table
|
|
.od_range
|
|
.curve_sclk_points
|
|
.get(*i as usize)
|
|
.copied()
|
|
.unwrap_or_default(),
|
|
};
|
|
let (min, max) = range.into_full().context("SCLK range not present")?;
|
|
|
|
if !(min..=max).contains(&clockspeed) {
|
|
bail!("Clockspeed {clockspeed} is not in the allowed range {min}-{max}");
|
|
}
|
|
}
|
|
|
|
if let Some(voltage) = point.voltage {
|
|
let range = match table {
|
|
ClocksTableGen::Gcn(table) => table.od_range.vddc,
|
|
ClocksTableGen::Rdna(table) => table
|
|
.od_range
|
|
.curve_voltage_points
|
|
.get(*i as usize)
|
|
.copied(),
|
|
};
|
|
let (min, max) = range
|
|
.unwrap_or_default()
|
|
.into_full()
|
|
.context("Voltage range not present")?;
|
|
|
|
if !(min..=max).contains(&voltage) {
|
|
bail!("Voltage {voltage} is not in the allowed range {min}-{max}");
|
|
}
|
|
}
|
|
}
|
|
|
|
let points = match table {
|
|
ClocksTableGen::Gcn(table) => &mut table.sclk_levels,
|
|
ClocksTableGen::Rdna(table) => &mut table.vddc_curve,
|
|
};
|
|
for (i, point) in &config.gpu_vf_curve {
|
|
let level = points
|
|
.get_mut(*i as usize)
|
|
.with_context(|| format!("GPU does not have a p-state {i}"))?;
|
|
|
|
if let Some(clockspeed) = point.clockspeed {
|
|
level.clockspeed = clockspeed;
|
|
}
|
|
|
|
if let Some(voltage) = point.voltage {
|
|
level.voltage = voltage;
|
|
}
|
|
}
|
|
|
|
// Apply RDNA1 OD_SCLK options in addition to the vddc curve
|
|
if let ClocksTableGen::Rdna(table) = table {
|
|
if let Some(min_clock) = config.gpu_vf_curve.get(&0) {
|
|
table.current_sclk_range.min = min_clock.clockspeed;
|
|
}
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
if let Some(max_clock) = config.gpu_vf_curve.get(&(table.vddc_curve.len() as u8 - 1)) {
|
|
table.current_sclk_range.max = max_clock.clockspeed;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !config.mem_vf_curve.is_empty() {
|
|
match table {
|
|
ClocksTableGen::Gcn(table) => {
|
|
// Validate first
|
|
for (_, point) in &config.mem_vf_curve {
|
|
if let Some(clockspeed) = point.clockspeed {
|
|
let (min, max) = table
|
|
.od_range
|
|
.mclk
|
|
.unwrap_or_default()
|
|
.into_full()
|
|
.context("MCLK range not present")?;
|
|
|
|
if !(min..=max).contains(&clockspeed) {
|
|
bail!(
|
|
"Memory clockspeed {clockspeed} is not in the allowed range {min}-{max}"
|
|
);
|
|
}
|
|
}
|
|
|
|
if let Some(voltage) = point.voltage {
|
|
let (min, max) = table
|
|
.od_range
|
|
.vddc
|
|
.unwrap_or_default()
|
|
.into_full()
|
|
.context("Voltage range not present")?;
|
|
|
|
if !(min..=max).contains(&voltage) {
|
|
bail!("Voltage {voltage} is not in the allowed range {min}-{max}");
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i, point) in &config.mem_vf_curve {
|
|
let level = table
|
|
.mclk_levels
|
|
.get_mut(*i as usize)
|
|
.with_context(|| format!("GPU does not have a p-state {i}"))?;
|
|
|
|
if let Some(clockspeed) = point.clockspeed {
|
|
level.clockspeed = clockspeed;
|
|
}
|
|
|
|
if let Some(voltage) = point.voltage {
|
|
level.voltage = voltage;
|
|
}
|
|
}
|
|
}
|
|
ClocksTableGen::Rdna(_) => bail!("VRAM VF curve not supported"),
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn read_cache_info_from_kfd(node_path: &Path) -> anyhow::Result<CacheInfo> {
|
|
let mut caches: BTreeMap<AmdCacheInstance, u16> = BTreeMap::new();
|
|
|
|
let caches_dir = fs::read_dir(node_path.join("caches"))?;
|
|
for entry in caches_dir {
|
|
let entry = entry?;
|
|
|
|
let contents = fs::read_to_string(entry.path().join("properties"))
|
|
.context("Could not read cache properties")?;
|
|
let cache_info = contents
|
|
.lines()
|
|
.filter_map(|line| line.split_once(' '))
|
|
.collect::<HashMap<_, _>>();
|
|
|
|
if let (Some(cache_type), Some(level), Some(size), Some(sibling_map)) = (
|
|
cache_info.get("type"),
|
|
cache_info.get("level"),
|
|
cache_info.get("size"),
|
|
cache_info.get("sibling_map"),
|
|
) {
|
|
let level: u8 = level.parse().context("Invalid cache size")?;
|
|
let size: u32 = size.parse().context("Invalid cache size")?;
|
|
let cache_type: u32 = cache_type.parse().context("Invalid cache size")?;
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
let cu_count = sibling_map
|
|
.split(',')
|
|
.take_while(|item| *item == "1")
|
|
.count() as u16;
|
|
|
|
let types = [
|
|
(HSA_CACHE_TYPE_DATA, CacheType::Data),
|
|
(HSA_CACHE_TYPE_INSTRUCTION, CacheType::Instruction),
|
|
(HSA_CACHE_TYPE_CPU, CacheType::Cpu),
|
|
]
|
|
.into_iter()
|
|
.filter(|(mask, _)| cache_type & mask > 0)
|
|
.map(|(_, cache_type)| cache_type)
|
|
.collect();
|
|
|
|
let instance = AmdCacheInstance {
|
|
level,
|
|
types,
|
|
size: size * 1024,
|
|
cu_count,
|
|
};
|
|
*caches.entry(instance).or_default() += 1;
|
|
}
|
|
}
|
|
let mut items = caches.into_iter().collect::<Vec<_>>();
|
|
|
|
items.sort_by_key(|(instance, _)| {
|
|
(
|
|
instance.level,
|
|
instance.cu_count,
|
|
instance.size,
|
|
instance.types.clone(),
|
|
)
|
|
});
|
|
|
|
Ok(CacheInfo::Amd(items))
|
|
}
|