Skip to content

Commit cb88543

Browse files
vupleaclaude
andcommitted
Persist the logon re-apply cap-relative, immune to read temperature
The driver's reported V/F curve shifts with core temperature, so a logon task that replays an absolute clock target re-applies a tuning that drifts from the validated one by the thermal shift between the original read and the boot-time read. The task now persists the clock as the offset written at the cap anchor (--mhz-offset with --peak-mv at the cap itself) and re-resolves it against the boot-time curve, reproducing the validated frequency offset at any boot temperature. The cap voltage and memory clock stay absolute; their references (anchor voltages, the factory base memory clock) are static. READMEs: advise applying at a consistent (cooled-down) temperature, since the original invocation still resolves against the live curve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUS4E5wBeXbVJ4BWSKYvcb
1 parent 30f9d71 commit cb88543

10 files changed

Lines changed: 127 additions & 45 deletions

‎PROFILES.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ tier 3 — harmless, just no extra gain.
7878
−1% to −10% performance cost that grows with the tier and is steepest on RTX 40/50 (their clock
7979
follows voltage more tightly than older generations).
8080

81+
Apply a profile with the card cooled down (idle desktop, not right after a gaming session). The
82+
tuning resolves against the live curve, which shifts slightly with temperature, so applying hot
83+
bakes in a slightly different tuning than applying cold.
84+
8185
## Where the values come from
8286

8387
Instrumented reviews (TechPowerUp, Tom's Hardware, Igor's Lab, TechSpot, thefpsreview, QuasarZone)

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ Other:
8585
The offset/pct forms are relative to the real under-load operating voltage — read it from `watch` under a
8686
sustained load.
8787

88+
Apply your settings at a consistent GPU temperature — simplest with a cooled-down card. Clock targets
89+
resolve against the live curve, which shifts slightly with temperature, so applying hot vs. cold bakes
90+
in a slightly different tuning.
91+
8892
### Persisting at startup
8993

9094
A tuning run **persists by default**: it copies the app to Program Files, and registers a Task Scheduler

‎src/GpuTuning.cs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,12 @@ public static IReadOnlyList<string> DescribePlan(CurvePlan plan, int? targetMhz)
228228
internal readonly record struct CurveChange(int Mv, int OldMhz, int NewMhz, int NewDeltaKhz);
229229

230230
/// <summary>The computed curve write: the per-point frequency deltas (kHz, index-aligned with the
231-
/// curve), the cap anchor and its flat frequency, and a description of every point that moves.</summary>
232-
internal sealed record CurvePlan(int CapMv, int CapMhz, IReadOnlyList<CurveChange> Changes,
233-
int[] DeltasKhz);
231+
/// curve), the cap anchor, its flat frequency and the frequency offset written there
232+
/// (<see cref="CapDeltaMhz"/>, 0 when the cap sits at the unwritable anchor 0 — see
233+
/// <see cref="TuneRequest.ToPersistedArgs"/> for why the offset is what persists), and a
234+
/// description of every point that moves.</summary>
235+
internal sealed record CurvePlan(int CapMv, int CapMhz, int CapDeltaMhz,
236+
IReadOnlyList<CurveChange> Changes, int[] DeltasKhz);
234237

235238
/// <summary>
236239
/// Builds the curve write that caps voltage at <paramref name="capMv"/>, with every delta measured
@@ -314,7 +317,7 @@ internal static CurvePlan BuildCurvePlan(IReadOnlyList<(int Mv, int Mhz)> stock,
314317
}
315318
}
316319

317-
return new CurvePlan(stock[k].Mv, newMhz[k], changes, deltasKhz);
320+
return new CurvePlan(stock[k].Mv, newMhz[k], deltasKhz[k] / 1000, changes, deltasKhz);
318321
}
319322

320323
private static int NearestAnchorIndex(IReadOnlyList<(int Mv, int Mhz)> curve, int mv)

‎src/Persistence.cs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ internal static class Persistence
1717
private const int MaxTaskRunLength = 261;
1818

1919
/// <summary>Registers the logon task that re-applies the undervolt, returning the one-line log
20-
/// message. <paramref name="absoluteArgs"/> is the fully-resolved undervolt command line (see
21-
/// <see cref="TuneRequest.ToAbsoluteArgs"/>). The caller installs the app first (see
20+
/// message. <paramref name="persistedArgs"/> is the resolved undervolt command line (see
21+
/// <see cref="TuneRequest.ToPersistedArgs"/>). The caller installs the app first (see
2222
/// <see cref="InstallApp"/>); the task targets that copy.</summary>
23-
public static string RegisterLogonTask(string[] absoluteArgs)
23+
public static string RegisterLogonTask(string[] persistedArgs)
2424
{
2525
// Run at logon (not at boot): the task then lives in the user's interactive session, so it can
2626
// reach the GPU and its failure box is actually on screen. /RL HIGHEST runs it elevated,
2727
// which the driver writes need.
28-
string taskRun = BuildTaskRun(InstalledNoCmdExePath(), absoluteArgs);
28+
string taskRun = BuildTaskRun(InstalledNoCmdExePath(), persistedArgs);
2929
RunSchtasks("/Create", "/F", "/TN", TaskName, "/SC", "ONLOGON", "/RL", "HIGHEST", "/TR", taskRun);
3030
return $"Registered logon task '{TaskName}'.";
3131
}
@@ -225,9 +225,9 @@ internal static bool IsAppSidecar(string fileName, string exeName)
225225
/// <summary>The logon task's full command line: the quoted installed exe, the resolved undervolt and
226226
/// the <see cref="StartupFixedArgs"/>. Validated against <see cref="MaxTaskRunLength"/> here, since
227227
/// schtasks' own error doesn't name the limit.</summary>
228-
internal static string BuildTaskRun(string targetExe, string[] absoluteArgs)
228+
internal static string BuildTaskRun(string targetExe, string[] persistedArgs)
229229
{
230-
string taskRun = CommandLine.Join(absoluteArgs.Concat(StartupFixedArgs).Prepend(targetExe));
230+
string taskRun = CommandLine.Join(persistedArgs.Concat(StartupFixedArgs).Prepend(targetExe));
231231
if (taskRun.Length > MaxTaskRunLength)
232232
{
233233
throw new CliError($"The logon task's command line is {taskRun.Length} characters; "

‎src/Program.cs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -395,9 +395,9 @@ private static void RunTune(IntPtr gpu, TuneRequest request, string? launchingLn
395395
Console.WriteLine(targetMhz is { } f ? $" Frequency: {f} MHz" : " Frequency: stock clock");
396396
}
397397

398-
// The absolute memory clock and the delta to write; the absolute value is captured for the
399-
// logon task so it re-applies this exact undervolt without re-resolving against the base
400-
// clock (see ToAbsoluteArgs).
398+
// The absolute memory clock and the delta to write. The absolute clock is also what the logon
399+
// task re-applies: its reference (the factory base clock) is static, so unlike the curve
400+
// clock it can't drift between runs (see ToPersistedArgs).
401401
(int TargetMhz, int DeltaKhz)? memory = null;
402402
if (request.Mem.IsSet)
403403
{
@@ -470,8 +470,8 @@ private static void RunTune(IntPtr gpu, TuneRequest request, string? launchingLn
470470
Console.WriteLine($"Installed to {Persistence.InstallDir()}.");
471471
}
472472

473-
Console.WriteLine(Persistence.RegisterLogonTask(
474-
request.ToAbsoluteArgs(plan?.CapMv, targetMhz, memory?.TargetMhz)));
473+
Console.WriteLine(Persistence.RegisterLogonTask(request.ToPersistedArgs(
474+
plan?.CapMv, targetMhz is null ? null : plan?.CapDeltaMhz, memory?.TargetMhz)));
475475
}
476476
catch (Exception ex)
477477
{

‎src/TuneRequest.cs‎

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -238,25 +238,33 @@ public static TuneRequest Parse(string[] args)
238238
return (targetMv, targetMhz);
239239
}
240240

241-
/// <summary>The fully-resolved command line to persist for the logon re-apply: the absolute values
242-
/// actually applied, so the task reproduces this exact tuning without re-reading the (idle) curve
243-
/// or depending on a peak reference. Every offset/percentage and peak flag collapses into a plain
244-
/// <c>--mv</c>/<c>--mhz</c>/<c>--mem</c> here (<paramref name="capMv"/> is null on a memory-only
245-
/// run); <c>--cap-points</c> rides along only when non-default. No command word: a leading option
246-
/// implies <c>tune</c>, so the generated line is exactly what a user would type.</summary>
247-
public string[] ToAbsoluteArgs(int? capMv, int? targetMhz, int? memMhz)
241+
/// <summary>The resolved command line to persist for the logon re-apply. The cap voltage and
242+
/// memory clock persist as the absolute values actually applied — both reference static
243+
/// quantities (the curve's anchor voltages, the factory base memory clock). The clock persists
244+
/// <em>cap-relative</em>: <c>--mhz-offset</c> with <c>--peak-mv</c> set to the cap anchor
245+
/// itself. The driver's reported curve shifts with temperature, so re-resolving that offset
246+
/// against the logon-time read re-applies the exact frequency offset that was validated; a
247+
/// baked absolute clock would drift from it by the thermal shift between the original read and
248+
/// the boot-time read. Every user-typed offset/percentage and peak flag collapses into this
249+
/// canonical form (<paramref name="capMv"/> is null on a memory-only run,
250+
/// <paramref name="capDeltaMhz"/> also when no clock was requested); <c>--cap-points</c> rides
251+
/// along only when non-default. No command word: a leading option implies <c>tune</c>, so the
252+
/// generated line is exactly what a user would type.</summary>
253+
public string[] ToPersistedArgs(int? capMv, int? capDeltaMhz, int? memMhz)
248254
{
249255
var args = new List<string>();
250256
if (capMv is { } mv)
251257
{
252258
args.Add("--mv");
253259
args.Add(Invariant(mv));
254-
}
255260

256-
if (targetMhz is { } f)
257-
{
258-
args.Add("--mhz");
259-
args.Add(Invariant(f));
261+
if (capDeltaMhz is { } d)
262+
{
263+
args.Add("--mhz-offset");
264+
args.Add(Invariant(d));
265+
args.Add("--peak-mv");
266+
args.Add(Invariant(mv));
267+
}
260268
}
261269

262270
if (memMhz is { } m)

‎tests/CurvePlanTests.cs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public void PlainCap_FlattensCapAnchorAndAbove_ToStockClockThere()
1212

1313
Assert.Equal(1000, plan.CapMv);
1414
Assert.Equal(2500, plan.CapMhz); // the stock clock at the cap
15+
Assert.Equal(0, plan.CapDeltaMhz); // no offset written at the cap
1516

1617
// A plain cap has no offset (delta 0 at the cap), so the band changes nothing below it.
1718
for (int i = 0; i < stock.Count; i++)
@@ -29,6 +30,7 @@ public void ExplicitClock_RaisesTheCapBandAndFlattensAbove()
2930

3031
Assert.Equal(1000, plan.CapMv);
3132
Assert.Equal(2600, plan.CapMhz);
33+
Assert.Equal(100, plan.CapDeltaMhz); // the offset written at the cap anchor
3234

3335
// d = 2600 - 2500 = 100. The band is the cap (10) plus the 7 below it (3..9): all carry +100.
3436
// Above the cap the curve is flattened down to 2600; below the band it is untouched.
@@ -157,6 +159,7 @@ public void CapAtTheLowestAnchor_ReportsTheFlooredClock_NotTheUnwritableTarget()
157159

158160
Assert.Equal(800, plan.CapMv);
159161
Assert.Equal(2000, plan.CapMhz);
162+
Assert.Equal(0, plan.CapDeltaMhz); // nothing was written at the unwritable anchor 0
160163
Assert.Equal(0, plan.DeltasKhz[0]);
161164
}
162165

‎tests/CurveWriteVerificationTests.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ public void PureOverclock_WithNoReductions_IsUnverifiable()
111111
{
112112
// No anchor is flattened down, so there is no reliable signal (a raise the driver may clamp).
113113
var changes = new[] { new GpuTuning.CurveChange(1000, 2500, 2600, 100_000) };
114-
var plan = new GpuTuning.CurvePlan(CapMv: 1000, CapMhz: 2600, changes, DeltasKhz: Array.Empty<int>());
114+
var plan = new GpuTuning.CurvePlan(CapMv: 1000, CapMhz: 2600, CapDeltaMhz: 100, changes,
115+
DeltasKhz: Array.Empty<int>());
115116

116117
Assert.Equal(GpuTuning.WriteVerification.Unverifiable,
117118
GpuTuning.VerifyWriteReachedCurve(plan, TestCurves.Realistic()));

‎tests/PersistenceTests.cs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace SimpleNvidiaUndervolt.Tests;
22

33
/// <summary>Tests for the logon-task registration: the command line it runs — the resolved undervolt
4-
/// (from <see cref="TuneRequest.ToAbsoluteArgs"/>) plus the fixed flags an unattended,
4+
/// (from <see cref="TuneRequest.ToPersistedArgs"/>) plus the fixed flags an unattended,
55
/// already-elevated re-apply needs — and how it is built and read back.</summary>
66
public class PersistenceTests
77
{
@@ -16,13 +16,14 @@ public void ParseTaskArguments_ReadsTheExecActionsArguments()
1616
<Actions Context="Author">
1717
<Exec>
1818
<Command>"C:\Program Files\simple-nvidia-undervolt\simple-nvidia-undervolt.exe"</Command>
19-
<Arguments>--mv 960 --mhz 2880 --no-persist</Arguments>
19+
<Arguments>--mv 960 --mhz-offset 190 --peak-mv 960 --no-persist --silent</Arguments>
2020
</Exec>
2121
</Actions>
2222
</Task>
2323
""";
2424

25-
Assert.Equal("--mv 960 --mhz 2880 --no-persist", Persistence.ParseTaskArguments(xml));
25+
Assert.Equal("--mv 960 --mhz-offset 190 --peak-mv 960 --no-persist --silent",
26+
Persistence.ParseTaskArguments(xml));
2627
}
2728

2829
[Fact]

‎tests/TuneRequestTests.cs‎

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -379,44 +379,102 @@ public void ImplausibleMemory_Throws(string mem)
379379
() => Parse("tune", "--mv", "960", "--mem", mem).ResolveMemory(baseMemMhz: 14001));
380380
}
381381

382-
// --- ToAbsoluteArgs (what the logon task re-applies) ---
382+
// --- ToPersistedArgs (what the logon task re-applies) ---
383383

384384
[Fact]
385-
public void ToAbsoluteArgs_BakesTheResolvedCapAndClock()
385+
public void ToPersistedArgs_EmitsTheClockCapRelative()
386386
{
387-
// The relative form and peak reference collapse into the plain absolute values that were applied,
388-
// so the persisted run doesn't re-read the curve or need the peak.
387+
// The clock persists as an offset from the cap anchor's own stock clock (--peak-mv = the cap
388+
// voltage), so the logon run re-resolves it against the boot-time curve read: the reported
389+
// curve shifts with temperature, and the offset form re-applies the validated frequency
390+
// offset at any temperature where a baked absolute clock would drift.
389391
var args = Parse("tune", "--mv-offset", "-100", "--peak-mv", "1060")
390-
.ToAbsoluteArgs(capMv: 960, targetMhz: 2880, memMhz: null);
391-
Assert.Equal(new[] { "--mv", "960", "--mhz", "2880" }, args);
392+
.ToPersistedArgs(capMv: 960, capDeltaMhz: 190, memMhz: null);
393+
Assert.Equal(new[] { "--mv", "960", "--mhz-offset", "190", "--peak-mv", "960" }, args);
392394
}
393395

394396
[Fact]
395-
public void ToAbsoluteArgs_OmitsClockWhenNoneWasSet()
397+
public void ToPersistedArgs_OmitsClockWhenNoneWasSet()
396398
{
397-
var args = Parse("tune", "--mv", "960").ToAbsoluteArgs(capMv: 960, targetMhz: null, memMhz: null);
399+
var args = Parse("tune", "--mv", "960").ToPersistedArgs(capMv: 960, capDeltaMhz: null, memMhz: null);
398400
Assert.Equal(new[] { "--mv", "960" }, args);
399401
}
400402

401403
[Fact]
402-
public void ToAbsoluteArgs_IncludesMemoryAndNonDefaultCapPoints()
404+
public void ToPersistedArgs_IncludesMemoryAndNonDefaultCapPoints()
403405
{
404406
var args = Parse("tune", "--mv", "960", "--mem-offset", "1000", "--cap-points", "4")
405-
.ToAbsoluteArgs(capMv: 960, targetMhz: null, memMhz: 15001);
407+
.ToPersistedArgs(capMv: 960, capDeltaMhz: null, memMhz: 15001);
406408
Assert.Equal(new[] { "--mv", "960", "--mem", "15001", "--cap-points", "4" }, args);
407409
}
408410

409411
[Fact]
410-
public void ToAbsoluteArgs_OmitsDefaultCapPoints()
412+
public void ToPersistedArgs_OmitsDefaultCapPoints()
411413
{
412-
var args = Parse("tune", "--mv", "960").ToAbsoluteArgs(capMv: 960, targetMhz: null, memMhz: null);
414+
var args = Parse("tune", "--mv", "960").ToPersistedArgs(capMv: 960, capDeltaMhz: null, memMhz: null);
413415
Assert.DoesNotContain("--cap-points", args);
414416
}
415417

416418
[Fact]
417-
public void ToAbsoluteArgs_OmitsTheCapOnAMemoryOnlyRun()
419+
public void ToPersistedArgs_OmitsTheCapOnAMemoryOnlyRun()
418420
{
419-
var args = Parse("tune", "--mem-pct", "5").ToAbsoluteArgs(capMv: null, targetMhz: null, memMhz: 14700);
421+
var args = Parse("tune", "--mem-pct", "5").ToPersistedArgs(capMv: null, capDeltaMhz: null, memMhz: 14700);
420422
Assert.Equal(new[] { "--mem", "14700" }, args);
421423
}
424+
425+
// --- The persisted line's round trip: the logon run plans from a curve read at a different
426+
// temperature than the original run, and must re-apply the same tuning ---
427+
428+
/// <summary>Resolves and plans <paramref name="request"/> against <paramref name="stock"/>,
429+
/// exactly as a tune run does.</summary>
430+
private static GpuTuning.CurvePlan PlanOn(TuneRequest request, List<(int Mv, int Mhz)> stock)
431+
{
432+
var (capMv, targetMhz) = request.Resolve(stock);
433+
return GpuTuning.BuildCurvePlan(stock, capMv, targetMhz, request.CapPoints);
434+
}
435+
436+
/// <summary>The re-apply a persisted run performs: parse the persisted line (with the implied
437+
/// command word) and plan it against the boot-time curve.</summary>
438+
private static GpuTuning.CurvePlan ReapplyOn(string[] persisted, List<(int Mv, int Mhz)> curve)
439+
=> PlanOn(Parse(persisted.Prepend("tune").ToArray()), curve);
440+
441+
[Fact]
442+
public void PersistedArgs_ReapplyTheSameDeltas_OnATemperatureShiftedCurve()
443+
{
444+
// GPU Boost shifts the whole reported stock curve with core temperature, and the logon
445+
// re-apply reads at boot temperature, not at the original run's. The cap-relative persisted
446+
// form resolves every curve-dependent term from the boot-time read alone, so a uniform shift
447+
// cancels out of all of it - the cap delta, the flatten above and the band - and the written
448+
// vector is identical.
449+
var stock = TestCurves.Realistic(); // anchor 10 = (1000 mV, 2500 MHz)
450+
var request = Parse("tune", "--mv", "1000", "--mhz", "2600");
451+
var plan = PlanOn(request, stock);
452+
string[] persisted = request.ToPersistedArgs(plan.CapMv, plan.CapDeltaMhz, memMhz: null);
453+
454+
var colder = stock.Select(p => (p.Mv, p.Mhz + 45)).ToList();
455+
var replan = ReapplyOn(persisted, colder);
456+
457+
Assert.Equal(plan.DeltasKhz, replan.DeltasKhz);
458+
}
459+
460+
[Fact]
461+
public void PersistedArgs_ReapplyTheExactCapOffset_EvenWhenTheShiftIsNotUniform()
462+
{
463+
// If the thermal shift bends across the curve instead of moving it as one block, the deltas
464+
// in the flatten/band region can wobble - but the cap anchor's offset is resolved against
465+
// that same anchor's own read, so it reproduces exactly no matter the shift's shape. The cap
466+
// point is where the boost pins under load, so this is the part that must hold.
467+
var stock = TestCurves.Realistic(); // anchor 10 = (1000 mV, 2500 MHz)
468+
var request = Parse("tune", "--mv", "1000", "--mhz", "2600");
469+
var plan = PlanOn(request, stock);
470+
string[] persisted = request.ToPersistedArgs(plan.CapMv, plan.CapDeltaMhz, memMhz: null);
471+
472+
// A colder read where the shift tapers toward the top of the curve (+45 at the bottom anchor
473+
// down to +26 at the top) - the compensation compressing as clocks near their ceiling.
474+
var colder = stock.Select((p, i) => (p.Mv, p.Mhz + 45 - i)).ToList();
475+
var replan = ReapplyOn(persisted, colder);
476+
477+
Assert.Equal(plan.CapDeltaMhz, replan.CapDeltaMhz);
478+
Assert.Equal(plan.DeltasKhz[10], replan.DeltasKhz[10]); // the cap anchor's written delta
479+
}
422480
}

0 commit comments

Comments
 (0)