Skip to content

Commit eeb7425

Browse files
vupleaclaude
andcommitted
Refine the curve write against its read-back to land the operating point
The driver realizes written clocks by rounding every anchor onto its own ~7.5 MHz bin, and a flat written near a bin edge can realize as a staircase whose plateau - and so the boost's settle point - sits a step above the plan's promise. No value-level rule predicts the rounding (the same written flat rounds both ways within one write), but at a fixed thermal state it is deterministic and readable back in milliseconds. So instead of modeling it, the apply now probes: when the plan's own write misses its operating point, CurveRefinement writes nearby (cap, flat) pairs - the flat re-aimed at the value its first anchor realized, the cap re-centered to hold the settle clock, a small scan as fallback - and keeps the best verified realization. Measured convergence on an RTX 5090: exact in one or two extra writes at three of four probed caps, within a bin on the fourth. The refined deltas are copied into the plan in place, so persistence and exports store the write that actually landed and the logon replay reproduces it. What the probing cannot beat is temperature: anchors step individually across thermal thresholds with the deltas frozen (measured: 885 mV at ~52 C, 890-915 mV at ~58-61 C, 925-965 mV stable to 61 C), so a point landed at apply time can still sit a step away at operating temperature. That bound is recorded in DEVELOPMENT.md, and the status line reads the live slice. The e2e shape assertions allow the refinement's sub-bin adjustments to the written rise and cap offset; the status operating-point assertion tightens from an anchor of slack to one boost step, since the apply now lands the point rather than promising near it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PACndXGEsXBAWWWjSvpJpW
1 parent 19113ad commit eeb7425

7 files changed

Lines changed: 425 additions & 84 deletions

File tree

‎DEVELOPMENT.md‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,28 @@ settles. NVIDIA doesn't document this; measured on an RTX 5090 under sustained l
7575
settle drops 5 mV lower; 16 MHz holds. On generations with ~15 MHz bins, 16 MHz is one bin and
7676
may fold — degrading to less clock at less voltage (the safe direction), which the post-apply
7777
report flags when the rise reads back collapsed.
78-
- **A level flat can read back as a two-step staircase**: a flat written near a bin edge can keep
79-
its first anchor(s) at the written clock while the tail rounds a bin up (a 925 mV cap's flat of
80-
2498: 935/940 mV read 2497, 945 mV+ read 2505). The plateau pinning the boost is then the tail,
81-
and the settle follows it up — 940 mV instead of the aimed 930. A stable realized state, not a
82-
thermal transient: the staircase and the settle hold unchanged from 45 to 61 C with the deltas
83-
frozen, against a stock table matching the reference anchor-for-anchor. Which anchors round
84-
which way isn't predictable from the written value alone (the same 2498 rounds both ways within
85-
one write; a 2408 flat rounds up to 2415 uniformly), so the plan doesn't try to pre-compensate;
86-
the post-apply report names the higher point from the read-back instead.
78+
- **A level flat can read back as a two-step staircase**: every anchor rounds onto its own
79+
~7.5 MHz clock bin, and a flat written near a bin edge can keep its first anchor(s) at the
80+
written clock while the tail rounds a bin up (a 925 mV cap's flat of 2498: 935/940 mV read
81+
2497, 945 mV+ read 2505). The plateau pinning the boost is then the tail, and the settle
82+
follows it up — 940 mV instead of the aimed 930. Which anchors round which way isn't
83+
predictable from the written value alone (the same 2498 rounds both ways within one write; a
84+
2408 flat rounds up to 2415 uniformly), but at a fixed thermal state the rounding is
85+
deterministic and readable back in milliseconds — so the apply probes nearby (cap, flat) pairs
86+
against their read-backs until the realized settle is the plan's (`CurveRefinement`; one or two
87+
extra writes in the measured cases), keeping the closest attempt when nothing lands.
88+
- **The effective curve steps with temperature, per anchor, deltas frozen**: anchors cross
89+
individual thresholds where their realized clock moves one bin (measured: 885 mV at ~52 C, the
90+
890-915 mV region at ~58-61 C, the 925-965 mV window stable to 61 C). A point landed at apply
91+
time holds only while the operating temperature stays on the same side of the nearby
92+
thresholds: at 60 C under load a cool-applied 925 mV cap held its point exactly, while a
93+
cool-applied 890 mV cap settled a step higher. Irreducible without probing at operating
94+
temperature — the status line reads the live slice and says where the boost sits now.
8795

8896
`GpuTuning.BuildCurvePlan` therefore writes the cap→flat segment as a straight line through
8997
(settle voltage, requested clock) with a fixed 16 MHz rise: the smallest the driver keeps, and a
90-
bound on the realized miss at half of it — one bin more when the flat's tail rounds up, which the
91-
report calls out. On a 10 mV gap this operates the card 5 mV above the
98+
bound on the realized miss at half of it, with the post-write refinement absorbing the bin
99+
rounding. On a 10 mV gap this operates the card 5 mV above the
92100
named cap (`--mv 900` runs at 905 mV); keeping the voltage at or under the cap would cost a
93101
stock-slope step of clock, the worse side of the trade.
94102

‎e2e/TuningShapeTests.cs‎

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,20 @@ public void PlainCap_WritesTheStraddle_AndFlattensAbove()
2727
Assert.Equal(0, exitCode);
2828
App.AssertWriteConfirmed(output);
2929

30-
// The shape signature, exact in the written deltas: the cap anchor and the band below share
31-
// one offset at most a spread below stock (the segment is aimed so the settle voltage holds
32-
// the stock clock, which writes the cap anchor a little under it), the flat top starts
33-
// exactly the fixed 16 MHz spread above the cap anchor, and everything from there up is one
34-
// level plateau. A flatten-at-the-cap shape would leave no rise between k and k+1; the
30+
// The shape signature in the written deltas: the cap anchor and the band below share one
31+
// offset near a spread below stock (the segment is aimed so the settle voltage holds the
32+
// stock clock, which writes the cap anchor a little under it), the flat top starts about
33+
// the fixed 16 MHz spread above the cap anchor, and everything from there up is one level
34+
// plateau. The rise and the cap offset carry a bin of slack each way: the post-write
35+
// refinement may move either by a few MHz to land the settle on the card's bin grid. A
36+
// flatten-at-the-cap shape would leave no rise between k and k+1; the
3537
// stock-slope-continued shape would show a stock-step rise instead of the fixed spread.
3638
int[] deltas = ReadDeltasAlignedWith(stock);
3739
Assert.Equal(deltas[k], deltas[k - 1]);
38-
Assert.InRange(deltas[k], -16_000, 0);
40+
Assert.InRange(deltas[k], -24_000, 8_000);
3941
long capAnchorKhz = stock[k].Mhz * 1000L + deltas[k];
4042
long flatTopKhz = stock[k + 1].Mhz * 1000L + deltas[k + 1];
41-
Assert.Equal(16_000, flatTopKhz - capAnchorKhz);
43+
Assert.InRange(flatTopKhz - capAnchorKhz, 8_000, 24_000);
4244
for (int i = k + 2; i < deltas.Length; i++)
4345
{
4446
Assert.Equal(flatTopKhz, stock[i].Mhz * 1000L + deltas[i]);
@@ -61,16 +63,18 @@ public void ReducedClockCap_WritesOneSharedOffset_FromTheBandThroughTheFlatStart
6163

6264
// The cap anchor's offset is one value carried by the band below it - that shared offset is
6365
// what keeps the clock on a stock-parallel slope however deep the boost settles - and the
64-
// flat top starts exactly the fixed 16 MHz spread above the cap anchor. Both equalities
65-
// hold regardless of whether the run planned from the saved reference or a live read.
66+
// flat top starts about the fixed 16 MHz spread above the cap anchor, with a bin of slack
67+
// each way for the post-write refinement. Both hold regardless of whether the run planned
68+
// from the saved reference or a live read.
6669
// The margin on the reduction is loose on purpose: planned from the saved reference, the
6770
// written delta is (live - reference) - 60, so the thermal gap between this run and the
6871
// reference capture rides on it; this only has to prove a real reduction landed.
6972
int[] deltas = ReadDeltasAlignedWith(stock);
7073
Assert.True(deltas[k] < -20_000, $"the cap anchor's delta ({deltas[k]} kHz) is not the requested reduction");
7174
Assert.Equal(deltas[k], deltas[k - 1]);
72-
Assert.Equal(16_000,
73-
(stock[k + 1].Mhz * 1000L + deltas[k + 1]) - (stock[k].Mhz * 1000L + deltas[k]));
75+
Assert.InRange(
76+
(stock[k + 1].Mhz * 1000L + deltas[k + 1]) - (stock[k].Mhz * 1000L + deltas[k]),
77+
8_000, 24_000);
7478
}
7579

7680
[SkippableFact]
@@ -102,18 +106,18 @@ public void Status_WhileCapped_ReportsTheOperatingPoint()
102106

103107
var (exitCode, output) = App.Run(null, "status");
104108

105-
// The status line names the point the boost settles on: at or above the cap point, with
106-
// wide-but-bounded slack upward. The driver can round the written flatten's tail a bin
107-
// up, which starts the pinning plateau - and moves the inferred point - a few anchors
108-
// above the written flat start (the post-apply report carries a note when it does); a
109-
// read at a temperature away from the reference can seam the flat's edge similarly. The
110-
// settle-law physics itself is LoadTests' subject; this asserts status reports a sane
111-
// point near the cap.
109+
// The status line names the point the boost settles on. The apply refines its write
110+
// against the read-back until the realized point is the plan's settle (one boost step
111+
// below the flat start, floored at the cap), so a same-power-state status read moments
112+
// later reports that point - one boost step of slack each way covers an anchor stepping
113+
// with temperature between the two reads, and a refinement that ran out of probes on a
114+
// knife-edge write. The settle-law physics itself is LoadTests' subject.
112115
Assert.Equal(0, exitCode);
113116
Match m = Regex.Match(output, @"operating point (\d+) mV");
114117
Assert.True(m.Success, $"no 'operating point' report in: {output}");
118+
int settleMv = Math.Max(stock[k].Mv, stock[k + 1].Mv - GpuTuning.BoostStepMv);
115119
Assert.InRange(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture),
116-
stock[k].Mv - GpuTuning.BoostStepMv, stock[k].Mv + 25);
120+
settleMv - GpuTuning.BoostStepMv, settleMv + GpuTuning.BoostStepMv);
117121
}
118122

119123
[SkippableFact]

‎src/CurveRefinement.cs‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
namespace SimpleNvidiaUndervolt;
2+
3+
/// <summary>
4+
/// The post-write refinement of a planned curve write. The driver realizes written clocks by
5+
/// rounding every anchor onto its own clock bin, and a flat written near a bin edge can realize
6+
/// as a staircase whose plateau — and so the boost's settle point — sits a step above the plan's
7+
/// promise (see DEVELOPMENT.md, "Where the boost settles"). No value-level rule predicts the
8+
/// rounding, but it is deterministic at a fixed thermal state and readable back in milliseconds,
9+
/// so the exact write is found by probing: when the plan's own write misses its operating point,
10+
/// nearby (cap, flat) pairs are written and judged against their read-backs, keeping the best
11+
/// realization — measured, never modeled, so it carries no constants from any one card. The
12+
/// budget is small and the values move by less than a bin, and a probe sequence that never lands
13+
/// keeps the closest attempt: the refinement can only improve on the plan's write, and the
14+
/// realized point is reported from the final read-back either way. What it cannot beat is the
15+
/// thermal slice: anchors step with temperature, so a point landed at apply time can still sit a
16+
/// step away at operating temperature — physics the report's notes carry, not error.
17+
/// </summary>
18+
internal static class CurveRefinement
19+
{
20+
/// <summary>Extra writes the probing may spend after the plan's own. The measured cases
21+
/// converge in one or two; the budget covers a full bin of flat values either way.</summary>
22+
internal const int MaxExtraWrites = 8;
23+
24+
/// <summary>The clock slack an on-target realization may keep: half a read-back bin — the
25+
/// resolution the effective curve reports at, so probing below it chases noise.</summary>
26+
internal const int ClockToleranceMhz = 4;
27+
28+
/// <summary>What the refinement left on the card: the deltas the final write holds (also
29+
/// copied into the plan's array by <see cref="GpuTuning.Apply"/>), the settle point of the
30+
/// final read-back (null when it wasn't readable), how many extra writes were spent, and
31+
/// whether the point is the plan's own.</summary>
32+
internal sealed record Outcome(int[] DeltasKhz, (int Mv, int Mhz)? Realized, int ExtraWrites,
33+
bool OnTarget);
34+
35+
/// <summary>
36+
/// Judges the plan's write by <paramref name="planReadBack"/> and, when its realized settle
37+
/// point misses the plan's, probes alternative (cap, flat) pairs through
38+
/// <paramref name="apply"/> — which writes a delta array and returns the effective read-back.
39+
/// Candidates come from the read-back itself (the flat re-aimed at the value its first anchor
40+
/// realized, the cap re-centered to keep the settle clock) with a small scan as fallback.
41+
/// Ends on the best-scoring pair, re-applied if a later probe overwrote it; the outcome's
42+
/// realization is always the final read-back's, so a thermal step between probes can't leave
43+
/// a stale claim behind.
44+
/// </summary>
45+
public static Outcome Refine(Func<int[], IReadOnlyList<(int Mv, int Mhz)>> apply,
46+
IReadOnlyList<(int Mv, int Mhz)> stock, GpuTuning.CurvePlan plan,
47+
IReadOnlyList<(int Mv, int Mhz)> planReadBack)
48+
{
49+
(int Mv, int Mhz)? realized = GpuTuning.EffectiveOperatingPoint(planReadBack);
50+
if (OnTarget(realized, plan))
51+
{
52+
return new Outcome(plan.DeltasKhz, realized, 0, true);
53+
}
54+
55+
int bestScore = Score(realized, plan);
56+
int[] bestDeltas = plan.DeltasKhz;
57+
var bestRealized = realized;
58+
int[] lastWritten = plan.DeltasKhz;
59+
int writes = 0;
60+
61+
var tried = new HashSet<(int C, int V)> { (plan.CapMhz, plan.FlatMhz) };
62+
foreach ((int c, int v) in Candidates(plan, planReadBack))
63+
{
64+
if (writes >= MaxExtraWrites)
65+
{
66+
break;
67+
}
68+
69+
if (v <= c || !tried.Add((c, v)))
70+
{
71+
continue;
72+
}
73+
74+
int[] deltas = DeltasFor(stock, plan, c, v);
75+
IReadOnlyList<(int Mv, int Mhz)> effective = apply(deltas);
76+
writes++;
77+
lastWritten = deltas;
78+
79+
var r = GpuTuning.EffectiveOperatingPoint(effective);
80+
int score = Score(r, plan);
81+
if (score < bestScore)
82+
{
83+
(bestScore, bestDeltas, bestRealized) = (score, deltas, r);
84+
}
85+
86+
if (OnTarget(r, plan))
87+
{
88+
return new Outcome(deltas, r, writes, true);
89+
}
90+
}
91+
92+
// Land on the best probed pair. Its realization is re-read rather than recalled: between
93+
// probes an anchor can step with temperature, and the report must carry what the card
94+
// holds now, not what this pair realized a moment ago.
95+
if (!ReferenceEquals(lastWritten, bestDeltas))
96+
{
97+
bestRealized = GpuTuning.EffectiveOperatingPoint(apply(bestDeltas));
98+
writes++;
99+
}
100+
101+
return new Outcome(bestDeltas, bestRealized, writes, OnTarget(bestRealized, plan));
102+
}
103+
104+
/// <summary>Whether a realization is the plan's settle point: the voltage exactly (settle
105+
/// voltages move in whole boost steps, so any miss is a full step) and the clock within
106+
/// read-back resolution.</summary>
107+
internal static bool OnTarget((int Mv, int Mhz)? realized, GpuTuning.CurvePlan plan)
108+
=> realized is { } r && r.Mv == plan.SettleMv
109+
&& Math.Abs(r.Mhz - plan.SettleMhz) <= ClockToleranceMhz;
110+
111+
/// <summary>The miss of a realization, for keep-best: clock error plus heavily weighted
112+
/// voltage error — a settle step costs power at every operating second, a few MHz is within
113+
/// read-back noise. An unreadable realization never wins.</summary>
114+
private static int Score((int Mv, int Mhz)? realized, GpuTuning.CurvePlan plan)
115+
=> realized is { } r
116+
? Math.Abs(r.Mhz - plan.SettleMhz) + 4 * Math.Abs(r.Mv - plan.SettleMv)
117+
: int.MaxValue;
118+
119+
/// <summary>The probe sequence: first the pairs the read-back suggests — the flat re-aimed at
120+
/// the value its first anchor realized (the read-back is the local bin grid), with the cap
121+
/// kept, re-centered so the settle-point clock stays on the request, and dropped a step — then
122+
/// a flat-value scan around the plan's own, downward through one bin and one step up.</summary>
123+
private static IEnumerable<(int C, int V)> Candidates(GpuTuning.CurvePlan plan,
124+
IReadOnlyList<(int Mv, int Mhz)> planReadBack)
125+
{
126+
if (GpuTuning.PointAtVoltage(planReadBack, plan.FlatMv) is { } flatRead)
127+
{
128+
int vr = flatRead.Mhz;
129+
yield return (plan.CapMhz, vr);
130+
yield return (2 * plan.SettleMhz - vr, vr);
131+
yield return (plan.CapMhz - 4, vr - 8);
132+
}
133+
134+
for (int d = 1; d <= 6; d++)
135+
{
136+
yield return (plan.CapMhz, plan.FlatMhz - d);
137+
}
138+
139+
yield return (plan.CapMhz, plan.FlatMhz + 1);
140+
yield return (plan.CapMhz, plan.FlatMhz + 2);
141+
}
142+
143+
/// <summary>The delta array for a probed (cap, flat) pair — the plan's own shape
144+
/// (<see cref="GpuTuning.CapShapeMhz"/>) at the probed clocks, relative to the same stock
145+
/// curve the plan was built from. Anchor 0 stays unwritten like every plan.</summary>
146+
private static int[] DeltasFor(IReadOnlyList<(int Mv, int Mhz)> stock,
147+
GpuTuning.CurvePlan plan, int capMhz, int flatMhz)
148+
{
149+
int[] newMhz = GpuTuning.CapShapeMhz(stock, plan.CapIndex, capMhz, flatMhz, plan.CapPoints);
150+
var deltasKhz = new int[stock.Count];
151+
for (int i = 1; i < stock.Count; i++)
152+
{
153+
deltasKhz[i] = (newMhz[i] - stock[i].Mhz) * 1000;
154+
}
155+
156+
return deltasKhz;
157+
}
158+
}

0 commit comments

Comments
 (0)