feat: experimental NVIDIA power control via RM ioctl interface
Adds an experimental power-cap mode using the undocumented RM ioctl interface (based on panchovix's LACT PR #1205) to set power limits below the VBIOS minimum (down to 30 W). - hal/rm_power.py: RM ioctl power-cap read/write/reset + runtime probe - limits.py: power_cap_mode (nvml/ioctl) with support detection - config.py: persist power_cap_mode per GPU - profiles: record/apply power_cap_mode - server.py: POST /api/limits validates ioctl support (409 on failure) - cli.py: profile save falls back to persisted mode - client.py: power_cap_mode in Limits - frontend: toggle + warning with panchovix attribution (LACT #1205) - tests: test_rm_power.py (unit) + integration coverage - Makefile: add test_rm_power.py to make test Also includes automated linter reformatting (prettier, ruff, shellcheck, isort, markdownlint) that the linter would apply anyway.
This commit is contained in:
1 parent
a8462e696c
commit
e148c83622
21 files changed
+1641
-271
No files matched your search
@@ -1,12 +1,12 @@
|
||||
import { fmt } from '../../utils/units.js';
|
||||
import type { VFPoint } from '../../types.js';
|
||||
import { fmt } from "../../utils/units.js";
|
||||
import type { VFPoint } from "../../types.js";
|
||||
|
||||
interface Props {
|
||||
point: VFPoint;
|
||||
/** Pending delta in kHz (from store), if any */
|
||||
pendingDeltaKhz?: number;
|
||||
/** True if this point is being held up by monotonicity enforcement */
|
||||
isClamped?: boolean;
|
||||
point: VFPoint;
|
||||
/** Pending delta in kHz (from store), if any */
|
||||
pendingDeltaKhz?: number;
|
||||
/** True if this point is being held up by monotonicity enforcement */
|
||||
isClamped?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,49 +14,70 @@ interface Props {
|
||||
* SVG so it's never clipped by the SVG viewport.
|
||||
*/
|
||||
export function CurveTooltip({ point, pendingDeltaKhz, isClamped }: Props) {
|
||||
const hasPending = pendingDeltaKhz !== undefined;
|
||||
const pendingMhz = hasPending ? pendingDeltaKhz! / 1000 : 0;
|
||||
const deltaChange = hasPending ? pendingDeltaKhz! - point.delta_khz : 0;
|
||||
const pendingEffMhz = hasPending ? point.freq_mhz + deltaChange / 1000 : null;
|
||||
const hasPending = pendingDeltaKhz !== undefined;
|
||||
const pendingMhz = hasPending ? pendingDeltaKhz! / 1000 : 0;
|
||||
const deltaChange = hasPending ? pendingDeltaKhz! - point.delta_khz : 0;
|
||||
const pendingEffMhz = hasPending
|
||||
? point.freq_mhz + deltaChange / 1000
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute right-4 bottom-4 pointer-events-none z-50 w-[172px] bg-zinc-800 border border-zinc-700 rounded-md p-2 text-xs shadow-xl"
|
||||
>
|
||||
<div className="text-zinc-400 mb-1">Point {point.index}</div>
|
||||
<div className="text-zinc-200">
|
||||
<span className="text-zinc-400">Volt: </span>{fmt.mv(point.volt_mv, 1)}
|
||||
</div>
|
||||
<div className="text-zinc-200">
|
||||
<span className="text-zinc-400">Offset: </span>
|
||||
<span className={point.delta_khz > 0 ? 'text-emerald-400' : point.delta_khz < 0 ? 'text-red-400' : 'text-zinc-400'}>
|
||||
{point.delta_khz > 0 ? '+' : ''}{fmt.mhz(point.delta_mhz, 1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-emerald-300 font-semibold">
|
||||
<span className="text-zinc-400">Eff.: </span>{fmt.mhz(point.freq_mhz, 0)}
|
||||
{isClamped && <span className="text-amber-500 ml-1">⇡</span>}
|
||||
</div>
|
||||
{isClamped && (
|
||||
<div className="text-amber-500/80 text-[10px] mt-0.5">
|
||||
Clamped by lower-voltage point
|
||||
</div>
|
||||
)}
|
||||
{hasPending && (
|
||||
<>
|
||||
<div className="border-t border-zinc-700 mt-1.5 pt-1.5">
|
||||
return (
|
||||
<div className="absolute right-4 bottom-4 pointer-events-none z-50 w-[172px] bg-zinc-800 border border-zinc-700 rounded-md p-2 text-xs shadow-xl">
|
||||
<div className="text-zinc-400 mb-1">Point {point.index}</div>
|
||||
<div className="text-zinc-200">
|
||||
<span className="text-zinc-400">Pending: </span>
|
||||
<span className={pendingMhz > 0 ? 'text-cyan-400' : pendingMhz < 0 ? 'text-orange-400' : 'text-zinc-400'}>
|
||||
{pendingMhz > 0 ? '+' : ''}{pendingMhz.toFixed(1)} MHz
|
||||
</span>
|
||||
<span className="text-zinc-400">Volt: </span>
|
||||
{fmt.mv(point.volt_mv, 1)}
|
||||
</div>
|
||||
<div className="text-cyan-300 font-semibold">
|
||||
<span className="text-zinc-400">→ Eff.: </span>{fmt.mhz(pendingEffMhz, 0)}
|
||||
<div className="text-zinc-200">
|
||||
<span className="text-zinc-400">Offset: </span>
|
||||
<span
|
||||
className={
|
||||
point.delta_khz > 0
|
||||
? "text-emerald-400"
|
||||
: point.delta_khz < 0
|
||||
? "text-red-400"
|
||||
: "text-zinc-400"
|
||||
}
|
||||
>
|
||||
{point.delta_khz > 0 ? "+" : ""}
|
||||
{fmt.mhz(point.delta_mhz, 1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
<div className="text-emerald-300 font-semibold">
|
||||
<span className="text-zinc-400">Eff.: </span>
|
||||
{fmt.mhz(point.freq_mhz, 0)}
|
||||
{isClamped && <span className="text-amber-500 ml-1">⇡</span>}
|
||||
</div>
|
||||
{isClamped && (
|
||||
<div className="text-amber-500/80 text-[10px] mt-0.5">
|
||||
Clamped by lower-voltage point
|
||||
</div>
|
||||
)}
|
||||
{hasPending && (
|
||||
<>
|
||||
<div className="border-t border-zinc-700 mt-1.5 pt-1.5">
|
||||
<div className="text-zinc-200">
|
||||
<span className="text-zinc-400">Pending: </span>
|
||||
<span
|
||||
className={
|
||||
pendingMhz > 0
|
||||
? "text-cyan-400"
|
||||
: pendingMhz < 0
|
||||
? "text-orange-400"
|
||||
: "text-zinc-400"
|
||||
}
|
||||
>
|
||||
{pendingMhz > 0 ? "+" : ""}
|
||||
{pendingMhz.toFixed(1)} MHz
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-cyan-300 font-semibold">
|
||||
<span className="text-zinc-400">→ Eff.: </span>
|
||||
{fmt.mhz(pendingEffMhz, 0)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -72,6 +72,21 @@ export function PerformancePanel() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleModeChange(enabled: boolean) {
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.updateLimits(
|
||||
{ power_cap_mode: enabled ? "ioctl" : "nvml" },
|
||||
selectedGpuIndex,
|
||||
);
|
||||
await fetchLimits();
|
||||
} catch (e: unknown) {
|
||||
toast.error(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading && !limits) {
|
||||
return (
|
||||
<div className="bg-zinc-900 rounded-lg overflow-hidden flex flex-col animate-pulse">
|
||||
@@ -158,12 +173,80 @@ export function PerformancePanel() {
|
||||
)}
|
||||
|
||||
<div className="flex flex-col divide-y divide-zinc-800">
|
||||
{/* ── Experimental NVIDIA power control ─────────────────────────── */}
|
||||
{(limits.rm_power_supported || limits.power_cap_mode === "ioctl") && (
|
||||
<div className="px-4 py-3 flex flex-col gap-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={limits.power_cap_mode === "ioctl"}
|
||||
disabled={busy}
|
||||
onChange={(e) => handleModeChange(e.target.checked)}
|
||||
className="accent-red-500"
|
||||
/>
|
||||
<span className="text-xs text-zinc-300">
|
||||
Experimental NVIDIA power control
|
||||
</span>
|
||||
</label>
|
||||
{limits.rm_power_supported ? (
|
||||
<div
|
||||
role="alert"
|
||||
className={
|
||||
"px-2.5 py-1.5 rounded border text-xs leading-relaxed " +
|
||||
(limits.power_cap_mode === "ioctl"
|
||||
? "bg-red-950/80 border-red-500 text-red-300"
|
||||
: "bg-red-950/40 border-red-800 text-red-400")
|
||||
}
|
||||
>
|
||||
<span className="font-bold">⚠ WARNING:</span> uses an
|
||||
undocumented driver interface for ALL power limits, including
|
||||
resets. Allows values below the VBIOS minimum (down to 30 W)
|
||||
and may cause instability or stop working after driver
|
||||
updates. Enable at your own risk. Based on the work of{" "}
|
||||
<a
|
||||
href="https://github.com/ilya-zlobintsev/LACT/pull/1205"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-red-200"
|
||||
>
|
||||
panchovix
|
||||
</a>{" "}
|
||||
(LACT PR #1205).
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
role="alert"
|
||||
className="px-2.5 py-1.5 rounded border border-red-500 bg-red-950/80 text-red-300 text-xs leading-relaxed"
|
||||
>
|
||||
<span className="font-bold">
|
||||
⚠ Interface not currently available.
|
||||
</span>
|
||||
The driver no longer exposes the RM power interface (it may
|
||||
have been updated). Experimental mode is still enabled, so
|
||||
power-limit changes will fail. Uncheck to switch back to the
|
||||
standard NVML mode.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Board Power Limit ─────────────────────────────────────────── */}
|
||||
<div className="px-4 py-4 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-zinc-500 uppercase tracking-wider">
|
||||
Board Power Limit
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-zinc-500 uppercase tracking-wider">
|
||||
Board Power Limit
|
||||
</span>
|
||||
{limits.power_cap_mode === "ioctl" &&
|
||||
limits.min_power_limit_w_native != null && (
|
||||
<span
|
||||
className="text-xs text-red-400/80 font-mono"
|
||||
title="Native VBIOS minimum — experimental mode allows lower"
|
||||
>
|
||||
VBIOS min {limits.min_power_limit_w_native} W
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="number"
|
||||
|
||||
@@ -345,6 +345,11 @@ export function ProfilePanel({
|
||||
{badges && (
|
||||
<p className="text-xs text-zinc-500">{badges}</p>
|
||||
)}
|
||||
{p.power_cap_mode === "ioctl" && (
|
||||
<p className="text-xs text-red-400 font-medium">
|
||||
⚠ experimental power (below VBIOS min)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -98,7 +98,11 @@ export interface LimitsState {
|
||||
power_limit_w: number | null;
|
||||
default_power_limit_w: number | null;
|
||||
min_power_limit_w: number | null;
|
||||
min_power_limit_w_native: number | null;
|
||||
max_power_limit_w: number | null;
|
||||
// "nvml" (default) or "ioctl" (experimental RM power control)
|
||||
power_cap_mode: "nvml" | "ioctl";
|
||||
rm_power_supported: boolean;
|
||||
// Clock offsets — current values
|
||||
gpc_offset_mhz: number | null;
|
||||
mem_offset_mhz: number | null;
|
||||
@@ -136,6 +140,8 @@ export interface ProfileData {
|
||||
curve_deltas: Record<string, number>;
|
||||
mem_offset_mhz: number | null;
|
||||
power_limit_w: number | null;
|
||||
// "nvml" (default) or "ioctl" (experimental RM power control)
|
||||
power_cap_mode: "nvml" | "ioctl" | null;
|
||||
fan_curve: FanPoint[] | null;
|
||||
fan_targets: number[] | null;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { VFPoint } from '../types.js';
|
||||
import type { VFPoint } from "../types.js";
|
||||
|
||||
/**
|
||||
* Approximate reference frequency (MHz) for a point: effective − delta.
|
||||
@@ -14,35 +14,37 @@ import type { VFPoint } from '../types.js';
|
||||
* useful as a faint reference line ("where this point would sit with no boost").
|
||||
*/
|
||||
export function refBaseMhz(p: VFPoint): number {
|
||||
return p.freq_mhz - p.delta_mhz;
|
||||
return p.freq_mhz - p.delta_mhz;
|
||||
}
|
||||
|
||||
/** Find which VF point the GPU is currently near based on voltage reading */
|
||||
export function findCurrentPoint(
|
||||
points: VFPoint[],
|
||||
voltage_mv: number | null,
|
||||
points: VFPoint[],
|
||||
voltage_mv: number | null,
|
||||
): VFPoint | null {
|
||||
if (voltage_mv == null || points.length === 0) return null;
|
||||
return points.reduce((best, p) =>
|
||||
Math.abs(p.volt_mv - voltage_mv) < Math.abs(best.volt_mv - voltage_mv) ? p : best,
|
||||
);
|
||||
if (voltage_mv == null || points.length === 0) return null;
|
||||
return points.reduce((best, p) =>
|
||||
Math.abs(p.volt_mv - voltage_mv) < Math.abs(best.volt_mv - voltage_mv)
|
||||
? p
|
||||
: best,
|
||||
);
|
||||
}
|
||||
|
||||
/** Voltage domain extent, with padding */
|
||||
export function voltExtent(points: VFPoint[], padMv = 20): [number, number] {
|
||||
if (points.length === 0) return [600, 1100];
|
||||
const min = Math.min(...points.map((p) => p.volt_mv));
|
||||
const max = Math.max(...points.map((p) => p.volt_mv));
|
||||
return [min - padMv, max + padMv];
|
||||
if (points.length === 0) return [600, 1100];
|
||||
const min = Math.min(...points.map((p) => p.volt_mv));
|
||||
const max = Math.max(...points.map((p) => p.volt_mv));
|
||||
return [min - padMv, max + padMv];
|
||||
}
|
||||
|
||||
/** Frequency domain extent for the effective (boosted) curve, with padding */
|
||||
export function freqExtent(points: VFPoint[], padMhz = 50): [number, number] {
|
||||
if (points.length === 0) return [1000, 3000];
|
||||
const allFreqs = points.flatMap((p) => [p.freq_mhz, refBaseMhz(p)]);
|
||||
const min = Math.min(...allFreqs);
|
||||
const max = Math.max(...points.map((p) => p.freq_mhz));
|
||||
return [min - padMhz, max + padMhz];
|
||||
if (points.length === 0) return [1000, 3000];
|
||||
const allFreqs = points.flatMap((p) => [p.freq_mhz, refBaseMhz(p)]);
|
||||
const min = Math.min(...allFreqs);
|
||||
const max = Math.max(...points.map((p) => p.freq_mhz));
|
||||
return [min - padMhz, max + padMhz];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,19 +61,19 @@ export function freqExtent(points: VFPoint[], padMhz = 50): [number, number] {
|
||||
* point 104 has +315 MHz → effective also 3907 MHz (clamped).
|
||||
*/
|
||||
export function detectClampedPoints(points: VFPoint[]): Set<number> {
|
||||
const clamped = new Set<number>();
|
||||
let ceiling = -Infinity;
|
||||
let ceilingOffset = -Infinity;
|
||||
const clamped = new Set<number>();
|
||||
let ceiling = -Infinity;
|
||||
let ceilingOffset = -Infinity;
|
||||
|
||||
for (const p of points) {
|
||||
if (p.freq_mhz <= ceiling && p.delta_khz < ceilingOffset) {
|
||||
clamped.add(p.index);
|
||||
}
|
||||
if (p.freq_mhz > ceiling) {
|
||||
ceiling = p.freq_mhz;
|
||||
ceilingOffset = p.delta_khz;
|
||||
}
|
||||
for (const p of points) {
|
||||
if (p.freq_mhz <= ceiling && p.delta_khz < ceilingOffset) {
|
||||
clamped.add(p.index);
|
||||
}
|
||||
if (p.freq_mhz > ceiling) {
|
||||
ceiling = p.freq_mhz;
|
||||
ceilingOffset = p.delta_khz;
|
||||
}
|
||||
}
|
||||
|
||||
return clamped;
|
||||
return clamped;
|
||||
}
|
||||
Reference in new issue
Block a user