Skip to content

feat: the Undervolt profile keeps its result only when it gains on stock (#84) - #87

Merged
Rovey merged 7 commits into
mainfrom
feat/fair-undervolt-compare
Nov 14, 2026
Merged

Rovey merged 7 commits into
mainfrom
feat/fair-undervolt-compare

Conversation

@Rovey

@Rovey Rovey commented Nov 14, 2026 •

Copy link
Copy Markdown
Owner

Refs #84. The implementation of the design in #86, for the Undervolt profile. The design was approved by the owner, including two deviations listed below. Hardware check 111 passed on the reference RTX 4070 (2026-11-14); check 112, a card that is hot at stock, has not run.

What changes

Core (src/core/, pure logic, unit-tested):

  • run_stability also averages the last half (kCountedShare = 0.5) of a measurement that ran its full length, in StabilityResult::counted: score, power, core and memory clock, temperature and fan speed. A measurement that ended early has no counted part. counted_line shows a measurement as it was compared: the averages, then the whole run's peak and last fan reading, each named.

  • judge_against_stock (core/undervolt.hpp) decides whether the Undervolt profile keeps its result. It needs a valid comparison: both sides ran their full length under the same fan control. Then:

    • not slower: at least kNotSlowerScore (99 %) of the stock score;
    • no thermal regression: not more than kKeepCoolerC (2 C) warmer unless at least kKeepQuieterPts (5 points) quieter, and not more than 5 points louder unless at least 2 C cooler;
    • at least one benefit: power at most kKeepPowerShare (97 %) of stock's, or 2 C cooler, or 5 points quieter.

    Higher power alone never discards a result, and finishing the soak alone never keeps one. The tolerances are provisional named constants.

App (src/app/):

  • measure_stock can drive the fans with a curve (FanDriver, handed back by FanRelease).
  • run_undervolt, when not on top of a saved tune:
    • measures stock on the curve the run uses on a card that stays under the profile's limit;
    • when that peak raises the ceiling and so moves the curve up, measures stock once more on the moved curve, and compares the result with that second measurement (the first still sets the ceiling and the clock to keep);
    • stops before the search with not compared: the measurement at stock stopped at N C after S s when the measurement to compare with did not run its full length;
    • after the soak, judges the result. One that is not kept is put back to stock and not saved. RESULT: not applied -- <reason> gives the reason with both sides' numbers.
  • A kept result shows and saves exactly what it was judged on:
    • the result lines: averages of the last 150 s: score= core= power= temp= fan=; whole 300 s: peak= fan at the end=;
    • the result screen: a row for each average (speed, core clock, power, temperature, fan speed), plus Peak temperature, whole run and Fan speed at the end apart. The headers read Before (stock, fans on the app's curve) and After (fans on the app's curve), with a note under the table;
    • the saved against_stock: MeasuredRun gains counted_s, avg_temp_c and avg_fan_pct. They are written only for such a comparison, so every other profile file is written as before; a file without them reads as before. The Dashboard shows Temperature, average and Fans, average next to Peak temperature.
  • The Optimize page adds, for Undervolt: Up to 5 minutes longer when the card runs hotter than 75 °C at stock: it is then measured at stock a second time, on the fan curve the run uses.

Docs:

  • README (the Undervolt rules and the time estimate);
  • a design-decision row in docs/development.md;
  • hardware checks 111 and 112 in docs/hardware-checks.md, marked Not run yet.

Review fix (553ff50)

The verdict used the counted half's average temperature and fan speed, but the result lines, the result screen and the saved comparison showed the whole run's peak temperature and last fan reading in their place (counted_view copied only score, clocks and power). counted_view is removed, and every place now shows and keeps the averages the verdict used, with the peak and the last reading named apart (see above). The thresholds and the comparison are unchanged.

Deviations from the design, approved by the owner

  • The speed profiles and All in one keep their measurement at stock on the driver's fans. Changing it would change the "before" their own slower-than-stock rule and the All in one table compare with; that is a change of its own.
  • An undervolt on top of a saved tune (gao --undervolt --on-overclock, the undervolt step of All in one) is not judged against stock. Its clock is lower than the overclock's on purpose, so "not slower than the reference" would discard it every time.

Tests

  • cmake --workflow --preset ci (Release, /W4 /WX): no warnings.
  • core_tests: 514 test cases, 11794 assertions, all passed (was 501). New tests cover:
    • the counted half: the averages, an early end, a card without a fan reading;
    • counted_line;
    • the new against_stock fields: written only with averages, read back, and ignored unless all three are there;
    • judge_against_stock with the RTX 4070 and GTX 1080 Ti numbers, every rule at its boundary, a missing reading, other fan control, and a soak without a counted part.
  • app_tests: 55 test cases, 320 assertions, all passed (was 52). New tests cover:
    • the result lines of a judged undervolt: averages, not the whole run's numbers, with the peak apart;
    • the result lines of a discarded one;
    • the saved comparison (counted_against_stock) keeping the judged averages through a profile-file round-trip.

Hardware check 111: passed on the reference RTX 4070 (2026-11-14)

Build f2b235e, driver 617.42, fan curve 50 C 52 %, 70 C 52 %, 75 C 100 %.

Run Applied Stock (last 150 s): score / power / temp / fan Result (last 150 s) Against stock
1 990 mV, kept 5727.0 it/s / 198 W / 70 C / 62 % 5702.1 / 159 / 67 / 52 -0.4 %, -19.7 %, -3 C, -10 pts
2 990 mV, kept 5728.2 / 198 / 70 / 62 5701.3 / 158 / 66 / 51 -0.5 %, -20.2 %, -4 C, -11 pts
3 990 mV, kept 5725.7 / 198 / 70 / 61 5701.0 / 159 / 67 / 52 -0.4 %, -19.7 %, -3 C, -9 pts
window 990 mV, kept 5710 / 198 / 70 / 61 5682 / 158 / 67 / 52 -0.5 %, -20.2 %, -3 C, -9 pts
  • No second measurement at stock, as expected: the card peaked at 71 to 72 C on the curve.

  • The scatter, twice the range over the three command-line runs (one tune, so it is measurement noise):

    Score Power Temperature Fan
    Stock 0.09 % 0 % 0 C 2 pts
    Result 0.04 % 1.3 % 2 C 2 pts
    Tolerance 1 % 3 % 2 C 5 pts

    Every tolerance holds. The values stay as they are. Power, temperature and fan are whole numbers, so a range of 1 is the smallest step; the temperature tolerance sits at twice that step.

  • The window and the Dashboard showed the judged averages with the right headers, rows and notes. The window run scored about 0.3 % lower on both sides; it is left out of the scatter.

  • Not seen on hardware: a result that is discarded, and not compared:. These are covered by unit tests only.

  • Found during the check, not caused by this PR: a kept result also installs the build under test as the copy the logon task starts (update_logon_copy). This is existing behaviour. The procedure in AGENTS.md and checks 111 and 112 now says to put the installed release back afterwards; that was done here.

Hardware check 112: not run

It needs a tester with a GTX 1080 Ti or RTX 3080 Ti that runs hot at stock. A local test build, version 0.4.10-check112, has been prepared with instructions. It is not published.

Not to be merged before the owner's review.

🤖 Generated with Claude Code

Rovey and others added 4 commits November 14, 2026 19:14
…d against stock

run_stability now also averages the last half of a measurement that ran
its full length (StabilityResult::counted): score, power, clocks,
temperature and fan speed, when the card has run under load for minutes
on either side. counted_view gives a measurement as that half.

judge_against_stock decides whether the Undervolt profile keeps its
result, over performance, power and thermals apart (#84): not slower
than kNotSlowerScore of stock, no thermal regression, and at least one
benefit. Without a valid comparison (a side that did not run its full
length, or other fan control) nothing is kept. The tolerances are
provisional named constants until the scatter is measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The measurement at stock of an Undervolt run now drives the fans with the
curve the run uses (measure_stock with a curve), so the result is
compared under the same fan control (#84). When its peak raises the
ceiling and so moves the curve up, the card is measured at stock once
more on the moved curve; that second measurement is what the result is
compared with, while the first still sets the ceiling and the clock to
keep. A measurement at stock that did not run its full length ends the
run before the search ("not compared").

After the soak, judge_against_stock decides over the last half of both
measurements. A result that is not kept is put back to stock and not
saved; the RESULT line gives the reason. A kept result shows and saves
those counted halves, and the window's table says that both sides ran
on the app's fan curve.

Not changed: the speed profiles and All in one measure stock on the
driver's fans as before, and an undervolt on top of a saved tune is not
judged against stock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ainst stock (#84)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stock

A card that runs hotter than 75 C at stock is measured at stock a second
time, on the moved fan curve (#84): up to five minutes more than the 13
the Optimize page states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rovey and others added 3 commits November 14, 2026 19:48
…t was judged on

judge_against_stock decides on the counted half's average temperature and
fan speed, but counted_view copied only score, clocks and power, and the
result lines, the result screen and the saved against_stock went on
showing the whole run's peak temperature and last fan reading in their
place.

counted_view is gone. UndervoltOutcome::before/after hold the two whole
measurements as judged, and whoever shows them names each value:
- counted_line (result lines): "averages of the last 150 s: score, core,
  power, temp, fan;  whole 300 s: peak, fan at the end";
- the result screen: rows for each average, and "Peak temperature, whole
  run" and "Fan speed at the end" apart;
- the saved against_stock (counted_against_stock): MeasuredRun gains
  counted_s, avg_temp_c and avg_fan_pct, written only for such a
  comparison, so every other profile file stays as it was; the Dashboard
  shows the averages next to the peak.

The thresholds and the comparison are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w the scatter is read

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the installed logon copy

Three gao --undervolt runs and one from the window, each kept at 990 mV.
Twice the scatter of the counted half fits every provisional tolerance.
A kept result also installs the build under test as the logon copy;
the procedure now says to put the installed release back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Rovey
Rovey merged commit 17964d2 into main Nov 14, 2026
2 checks passed
@Rovey
Rovey deleted the feat/fair-undervolt-compare branch November 14, 2026 22:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant