Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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