Skip to content

Commit

Permalink
feat: add support for custom fan curves and static fan speed on RDNA3 (
Browse files Browse the repository at this point in the history
…#248)

* feat: add support for custom fan curves and static fan speed on RDNA3

* fix: use gpu-provided min and max fan speed values

* fix: set min temperature point too in static curve

* fix: reset fan curve when switching mode back

* fix: normalize fan speed values by taking min speed into account

* test

* Revert "test"

This reverts commit dbd37bd.

* fix: dont use the upper range value as visibility trigger for oc scales

* fix visibility

* Revert "fix: normalize fan speed values by taking min speed into account"

This reverts commit 945cfd2.

* fix: get_adjustment_value

* doc: update README
  • Loading branch information
ilya-zlobintsev authored Jan 20, 2024
1 parent a8c2c60 commit 8a85b2b
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 115 deletions.
5 changes: 2 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Tested GPU generations:
- [X] Vega
- [X] RDNA1 (RX 5000 series)
- [X] RDNA2 (RX 6000 series)
- [ ] RDNA3 (RX 7000 series) - basic support available. Fan control available via thermal target settings, but full custom curve support is currently missing. Requires Kernel 6.7+
- [X] RDNA3 (RX 7000 series) - Requires Kernel 6.7+

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.
Expand Down
62 changes: 55 additions & 7 deletions lact-daemon/src/server/gpu_controller/fan_control.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use anyhow::anyhow;
use lact_schema::{amdgpu_sysfs::hw_mon::Temperature, default_fan_curve, FanCurveMap};
use std::cmp;

use anyhow::{anyhow, Context};
use lact_schema::{
amdgpu_sysfs::{gpu_handle::fan_control::FanCurve as PmfwCurve, hw_mon::Temperature},
default_fan_curve, FanCurveMap,
};
use serde::{Deserialize, Serialize};
use tracing::warn;

Expand Down Expand Up @@ -39,6 +44,36 @@ impl FanCurve {

(f32::from(u8::MAX) * percentage) as u8
}

pub fn into_pmfw_curve(self, current_pmfw_curve: PmfwCurve) -> anyhow::Result<PmfwCurve> {
if current_pmfw_curve.points.len() != self.0.len() {
return Err(anyhow!(
"The GPU only supports {} curve points, given {}",
current_pmfw_curve.points.len(),
self.0.len()
));
}
let allowed_ranges = current_pmfw_curve
.allowed_ranges
.context("The GPU does not allow fan curve modifications")?;
let min_pwm = *allowed_ranges.speed_range.start();
let max_pwm = f32::from(*allowed_ranges.speed_range.end());

let points = self
.0
.into_iter()
.map(|(temp, ratio)| {
let custom_pwm = (max_pwm * ratio) as u8;
let pwm = cmp::max(min_pwm, custom_pwm);
(temp, pwm)
})
.collect();

Ok(PmfwCurve {
points,
allowed_ranges: Some(allowed_ranges),
})
}
}

impl FanCurve {
Expand All @@ -60,8 +95,8 @@ impl Default for FanCurve {

#[cfg(test)]
mod tests {
use super::FanCurve;
use lact_schema::amdgpu_sysfs::hw_mon::Temperature;
use super::{FanCurve, PmfwCurve};
use lact_schema::amdgpu_sysfs::{gpu_handle::fan_control::FanCurveRanges, hw_mon::Temperature};

fn simple_pwm(temp: f32) -> u8 {
let curve = FanCurve([(0, 0.0), (100, 1.0)].into());
Expand Down Expand Up @@ -147,9 +182,7 @@ mod tests {
};
curve.pwm_at_temp(temp)
};
assert_eq!(pwm_at_temp(20.0), 0);
assert_eq!(pwm_at_temp(30.0), 0);
assert_eq!(pwm_at_temp(33.0), 15);
assert_eq!(pwm_at_temp(40.0), 51);
assert_eq!(pwm_at_temp(60.0), 127);
assert_eq!(pwm_at_temp(65.0), 159);
assert_eq!(pwm_at_temp(70.0), 191);
Expand All @@ -158,4 +191,19 @@ mod tests {
assert_eq!(pwm_at_temp(100.0), 255);
assert_eq!(pwm_at_temp(-5.0), 255);
}

#[test]
fn default_curve_to_pmfw() {
let curve = FanCurve::default();
let current_pmfw_curve = PmfwCurve {
points: Box::new([(0, 0); 5]),
allowed_ranges: Some(FanCurveRanges {
temperature_range: 15..=90,
speed_range: 20..=100,
}),
};
let pmfw_curve = curve.into_pmfw_curve(current_pmfw_curve).unwrap();
let expected_points = [(40, 20), (50, 35), (60, 50), (70, 75), (80, 100)];
assert_eq!(&expected_points, pmfw_curve.points.as_ref());
}
}
157 changes: 111 additions & 46 deletions lact-daemon/src/server/gpu_controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use lact_schema::{
amdgpu_sysfs::{
error::Error,
gpu_handle::{
fan_control::FanCurve as PmfwCurve,
overdrive::{ClocksTable, ClocksTableGen},
GpuHandle, PerformanceLevel, PowerLevelKind, PowerLevels,
},
Expand All @@ -24,6 +25,7 @@ use pciid_parser::Database;
use std::{
borrow::Cow,
cell::RefCell,
cmp,
path::{Path, PathBuf},
rc::Rc,
str::FromStr,
Expand Down Expand Up @@ -331,34 +333,93 @@ impl GpuController {
// Stop existing task to set static speed
self.stop_fan_control(false).await?;

let hw_mon = self
.handle
.hw_monitors
.first()
.cloned()
.context("This GPU has no monitor")?;
// Use PMFW curve functionality for static speed when it is available
if let Ok(current_curve) = self.handle.get_fan_curve() {
let allowed_ranges = current_curve.allowed_ranges.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 = (f64::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));
}

hw_mon
.set_fan_control_method(FanControlMethod::Manual)
.context("Could not set fan control method")?;
let new_curve = PmfwCurve {
points: points.into_boxed_slice(),
allowed_ranges: Some(allowed_ranges),
};

#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let static_speed_converted = (f64::from(u8::MAX) * static_speed) as u8;
debug!("setting static curve {new_curve:?}");

hw_mon
.set_fan_pwm(static_speed_converted)
.context("could not set fan speed")?;
self.handle
.set_fan_curve(&new_curve)
.context("Could not set fan curve")?;

debug!("set fan speed to {}", static_speed);
Ok(())
} else {
let hw_mon = self
.handle
.hw_monitors
.first()
.cloned()
.context("This GPU has no monitor")?;

Ok(())
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 = (f64::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(())
}
}

async fn start_curve_fan_control(
&self,
curve: FanCurve,
temp_key: String,
interval: Duration,
) -> anyhow::Result<()> {
// Use the PMFW curve functionality when it is available
// Otherwise, fall back to manual fan control via a task
match self.handle.get_fan_curve() {
Ok(current_curve) => {
let new_curve = curve
.into_pmfw_curve(current_curve)
.context("Invalid fan curve")?;
debug!("setting pmfw curve {new_curve:?}");

self.handle
.set_fan_curve(&new_curve)
.context("Could not set fan curve")?;

Ok(())
}
Err(_) => {
self.start_curve_fan_control_task(curve, temp_key, interval)
.await
}
}
}

async fn start_curve_fan_control_task(
&self,
curve: FanCurve,
temp_key: String,
interval: Duration,
) -> anyhow::Result<()> {
// Stop existing task to re-apply new curve
self.stop_fan_control(false).await?;
Expand Down Expand Up @@ -425,6 +486,12 @@ impl GpuController {
}

if reset_mode {
if self.handle.get_fan_curve().is_ok() {
if 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() {
if let Ok(current_control) = hw_mon.get_fan_control_method() {
if !matches!(current_control, FanControlMethod::Auto) {
Expand Down Expand Up @@ -500,35 +567,6 @@ impl GpuController {
}

pub async fn apply_config(&self, config: &config::Gpu) -> anyhow::Result<()> {
if config.fan_control_enabled {
if let Some(ref settings) = config.fan_control_settings {
match settings.mode {
lact_schema::FanControlMode::Static => {
self.set_static_fan_control(settings.static_speed).await?;
}
lact_schema::FanControlMode::Curve => {
if settings.curve.0.is_empty() {
return Err(anyhow!("Cannot use empty fan curve"));
}

let interval = Duration::from_millis(settings.interval_ms);
self.start_curve_fan_control(
settings.curve.clone(),
settings.temperature_key.clone(),
interval,
)
.await?;
}
}
} else {
return Err(anyhow!(
"Trying to enable fan control with no settings provided"
));
}
} else {
self.stop_fan_control(true).await?;
}

if let Some(cap) = config.power_cap {
let hw_mon = self.first_hw_mon()?;

Expand Down Expand Up @@ -629,7 +667,34 @@ impl GpuController {
.with_context(|| format!("Could not set {kind:?} power states"))?;
}

if !config.fan_control_enabled {
if config.fan_control_enabled {
if let Some(ref settings) = config.fan_control_settings {
match settings.mode {
lact_schema::FanControlMode::Static => {
self.set_static_fan_control(settings.static_speed).await?;
}
lact_schema::FanControlMode::Curve => {
if settings.curve.0.is_empty() {
return Err(anyhow!("Cannot use empty fan curve"));
}

let interval = Duration::from_millis(settings.interval_ms);
self.start_curve_fan_control(
settings.curve.clone(),
settings.temperature_key.clone(),
interval,
)
.await?;
}
}
} else {
return Err(anyhow!(
"Trying to enable fan control with no settings provided"
));
}
} else {
self.stop_fan_control(true).await?;

let pmfw = &config.pmfw_options;
if let Some(acoustic_limit) = pmfw.acoustic_limit {
self.handle
Expand Down
Loading

0 comments on commit 8a85b2b

Please sign in to comment.