feat: support nvidia voltage boost (#1133)

* feat: implement voltage boost for nvidia

* make NvApiVoltageBoost private

* fix typo

* cleanups

* switch from tooltip to popover
This commit is contained in:
Roman Makarov
2026-08-02 23:00:10 +03:00
committed by GitHub
parent da9fa95517
commit 8342f83d34
11 changed files with 220 additions and 55 deletions
+2
View File
@@ -188,6 +188,8 @@ gpus:
max_voltage: 1200
# Voltage offset value in mV for RDNA and newer AMD GPUs.
voltage_offset: 0
# Nvidia core voltage boost, in percent (0-100) of available boost, not total voltage
voltage_boost: 0
# GPU V/F curve with voltage and frequency points per each power state.
# Applicable only to AMD GCN and RDNA1 GPUs. Overrides the values of min/max clock/voltage fields.
@@ -13,7 +13,7 @@ use amdgpu_sysfs::{
gpu_handle::{PowerLevelId, fan_control::FanInfo, power_profile_mode::PowerProfileModesTable},
hw_mon::Temperature,
};
use anyhow::{Context, anyhow, bail};
use anyhow::{Context, anyhow, bail, ensure};
use driver::DriverHandle;
use futures::{FutureExt, future::LocalBoxFuture};
use indexmap::IndexMap;
@@ -21,8 +21,8 @@ use lact_schema::{
ActivePowerStates, CacheInfo, ClocksInfo, ClocksTable, ClockspeedStats, DeviceApiInfo,
DeviceFlag, DeviceInfo, DeviceStats, DeviceType, DrmInfo, DrmMemoryInfo, FanControlMode,
FanStats, IntelDrmInfo, LinkInfo, NvidiaClockOffset, NvidiaClocksTable, NvidiaThermalInfo,
NvidiaVfPoint, PmfwInfo, PowerState, PowerStates, PowerStats, ProcessInfo, ProcessList,
ProcessType, ProcessUtilizationType, TemperatureEntry, VoltageStats, VramStats,
NvidiaVfPoint, NvidiaVoltageBoost, PmfwInfo, PowerState, PowerStates, PowerStats, ProcessInfo,
ProcessList, ProcessType, ProcessUtilizationType, TemperatureEntry, VoltageStats, VramStats,
config::{CurvePoint, FanControlSettings, FanCurve, GpuConfig},
};
use nvapi::NvApi;
@@ -68,6 +68,7 @@ pub struct NvidiaGpuController {
last_applied_vram_locked_clocks: RefCell<Option<(u32, u32)>>,
// Check if reset is needed to avoid unnecessarily going to nvapi
vf_curve_written: Cell<bool>,
voltage_boost_written: Cell<bool>,
// Used as the initial value on cards which do not report base VF points themselves (Turing)
base_vf_curve: RefCell<Option<Vec<NvidiaVfPoint>>>,
}
@@ -144,6 +145,7 @@ impl NvidiaGpuController {
last_applied_gpu_locked_clocks: RefCell::new(None),
last_applied_vram_locked_clocks: RefCell::new(None),
vf_curve_written: Cell::new(false),
voltage_boost_written: Cell::new(false),
base_vf_curve: RefCell::new(None),
})
}
@@ -1087,6 +1089,17 @@ impl GpuController for NvidiaGpuController {
.inspect_err(|err| warn!("could not get VF curve: {err:#}"))
.unwrap_or_default();
let voltage_boost = self.nvapi.as_ref().and_then(|(nvapi, handle)| {
unsafe { nvapi.get_voltage_boost(*handle) }
.inspect_err(|err| warn!("could not get voltage boost: {err:#}"))
.ok()
.map(|current| NvidiaVoltageBoost {
current: current.into(),
min: 0,
max: 100,
})
});
let table = NvidiaClocksTable {
gpu_offsets,
mem_offsets,
@@ -1095,6 +1108,7 @@ impl GpuController for NvidiaGpuController {
gpu_clock_range,
vram_clock_range,
gpu_vf_curve,
voltage_boost,
};
Ok(ClocksInfo {
@@ -1198,6 +1212,27 @@ impl GpuController for NvidiaGpuController {
.context("Could not apply VF curve")?;
}
if let Some(percent) = clocks.voltage_boost {
let (nvapi, handle) = self.nvapi.as_ref().context("NvAPI not available")?;
// The driver field is a single byte, out of range values would be truncated instead of rejected
let percent =
u8::try_from(percent.clamp(0, 100)).expect("Clamped value fits into u8");
debug!("applying voltage boost {percent}%");
unsafe { nvapi.set_voltage_boost(*handle, percent) }
.context("Could not apply voltage boost")?;
self.voltage_boost_written.set(true);
// verify the boost was applied
let applied = unsafe { nvapi.get_voltage_boost(*handle) }
.context("Could not verify voltage boost")?;
ensure!(
applied == percent,
"Voltage boost was not applied: requested {percent}%, driver reports {applied}%"
);
}
if config.fan_control_enabled {
let settings = config
.fan_control_settings
@@ -1317,6 +1352,13 @@ impl GpuController for NvidiaGpuController {
self.reset_vf_curve().context("Could not reset VF curve")?;
}
if self.voltage_boost_written.get() {
let (nvapi, handle) = self.nvapi.as_ref().context("NvAPI not available")?;
unsafe { nvapi.set_voltage_boost(*handle, 0) }
.context("Could not reset voltage boost")?;
self.voltage_boost_written.set(false);
}
Ok(())
}
@@ -29,6 +29,8 @@ const QUERY_NVAPI_GET_ERROR_MESSAGE: u32 = 0x6c2d048c;
// Undocumented calls
const QUERY_NVAPI_THERMALS: u32 = 0x65fe3aad;
const QUERY_NVAPI_VOLTAGE: u32 = 0x465f9bcf;
const QUERY_NVAPI_VOLTAGE_BOOST_GET: u32 = 0x9df23ca1;
const QUERY_NVAPI_VOLTAGE_BOOST_SET: u32 = 0xb9306d9b;
const QUERY_NVAPI_GPU_CLOCK_CLIENT_CLK_VF_POINTS_GET_STATUS: u32 = 0x21537ad4;
const QUERY_NVAPI_GPU_CLOCK_CLIENT_CLK_VF_POINTS_GET_INFO: u32 = 0x507b4b59;
const QUERY_NVAPI_GPU_CLOCK_CLIENT_CLK_VF_POINTS_SET_CONTROL: u32 = 0x733e009;
@@ -111,18 +113,34 @@ impl NvApi {
}
pub unsafe fn get_voltage(&self, handle: NvPhysicalGpuHandle) -> anyhow::Result<u32> {
let mut data = NvApiVoltage {
#[allow(clippy::cast_possible_truncation)]
version: make_version::<NvApiVoltage>(1),
flags: 0,
padding_1: [0; 8],
value_uv: 0,
padding_2: [0; 8],
};
let mut data = NvApiVoltage::default();
self.physical_gpu_query(handle, &mut data, QUERY_NVAPI_VOLTAGE)?;
Ok(data.value_uv)
Ok(data.rails[0].current_voltage_uv)
}
pub unsafe fn get_voltage_boost(&self, handle: NvPhysicalGpuHandle) -> anyhow::Result<u8> {
let mut data = NvApiVoltageBoost::default();
self.physical_gpu_query(handle, &mut data, QUERY_NVAPI_VOLTAGE_BOOST_GET)?;
Ok(data.percent)
}
pub unsafe fn set_voltage_boost(
&self,
handle: NvPhysicalGpuHandle,
percent: u8,
) -> anyhow::Result<()> {
let mut data = NvApiVoltageBoost {
percent,
..Default::default()
};
self.physical_gpu_query(handle, &mut data, QUERY_NVAPI_VOLTAGE_BOOST_SET)?;
Ok(())
}
pub unsafe fn clock_client_clk_vf_points_get_info(
@@ -488,14 +506,52 @@ impl NvApiThermals {
}
}
// ensure the sizes used in the version fields stay correct
const _: () = assert!(mem::size_of::<NvApiVoltage>() == 76);
const _: () = assert!(mem::size_of::<NvApiVoltageBoost>() == 40);
#[repr(C)]
#[derive(Debug)]
struct NvApiVoltage {
version: u32,
flags: u32,
padding_1: [u32; 8],
value_uv: u32,
padding_2: [u32; 8],
version: NvU32,
rsvd: [NvU8; 32],
rails: [NvApiVoltageRail; 1],
}
impl Default for NvApiVoltage {
fn default() -> Self {
Self {
version: make_version::<Self>(1),
rsvd: [0; 32],
rails: [NvApiVoltageRail::default()],
}
}
}
#[repr(C)]
#[derive(Debug, Default)]
struct NvApiVoltageRail {
rail_id: NvU32,
current_voltage_uv: NvU32,
rsvd: [NvU8; 32],
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct NvApiVoltageBoost {
version: NvU32,
percent: NvU8,
rsvd: [NvU8; 32],
}
impl Default for NvApiVoltageBoost {
fn default() -> Self {
Self {
version: make_version::<Self>(1),
percent: 0,
rsvd: [0; 32],
}
}
}
#[repr(C)]
@@ -61,6 +61,7 @@ gpus:
3:
clockspeed: 920
voltage_offset: 0
voltage_boost: 0
power_profile_mode_index: 0
custom_power_profile_mode_hueristics:
- - 0
+2
View File
@@ -196,6 +196,8 @@ min-gpu-clock = Minimum GPU Clock (MHz)
min-vram-clock = Minimum VRAM Clock (MHz)
min-gpu-voltage = Minimum GPU Voltage (mV)
gpu-voltage-offset = GPU voltage offset (mV)
gpu-voltage-boost = GPU Voltage Boost (%)
gpu-voltage-boost-tooltip = Controls how much of the additional voltage headroom defined by the driver is available. 100% means all of this headroom, not 100% of total GPU voltage. More headroom may sustain higher clockspeeds but increases power draw and heat.
gpu-pstate-clock-offset = GPU P-State {$pstate} Clock Offset (MHz)
vram-pstate-clock-offset = VRAM P-State {$pstate} Clock Offset (MHz)
gpu-pstate-clock = GPU P-State {$pstate} Clock (MHz)
@@ -22,7 +22,8 @@ impl ClockCategory {
| ClockspeedType::GpuClockOffset(_) => ClockCategory::CoreClock,
ClockspeedType::MinVoltage
| ClockspeedType::MaxVoltage
| ClockspeedType::VoltageOffset => ClockCategory::CoreVoltage,
| ClockspeedType::VoltageOffset
| ClockspeedType::VoltageBoost => ClockCategory::CoreVoltage,
ClockspeedType::MaxMemoryClock
| ClockspeedType::MinMemoryClock
| ClockspeedType::MemClockOffset(_) => ClockCategory::VramClock,
@@ -7,11 +7,11 @@ use crate::{
};
use gtk::{
glib::{SignalHandlerId, object::ObjectExt},
prelude::{AdjustmentExt, EditableExt, OrientableExt, RangeExt, ScaleExt, WidgetExt},
prelude::{AdjustmentExt, BoxExt, EditableExt, OrientableExt, RangeExt, ScaleExt, WidgetExt},
};
use i18n_embed_fl::fl;
use lact_schema::request::ClockspeedType;
use relm4::prelude::FactoryComponent;
use relm4::{RelmWidgetExt, prelude::FactoryComponent};
pub struct ClockAdjustmentRow {
clock_type: ClockspeedType,
@@ -28,6 +28,20 @@ pub struct ClocksData {
pub max: i32,
pub custom_title: Option<String>,
pub is_secondary: bool,
pub step: i32,
}
impl Default for ClocksData {
fn default() -> Self {
Self {
current: 0,
min: 0,
max: 0,
custom_title: None,
is_secondary: false,
step: 10,
}
}
}
impl ClocksData {
@@ -36,8 +50,7 @@ impl ClocksData {
current,
min,
max,
is_secondary: false,
custom_title: None,
..Default::default()
}
}
}
@@ -69,31 +82,55 @@ impl FactoryComponent for ClockAdjustmentRow {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
#[name = "title_label"]
gtk::Label {
set_xalign: 0.0,
#[watch]
set_markup: &match &self.custom_title {
Some(title) => title.clone(),
None => {
match self.clock_type {
ClockspeedType::MaxCoreClock => fl!(I18N, "max-gpu-clock"),
ClockspeedType::MaxMemoryClock => fl!(I18N, "max-vram-clock"),
ClockspeedType::MaxVoltage => fl!(I18N, "max-gpu-voltage"),
ClockspeedType::MinCoreClock => fl!(I18N, "min-gpu-clock"),
ClockspeedType::MinMemoryClock => fl!(I18N, "min-vram-clock"),
ClockspeedType::MinVoltage => fl!(I18N, "min-gpu-voltage"),
ClockspeedType::VoltageOffset => fl!(I18N, "gpu-voltage-offset"),
ClockspeedType::GpuClockOffset(pstate) => fl!(I18N, "gpu-pstate-clock-offset", pstate = pstate),
ClockspeedType::MemClockOffset(pstate) => fl!(I18N, "vram-pstate-clock-offset", pstate = pstate),
ClockspeedType::GpuVfCurveClock(pstate) => fl!(I18N, "gpu-pstate-clock", pstate = pstate),
ClockspeedType::MemVfCurveClock(pstate) => fl!(I18N, "mem-pstate-clock", pstate = pstate),
ClockspeedType::GpuVfCurveVoltage(pstate) => fl!(I18N, "gpu-pstate-clock-voltage", pstate = pstate),
ClockspeedType::MemVfCurveVoltage(pstate) => fl!(I18N, "mem-pstate-clock-voltage", pstate = pstate),
ClockspeedType::Reset => unreachable!(),
#[name = "title_box"]
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
gtk::Label {
set_xalign: 0.0,
#[watch]
set_markup: &match &self.custom_title {
Some(title) => title.clone(),
None => {
match self.clock_type {
ClockspeedType::MaxCoreClock => fl!(I18N, "max-gpu-clock"),
ClockspeedType::MaxMemoryClock => fl!(I18N, "max-vram-clock"),
ClockspeedType::MaxVoltage => fl!(I18N, "max-gpu-voltage"),
ClockspeedType::MinCoreClock => fl!(I18N, "min-gpu-clock"),
ClockspeedType::MinMemoryClock => fl!(I18N, "min-vram-clock"),
ClockspeedType::MinVoltage => fl!(I18N, "min-gpu-voltage"),
ClockspeedType::VoltageOffset => fl!(I18N, "gpu-voltage-offset"),
ClockspeedType::VoltageBoost => fl!(I18N, "gpu-voltage-boost"),
ClockspeedType::GpuClockOffset(pstate) => fl!(I18N, "gpu-pstate-clock-offset", pstate = pstate),
ClockspeedType::MemClockOffset(pstate) => fl!(I18N, "vram-pstate-clock-offset", pstate = pstate),
ClockspeedType::GpuVfCurveClock(pstate) => fl!(I18N, "gpu-pstate-clock", pstate = pstate),
ClockspeedType::MemVfCurveClock(pstate) => fl!(I18N, "mem-pstate-clock", pstate = pstate),
ClockspeedType::GpuVfCurveVoltage(pstate) => fl!(I18N, "gpu-pstate-clock-voltage", pstate = pstate),
ClockspeedType::MemVfCurveVoltage(pstate) => fl!(I18N, "mem-pstate-clock-voltage", pstate = pstate),
ClockspeedType::Reset => unreachable!(),
}
}
}
}
},
},
gtk::MenuButton {
set_icon_name: "dialog-information-symbolic",
set_always_show_arrow: false,
add_css_class: "flat",
set_visible: self.clock_type == ClockspeedType::VoltageBoost,
#[wrap(Some)]
set_popover = &gtk::Popover {
gtk::Label {
set_margin_all: 5,
set_label: &fl!(I18N, "gpu-voltage-boost-tooltip"),
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
set_max_width_chars: 55,
}
},
},
},
gtk::Scale {
@@ -128,7 +165,7 @@ impl FactoryComponent for ClockAdjustmentRow {
data.current as f64,
data.min as f64,
data.max as f64,
10.0,
data.step as f64,
10.0,
);
@@ -178,7 +215,7 @@ impl FactoryComponent for ClockAdjustmentRow {
label_group,
input_group,
} => {
label_group.add_widget(&widgets.title_label);
label_group.add_widget(&widgets.title_box);
input_group.add_widget(&widgets.input_button);
}
ClockAdjustmentRowMsg::SetVisible(visible) => {
@@ -420,7 +420,7 @@ impl ClocksFrame {
min: sclk_offset_min,
max: sclk_offset_max,
custom_title: Some(fl!(I18N, "gpu-clock-offset")),
is_secondary: false,
..Default::default()
},
);
}
@@ -457,8 +457,7 @@ impl ClocksFrame {
current: level.clockspeed,
min: min_sclk,
max: max_sclk,
is_secondary: false,
custom_title: None,
..Default::default()
},
);
}
@@ -477,8 +476,7 @@ impl ClocksFrame {
current: level.voltage,
min: min_vddc,
max: max_vddc,
is_secondary: false,
custom_title: None,
..Default::default()
},
);
}
@@ -540,7 +538,7 @@ impl ClocksFrame {
min,
max,
is_secondary,
custom_title: None,
..Default::default()
},
);
}
@@ -600,6 +598,19 @@ impl ClocksFrame {
nvidia_clock_offset_to_data(offset, *pstate > 0),
);
}
if let Some(voltage_boost) = table.voltage_boost {
self.set_clock(
ClockspeedType::VoltageBoost,
ClocksData {
current: voltage_boost.current,
min: voltage_boost.min,
max: voltage_boost.max,
step: 1,
..Default::default()
},
);
}
}
fn set_intel_table(&mut self, table: &IntelClocksTable) {
@@ -664,7 +675,7 @@ fn nvidia_clock_offset_to_data(clock_info: &NvidiaClockOffset, is_secondary: boo
current: clock_info.current,
min: clock_info.min,
max: clock_info.max,
custom_title: None,
is_secondary,
..Default::default()
}
}
+2
View File
@@ -89,6 +89,7 @@ pub struct ClocksConfiguration {
)]
pub mem_vf_curve: IndexMap<u8, CurvePoint>,
pub voltage_offset: Option<i32>,
pub voltage_boost: Option<i32>,
}
impl ClocksConfiguration {
@@ -102,6 +103,7 @@ impl ClocksConfiguration {
ClockspeedType::MinMemoryClock => self.min_memory_clock = value,
ClockspeedType::MinVoltage => self.min_voltage = value,
ClockspeedType::VoltageOffset => self.voltage_offset = value,
ClockspeedType::VoltageBoost => self.voltage_boost = value,
ClockspeedType::GpuClockOffset(pstate) => match value {
Some(value) => {
self.gpu_clock_offsets.insert(pstate, value);
+10
View File
@@ -478,6 +478,16 @@ pub struct NvidiaClocksTable {
pub vram_clock_range: Option<(u32, u32)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub gpu_vf_curve: Vec<NvidiaVfPoint>,
#[serde(default)]
pub voltage_boost: Option<NvidiaVoltageBoost>,
}
/// Nvidia core voltage boost, in percent
#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy)]
pub struct NvidiaVoltageBoost {
pub current: i32,
pub min: i32,
pub max: i32,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy)]
+1
View File
@@ -163,6 +163,7 @@ pub enum ClockspeedType {
MinVoltage,
MaxVoltage,
VoltageOffset,
VoltageBoost,
MaxMemoryClock,
MinMemoryClock,