Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ The applied offset is always at least one step below the confirmed edge. Profile
| A logon apply that ends cleanly is no strike | Strikes count logons that may have crashed the machine. An apply that was refused before any write, or failed and reset the card to stock, did not: the strike it recorded up front is taken back. Only a failed reset keeps it. Before 0.3.1 three refused logons read as `disabled after 3 crashes`. |
| One NVIDIA GPU per PC; with more, no run starts and the logon apply applies nothing | The app takes the first NVIDIA card three times over, and each list has its own order: NVML device 0 (`kGpu`) for the power limit and the telemetry, NVAPI's first physical GPU for the offsets and the curve, and DXGI's first NVIDIA adapter for the stress load and the bandwidth measurement. With two NVIDIA cards the card that is tuned and the card that is tested can be different ones: the tuned card then idles while the other one works, every step reads as stable, and the run saves a tune that was never tested. The read-back does not see it, because each value is read back from the card it was written to. So Optimize, Undervolt, All in one and Retest stop before they touch the card, and `decide_boot` returns `SeveralGpus` at logon (before the strike), when NVML counts more than one device (`several_gpus` in `src/core/boot.hpp`). NVML counts NVIDIA devices only: an Intel or AMD integrated GPU next to one NVIDIA card counts 1 and changes nothing. A count that cannot be read does not refuse; the calls after it report that failure themselves. The window's **Apply saved tune** and `gao --apply` refuse the same way (`saved_tune_for` passes the count to `decide_boot`), and the tray watchdog does not put a tune back once NVML counts a second NVIDIA GPU (one plugged in after the tune was applied): it says so once in boot.log and in a balloon. **Not chosen:** matching the three lists to each other (DXGI by the adapter LUID, NVAPI by the PCI bus) so that two cards work. It changes `src/hw/` and needs a PC with two NVIDIA cards to check; it waits for someone with such a PC to ask. Not tested on such a PC; the count of 1 is seen on an RTX 5070 next to an AMD Radeon integrated GPU. |
| An undervolt is a flat top on the curve, found like a core offset | The driver lets a program offset the frequency of each point of the voltage/frequency curve; voltages cannot be written. To run a clock on less voltage, a lower point (the anchor) is raised to that clock and no point above it runs more, so the card has no reason to ask for more voltage. The points below keep the built-in curve, so idle clocks stay as they are. No voltage lock is set: the lock call hard-froze the reference RTX 4070 in the Python version. The write (`apply_flat_top`) uses the units the reference card stores (kHz, one to one; hardware check 61) and estimates nothing. What protects a card that stores them differently is the order: the first round raises a point only half of the way, so a card that moves twice as far lands on the target and not above it, and a card that did not move the anchor by what was written (within one step of its clock grid) is refused before the full step; each later round adds a point's own remaining gap as read back, which also follows a curve that shifts with temperature; no offset is ever written beyond what the plan asks plus two grid steps; and a card that has not arrived after four rounds is refused and cleared. (A first version estimated the units from the read-back; a review showed that estimate could push the anchor far above the target, and it was removed before any write ran on hardware.) A reset to stock zeroes a shape on the curve first and judges it by reading back; a plain core offset, which is the same offset on every point, still goes with the offset reset alone. The search (`find_undervolt`) keeps the clock the card runs under load at stock and moves the anchor one point down at a time. What a candidate asks of the card is its raise, the anchor's frequency above its built-in one, which is a core offset seen at one point; so the core search's rules carry over unchanged: first failure ends the descent, 30 s confirm, the same margin on the raise, 300 s soak, journal entry (`"uv"`) before the write, any failure ends at stock. The core offset and the curve are one table (hardware check 61), so a profile will carry one or the other. First version: `gao --undervolt` only, not saved, not in the window; no write has been run on hardware yet (checks 62 to 65). The curve calls are undocumented NVAPI, as the offsets are; the write procedure and the search are this project's own, with Green Curve (aufkrawall/green-curve, MIT) read as a worked example of what goes wrong on real cards. |
| A GTX 10 card's curve is read in half kHz | The curve calls are not documented, and the unit of a point's frequency and of its offset is the card's: kHz from the RTX 20 series on (hardware check 61), half kHz on a GTX 10 card (Pascal), where 1873 MHz reads `3746000`. Read as kHz, that curve lies twice as high as the clock NVML reports, no point of it is at or below the clock the card ran, and every undervolt search on such a card ended before its first candidate with `the load clock is below the card's curve`; All in one has that step too. Nothing wrong was written: the search stops there. `Nvapi` now asks the driver for the card's architecture (`NvAPI_GPU_GetArchInfo`, a public call) and brings both fields to kHz where the buffers are read and written (`vf_units_per_khz`, `parse_vf_curve`, `put_vf_raw_offset`), so the search, the saved tune and the window count in kHz on every card. An architecture that cannot be read counts as kHz, as before. The half kHz are not this project's own measurement: they come from a GTX 1080 on driver 582.78 (1556.5 MHz at 850 mV read `3113000`; reported for simple-nvidia-undervolt) and from two projects that drive the same calls on such cards (nvapi-rs counts the curve in half kHz; green-curve halves both fields for this architecture and uses the buffer layout this project does). **Verified on a GTX 1080 Ti** (driver 582.78, hardware check 110 (b)): the converted curve tops out at 1999.5 MHz, the clock under load was about 1860 MHz, and the undervolt search ran through its candidates to a saved result. Its clock steps are 38/3 MHz, read out in half MHz as 12.5 or 13 MHz; `clock_grid_khz` takes 12667 kHz for such a curve. Before that it took the greatest common divisor, 0.5 MHz, so the two grid steps of drift allowed after the points above the anchor are lowered shrank to 1 MHz, and the 1080 Ti's 881 mV candidate was refused for reading one step (13 MHz) above its target. |
| Dear ImGui on DX11 | One small binary with no runtime, and the D3D11 device is in the process anyway for the stress load. |
| The applied values are the profile's main fields; the tune the run found sits beside them in `step_down` | A step down (`gao --step-down`, **Lower tune 5 %**) writes the lowered core, memory and flat-top values into `core_mhz`, `mem_mhz` and `undervolt`, and keeps the found tune in `step_down.found` with the step, `tested` and `failed_step`. An older build ignores the unknown object and applies the main fields, which are the safer values; with the step count alone and the found values in the main fields it would apply the more aggressive tune the user stepped away from. Everything else (apply at logon, the watchdog, `tune_applied`, the retest) follows the main fields without a special case. Step n is n x 5 % of the gain over stock, always counted from the found tune, rounded towards stock (core 15 MHz, memory 50 MHz, the top clock down to the card's clock grid, 15 MHz when the curve cannot be read, but never below the built-in curve: the raise stays 0 or more); the top's voltage, the power limit and the fan curve never change. At most 10 steps. A `step_down` that cannot be read whole counts as absent. |
| A failed test of a step steps down by itself | A step is derived from a tested tune and applied without a test of its own; **Test this step (5 min)** runs the retest on it. When that fails, sending the user to a 40-minute run is the wrong answer to a stutter they were trying to fix cheaply, so the test goes on at the next step down, one five-minute test per step, and ends at the first step that holds (applied, saved, `tested`). `failed_step` records the largest step number that failed, and +5 % never goes back to it or above it; a step that later holds its test sets it to the step below (`retest_stepping`). At step 10, or at a step that changes nothing, the card goes to stock and the tune (`failed_its_test`: its step at or below `failed_step`) is neither applied at logon nor put back by the watchdog; `gao --retest` that ends there tells a running tray to stop keeping it (`TrayNotice::TuneFailedTest`). **−5 %** stays allowed from there while a step remains: the lower step is a new tune above `failed_step`. The same holds for the retest after a driver update. Abort puts back what was on the card before the test started (`tune_before_run` / `put_back_after_stop`, in the window and in `gao --retest`), except when the test had already stepped down: then `run_retest` puts the step under test back and saves it, and `put_back_after_stop` sees that the saved tune changed under the run (`put_back_target`) and puts back that saved step if it may be applied, never the earlier step, which failed. A step whose test ended in a driver reset (`DEVICE LOST`, `STALLED`, `NO TELEMETRY`) is followed by the optimize run's recovery before the next step is loaded: reconnect, stock, a 20 s rest, a fresh stress load, and a 5 s health probe at stock (up to three tries, measured against the stock score the run saved; a profile without a saved stock score counts as back when its health probe runs clean). Without it the next step was loaded on a card that was not back and failed within seconds: on the owner's RTX 5070 one forced reset cost two steps. A second reset in the same test, in a step or in the health probe, or a card that does not come back ends it at stock with "optimize again", as the run's reset budget does. Every failed step's verdict goes to `run.log` once: in the log when the next step is tested (or a stop ends it), in the result lines when it was the last test. A step change is a new tune for the safety nets: it clears `boot_strikes` and the watchdog's reset history. |
Expand Down
Loading