|
| 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