Skip to content

fix: protect the hardware when a fan speed stops making sense - #19

Merged
ricky-chaoju merged 1 commit into
mainfrom
fix/hardware-wear
Sep 5, 2026
Merged

fix: protect the hardware when a fan speed stops making sense#19
ricky-chaoju merged 1 commit into
mainfrom
fix/hardware-wear

Conversation

@ricky-chaoju

@ricky-chaoju ricky-chaoju commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

I went looking for anything that could shorten the life of the card, and found three ways the fans can end up too slow for the temperature.

A curve holds its last point's speed at everything above it, and manual mode never looks at temperature at all, so nvfd 0 manual 30 keeps a loaded card at 30% forever. The daemon now pushes the speed up to 100% — never down — once the GPU gets within a few degrees of throttling itself. The threshold comes from the driver rather than being hardcoded; on a 5090 that lands at 87 °C. It backs off 5 °C lower so it can't chatter at the boundary.

A falling curve like {"30":100,"80":30} used to load fine and make the fan slow down as the card heated up, which feeds back on itself. Saving one is now refused, from the CLI and the editor. An existing one still loads — a daemon that won't start is worse than a bad curve — but it gets logged once per start.

NVML's manual fan policy outlives the process that set it, and #17 only covers the exits the daemon can see. SIGKILL, the OOM killer, a segfault, or the start limit giving up would all leave a GPU pinned until the next reboot. So there's now a nvfd reset-fans verb wired to ExecStopPost=, which systemd runs however the process died. It only touches the fans — nvfd auto would rewrite the saved per-GPU modes, which isn't what you want after a crash. WatchdogSignal=SIGTERM also makes a watchdog kill go through the normal shutdown path instead of SIGABRT, which nothing catches.

Two smaller ones while I was in there. The clamp warning was logging every poll, about 17k journal lines a day for one bad config — now once per distinct value. And utils/nvfd-fan-control.sh only trapped INT and TERM, so a set -e abort (an nvidia-smi hiccup during a driver reset, say) skipped cleanup and left every GPU in curve mode with nothing supervising it; it also deleted the lock file it was still holding, which let a second copy run alongside it.

Tested in an Ubuntu 24.04 / systemd 255 container with the real device nodes against a 5090: the failsafe arms at 87 °C, reset-fans exits 0 and leaves config.json byte-identical, kill -9 on the main PID triggers the ExecStopPost reset, nvfd curve 40 20 is refused with the reason, an existing falling curve still starts the daemon and warns once, and Type=notify, the watchdog, systemctl reload and a clean stop all behave as before. make check covers the new failsafe logic.

One thing I deliberately left out. With the driver in charge this card sits at 0% fan at 35 °C; under curve or manual mode FAN_SPEED_MIN=30 means it never drops below 30%, because the floor is applied right at the NVML call. On a box that mostly idles that's something like 8,700 extra fan-hours a year. Restoring the zero-RPM window means handing the fans back to the driver below a threshold and reclaiming them above it, which is a behaviour change rather than a fix — happy to do it separately if you want it.

An audit of everything nvfd can leave the GPU doing turned up three ways
the fans can end up too slow for the die temperature, none of which any
existing code path notices.

Thermal failsafe. A curve holds its last point's speed at every
temperature above it, and manual mode never reads temperature at all, so
`nvfd 0 manual 30` pins a loaded card at 30% forever. Before every write
the daemon now raises - never lowers - the computed speed to 100% once
the GPU reaches a few degrees below its own throttle point, read from
the driver via nvmlDeviceGetTemperatureThreshold (87 C on an RTX 5090:
GPU_MAX 90 less a 3 C margin). It releases 5 C lower so it cannot
chatter at the boundary, and both edges are logged. The decision itself
is fan_failsafe_speed() in speed.c with unit tests; main.c keeps the
probing and the logging.

Falling curves. curve_load validated ranges and duplicates but never
shape, so {"30":100,"80":30} loaded clean and made the fan slow down as
the die heated - positive feedback that runs away to the hardware's own
throttle point. curve_write now refuses to save a curve that falls, from
the CLI and the editor alike. An existing one still loads, because a
daemon that will not start is worse than a badly shaped curve, but it is
logged once per start.

Fans after an unclean exit. NVML manual fan policy outlives the process
that set it. The daemon resets on every exit it can see, but not on
SIGKILL, the OOM killer or a segfault, and not once StartLimitBurst
gives up - so a GPU could sit pinned at 30% until the next reboot. Adds
a fans-only `nvfd reset-fans` verb and wires it to ExecStopPost=, which
systemd runs however the process died. `nvfd auto` is not usable there:
it would rewrite the operator's saved modes. WatchdogSignal=SIGTERM
likewise makes a watchdog kill land on the clean-shutdown path.

Also, three smaller things the audit turned up:

- manual_speed() logged its clamp warning every poll, 17k journal lines
  a day for one misconfigured GPU. It now warns once per distinct value.
- utils/nvfd-fan-control.sh trapped only INT and TERM, so a `set -e`
  abort - an unguarded nvidia-smi during a driver reset, say - skipped
  cleanup and left every GPU in curve mode with no supervisor. It now
  traps EXIT too, guards the temperature read, and no longer unlinks the
  lock file it still holds, which had let a second instance take a fresh
  inode and run concurrently.
@ricky-chaoju
ricky-chaoju merged commit fb36a37 into main Sep 5, 2026
2 checks passed
@ricky-chaoju
ricky-chaoju deleted the fix/hardware-wear branch September 5, 2026 09:07
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