Skip to content

Commit 7e21d5f

Browse files
committed
fix(core): a candidate may fall short of the target by five grid steps
The slack on the clock of an undervolt candidate existed for the built-in curve moving with temperature between the write and the loaded probe: 45 MHz cold against warm on the reference card. On an RTX 3080 Ti that runs 86 C at stock the first candidate read 53 MHz low with its score at 98 % of stock, and the run stopped at its first step with "the card held no undervolt". The slack is now 75 MHz, five steps of the clock grid. The share of the stock score a candidate must keep is unchanged, and stays what guards the speed of the result.
1 parent ebf2897 commit 7e21d5f

4 files changed

Lines changed: 30 additions & 13 deletions

File tree

‎docs/development.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ The applied offset is always at least one step below the confirmed edge. Profile
115115
| The undervolt profile measures stock as every other run does, and does not stop a hot card's candidates for being hot | The undervolt profile had a stock measurement of its own: five minutes with the fans on the curve and the profile's 75 C as the limit, and anything but `STABLE` ended the run with `the card is not stable at stock`. That is the mistake of the row above, left standing in a second place: the tester's RTX 3080 Ti runs 82 to 83 C at stock even with the fans at 100 %. Now `run_undervolt` starts with `measure_stock`, the one measurement every run begins with (fans with the driver, stopped at `kStockTempC`, kept when it was, a cool-down after it), and hands it to the search as its reference (`UndervoltBase::measured`, which the all-in-one run already used for the overclock's soak); `stock_refusal` decides what such a reference may be, in both places. There is one more thing the overclock profiles do for a hot card that the undervolt cannot: they lower the power limit until the profile's temperature holds. An undervolt changes neither clocks nor power. So the search does not stop a candidate below the temperature the card reached in the reference (`limit_c`, never above `kStockTempC`): an undervolt makes a card cooler than it was, never hotter, and without this every candidate of a card that is above the profile's limit to begin with ends `TOO HOT` and reads as "this card holds no undervolt". On a card that stays under the limit at stock nothing changes: the limit is the profile's. The fans still go to 100 % at the profile's limit. Not for a run on top of the saved tune (`--on-overclock` without a soak handed over): its reference is that overclock, measured by the search at the profile's limit, and one that gets too hot now says so instead of "not stable". |
116116
| With its reference handed over, the undervolt search warms its load up before the first candidate | A search that measures its own reference has five minutes of load behind it when the first 3 s candidate runs. One that is handed the reference (the measurement at stock, or the overclock's soak in All in one) starts its load with that candidate, and a load that has not run yet is slower in its first seconds than the card is: it sizes its batches and the card clocks up from idle. The candidate then averages a clock far below the target and reads `SLOW`, and a descent ends at its first failure. Seen three times on the reference card: `1070 mV (+15 MHz): SLOW ... clock=2323 MHz` and `1050 mV: SLOW ... clock=2411 MHz` in hardware check 92, where the undervolt profile then said "the card held no undervolt", and `1025 mV (+150 MHz): SLOW ... clock=2380 MHz` in an All in one run of 2026-10-08. That last one means the undervolt step of All in one has kept the overclock's curve cut off at its chosen clock and searched nothing below it, in every release that has it; its numbers in the README were measured that way. Now the load runs `kUvWarmUpS` (5 s) at the reference's settings first; only whether the card computes is taken from it. The score threshold was not loosened: a candidate that is slow on a load that is running is still `SLOW`. |
117117
| A flat top goes back on a card that is cooler than when it was found | The built-in curve moves with temperature: a point reads up to three steps (45 MHz) more on a cool card than on a warm one. A curve that was only cut off (All in one, when the margin leaves no raise above the overclock) is anchored on the point that ran the target by itself while the search had the card warm. Seconds later, in the next step, the same point read one step more, and `apply_flat_top` refused to put the tune back: "the point already runs more than the target" (hardware check 94). The warm-up of the row above is what made the search read its curve on a warm card; before it, the cut-off was anchored on a card that had idled, and the fault did not show. `apply_flat_top` now takes an anchor that runs up to `kVfTailDropKhz` more than the target and holds it on the target, at the voltage the tune was tested on; more than that is still refused, as not this tune's curve. `curve_state` knows such an anchor (stored below the base offset by no more than that, with the points above cut off) as this program's flat top, so the dashboard and the watchdog do not take it for another program's work. The other direction was always there: a top written on a cool card runs a step lower once it is warm (`kUvClockSlackKhz`). |
118+
| The clock a candidate may fall short of the target by is five grid steps | The slack existed for the built-in curve moving with temperature between the write and the loaded probe (45 MHz cold against warm on the reference card). On an RTX 3080 Ti that runs 86 C at stock, the first candidate of an undervolt read 53 MHz low with its score at 98 % of stock, and the run stopped at its first step. The slack is now 75 MHz. The share of the stock score a candidate must keep is unchanged, and stays what guards the speed of the result; the 30 s confirm and the soak judge what was found as before. |
118119
| The undervolt search keeps the fans on a curve | It left them to the driver and still stopped every probe at the profile's limit of 75 C. That holds on the reference card, where the driver's fans keep an undervolted card far below it, and cannot hold on a card the driver lets run at 85 C: every probe would end `TOO HOT` and read as "the card held no undervolt". The search now drives the fans as `run_optimize` does: the curve in use (the overclock's own, in an all-in-one run) or the preset's, 100 % at the limit. The undervolt is then tested with the fans it will run with, so the run turns the app's fan control on, as every other tune does. Not verified on a hot card: whether 100 % keeps a 3080 Ti below 75 C at stock power is not known, and the undervolt has no power limit to lower. |
119120
| The clock search takes the five minutes at stock as its baseline | The search judges every probe by a score at stock: below a quarter of it a probe is `STALLED`, a card is back after a reset at nine tenths of it, and the efficient profiles look for the lowest power limit within 2 % of it. It used to take that score in 30 s of its own, right after the five minutes at stock that every profile now starts with (`optimize(..., stock)`); a stock measurement that was not stable is not used, and without one the search still takes its 30 s. The five-minute score is a little higher than a 30 s one on a card that is already hot (5705 against 5662 it/s on the reference card), which makes the 2 % of the efficient profiles slightly stricter. Not yet measured on hardware. |
120121
| The search says which step it is at | `OptimizeIo::measuring`, as the undervolt and the fan tune have it: the step, and its length when it lasts (a confirm probe, the final test, the rest after a reset). A search probe lasts three seconds and gets no countdown, only its name. The window shows one row for all of it, as high with a countdown as without, so the page does not jump. |

‎src/core/undervolt.hpp‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@ inline constexpr double kUvWarmUpS = 5;
4545
inline constexpr int kUvConfirmTries = 3;
4646
inline constexpr int kUvSoakAttempts = 3;
4747
// A candidate must run the target clock within what the built-in curve moves
48-
// with temperature (45 MHz between cold and warm on the reference card: a
49-
// flat top written on a cooler card runs that much lower once it is hot), and
50-
// keep this share of the stock score (3 s probes scatter by a few percent
51-
// around the 30 s baseline). One that computes right but clearly slower took
52-
// the curve without running it ("SLOW").
53-
inline constexpr int kUvClockSlackKhz = 45000;
48+
// with temperature between the write and the loaded probe. On the reference
49+
// card that is 45 MHz cold against warm; on an RTX 3080 Ti that runs 86 C at
50+
// stock the first candidate read 53 MHz low with its score at 98 % of stock.
51+
// 75 MHz is five steps of the clock grid. And keep this share of the stock
52+
// score (3 s probes scatter by a few percent around the 30 s baseline). One
53+
// that computes right but clearly slower took the curve without running it
54+
// ("SLOW").
55+
inline constexpr int kUvClockSlackKhz = 75000;
5456
inline constexpr double kUvScoreKeep = 0.9;
5557

5658
// The overclock an undervolt is searched on top of: "overclock first, then

‎tests/test_boot.cpp‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,8 +331,8 @@ TEST_CASE("an undervolt that does not fit this card's curve is refused and ends
331331
CHECK(card.curve_writes == 0);
332332
}
333333
{
334-
CurveCard card; // the built-in curve reads 60 MHz lower than when it was tested: the raise would be 210, tested 150
335-
for (int& f : card.base_khz) f -= 60000;
334+
CurveCard card; // the built-in curve reads 100 MHz lower than when it was tested: the raise would be 250, tested 150
335+
for (int& f : card.base_khz) f -= 100000;
336336
bool clean = false;
337337
CHECK_FALSE(apply_profile(card.gpu(), undervolt_profile(), &why, &clean));
338338
CHECK(why.find("tested with 150 MHz") != std::string::npos);
@@ -429,11 +429,11 @@ TEST_CASE("the raise of an undervolt over a core offset is checked against the b
429429
p.core_mhz = 60;
430430
p.undervolt = UndervoltTune{850000, 2460000, 210000};
431431
CurveCard card;
432-
for (int& f : card.base_khz) f -= 60000; // the built-in curve reads 60 MHz lower: the point would run 270 over it, tested 210
432+
for (int& f : card.base_khz) f -= 100000; // the built-in curve reads 100 MHz lower: the point would run 310 over it, tested 210
433433
std::string why;
434434
bool clean = false;
435435
CHECK_FALSE(apply_profile(card.gpu(), p, &why, &clean));
436-
CHECK(why.find("would be raised by 270 MHz; the undervolt was tested with 210 MHz") != std::string::npos);
436+
CHECK(why.find("would be raised by 310 MHz; the undervolt was tested with 210 MHz") != std::string::npos);
437437
CHECK(clean);
438438
CHECK(card.at_stock());
439439
CHECK(card.core == 0); // the reset; the overclock was never written

‎tests/test_undervolt.cpp‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct UvCard {
2424
int long_lost_above_khz = -1; // the same for probes of 30 s and longer; -1: as the short ones
2525
int losses_left = 1;
2626
bool dead = false; // after a driver reset, until recover()
27-
bool slow = false; // the card accepts the curve but does not run the frequency
27+
int slow_khz = 0; // > 0: the card accepts the curve but runs this far below the frequency
2828
bool write_ok = true;
2929
int write_fails_above_khz = -1; // >= 0: a curve write that raises a point by more than this fails
3030
bool baseline_ok = true;
@@ -130,7 +130,7 @@ struct UvCard {
130130
// At stock the power limit holds the clock between two points of the curve.
131131
const bool cold = cold_load && seconds < 30;
132132
cold_load = false;
133-
const int f = (level() ? stock_load_khz + raw.front() : slow ? freq(op) - 105000 : freq(op)) - (cold ? 420000 : 0);
133+
const int f = (level() ? stock_load_khz + raw.front() : freq(op) - slow_khz) - (cold ? 420000 : 0);
134134
const int raise = freq(op) - base_khz[op];
135135
probe_raise_mhz.push_back(raise / 1000);
136136
const double v = volt_uv[op] / static_cast<double>(volt_uv[stock_point()]);
@@ -310,14 +310,28 @@ TEST_CASE("find_undervolt: a reset without a way to reconnect ends the run") {
310310

311311
TEST_CASE("find_undervolt: a card that takes the curve but does not run the frequency holds no undervolt") {
312312
UvCard card;
313-
card.slow = true;
313+
card.slow_khz = 105000;
314314
const auto r = card.go();
315315
CHECK_FALSE(r.ok);
316316
CHECK(r.reason == "the card held no undervolt: the first step below its own voltage already failed");
317317
CHECK(card.at_stock());
318318
CHECK(card.logged("SLOW"));
319319
}
320320

321+
TEST_CASE("find_undervolt: a candidate that runs a thermal step slow keeps being searched") {
322+
// Seen on an RTX 3080 Ti (2026-10-12, 0.4.8-rc1): the flat top read back
323+
// on the target, but under the 3 s load the card heated and its built-in
324+
// curve sank further than the slack allowed. The first candidate read
325+
// SLOW at 98 % of the stock score, and the run stopped at its first
326+
// step. The slack is there for exactly this movement; the score is what
327+
// guards the speed.
328+
UvCard card;
329+
card.slow_khz = 60000; // between the old slack (45 MHz) and the new one
330+
const auto r = card.go();
331+
REQUIRE(r.ok);
332+
CHECK_FALSE(card.logged("SLOW"));
333+
}
334+
321335
TEST_CASE("find_undervolt: refusals before anything is tried") {
322336
{
323337
UvCard card;

0 commit comments

Comments
 (0)