Controlling the fan curve of an AMD GPU on Pop!_OS (or other Ubuntu-like operating systems)

Around this time last year, I put together a small computer for playing games on my living room TV. I snagged a Sapphire Vega 56 Pulse GPU for a really good price on ebay, combined it with a mid-range i5 CPU and a few other bits, and I was laughing. The computer runs the Ubuntu-like operating system Pop!_OS.

Out of the box, my Vega 56 had a fairly unusual fan curve. At low loads, it wouldn’t run the fans full time, even at a slow speed – instead, it would intermittently pulse (ha!) the fans, for a few seconds at a time, then stop for a little while.

Sometimes, when playing games complex enough to work the GPU a bit, but not enough to trigger an always-on fan, the GPU would warm up to the point of—I assume—hitting some sort of internal safety mechanism, and cut off video output. Which was not ideal.

I figured it was time to set a proper fan curve on the GPU, so that the fans were always spinning, albeit slowly, even at idling loads, so that the temperatures never got a chance to creep up.

Around the same time, someone on the AMD Reddit forums was sharing a new program they’d written, called CoreCtrl, which was meant to make monitoring and controlling your GPU and CPU stats really easy. Kind of like an open source equivalent to AMD’s Windows-only WattMan.

One of the things CoreCtrl lets you do is set a custom fan curve for your GPU, on a nice point-and-click line graph. Awesome!

The downside, however, is that CoreCtrl has to be running to control that curve. I spent a while with CoreCtrl set as a startup application, but it was annoying having to close the window each time my computer finished starting up.

I figured there had to be a better way.

Turns out there is.


How to control AMD GPUs on Linux

By default, on Pop!_OS (and, I assume, other low-config Ubuntu-like operating systems), if you’ve got an AMD GPU, you’ll be using AMD’s open source amdgpu driver.

Following Unix’s “everything is a file” ideology, the amdgpu driver exposes a bunch of monitoring and control endpoints via special files at /sys/class/drm/card0/device/.

You can read a file like /sys/class/drm/card0/device/hwmon/hwmon0/temp1_input to find out the GPU’s current temperature (in millidegrees Celsius, eg: 51000 for 51°C), and you can write a number to a file like /sys/class/drm/card0/device/hwmon/hwmon0/pwm1 to set the fan speed (as an 8-bit binary number, so 0 for fans completely off, up to 255 for fans completely on).

What the amdgpu driver doesn’t expose is a file to set a series of temperatures/fan speeds on a curve. So, if you want to change the fan speed based on the GPU’s temperature, you have to write a script that runs in the background and monitors and sets the speed, automatically.

Yeesh, hard work.

Thankfully, however, lots of other people have already done this work for you. Yay Open Source!

There’s this one in Python. And another in bash.

I’m a sucker for a well written bash script, so I went with that one.


Setting up amdgpu-fancontrol

I took a look at my CoreCtrl config, and jotted down the parameters of the fan curve it had been setting:

Temperature Fan speed
35°C 20%
52°C 22%
67°C 30%
78°C 50%
85°C 82%

I figured now was an opportunity to get the cool end of that curve as quiet as possible, so I experimented a bit to see how low I could set the fan speed without it stopping completely. It turned out a speed of about 17% did the job.

Converting to millidegrees Celcius and an 8-bit binary PWM value, I got:

Temperature Fan PWM
35000 45
52000 56
67000 76
78000 128
85000 210

I knew I’d be setting systemd to run the ampgpu-fancontrol as root when the computer starts, so I made a decision not to put any of the ampgpu-fancontrol files in my user’s home directory – just in case, you know, I eventually create a second user on the machine, and want them to enjoy a non-crashing GPU too.

There are loads of places you could clone the amdgpu-fancontrol to, outside of your home directory. I picked /usr/local/src.1

First step – clone the repo:

cd /usr/local/src/
sudo git clone https://github.com/grmat/amdgpu-fancontrol.git

Then I wrote my fan speed values into a config file:2

echo 'TEMPS=( 35000 52000 67000 78000 85000 )' | sudo tee /usr/local/src/amdgpu-fancontrol/amdgpu-fancontrol.cfg > /dev/null
echo 'PWMS=( 45 56 76 128 210 )' | sudo tee --append /usr/local/src/amdgpu-fancontrol/amdgpu-fancontrol.cfg > /dev/null

Then I symlink that config file to the place that amdgpu-fancontrol expects to find it:

sudo ln -s /usr/local/src/amdgpu-fancontrol/amdgpu-fancontrol.cfg /etc/amdgpu-fancontrol.cfg

(Reminder: ln -s works just like cp – the original file goes first, and the new file you want to create goes second.)

I wanted to use the amdgpu-fancontrol.service file that came with the script, so I needed to also symlink the amdgpu-fancontrol script into /usr/bin, which has the added benefit of also putting the script on my PATH if I ever want to run it manually:

sudo ln -s /usr/local/src/amdgpu-fancontrol/amdgpu-fancontrol /usr/bin/amdgpu-fancontrol

Finally, I symlink the service file into place, and tell systemd to enable it (for the next boot) and also start it immediately:

sudo ln -s /usr/local/src/amdgpu-fancontrol/amdgpu-fancontrol.service /etc/systemd/system/amdgpu-fancontrol.service
sudo systemctl enable amdgpu-fancontrol.service
sudo systemctl start amdgpu-fancontrol.service

My GPU’s fans started purring, at their most whisper-quiet setting, so I knew my script was working its magic.

Running a very basic GPU stress test in one window:

vblank_mode=0 glxgears

And monitoring the output of the amdgpu-fancontrol service in another:

sudo journalctl --follow -u amdgpu-fancontrol.service

I could see amdgpu-fancontrol correctly updating the fan speeds as the temperature rises, and then backing them down once the stress test ended.

A quick restart, and I’d verified that my systemd service was starting automatically on boot. Job done!


If you want to simplify running most of the commands above, Manuel Grießmayr kindly bundled them all into a Makefile that you can run with make install, make update, and make uninstall. Twitter messed up the indentation, so here it is in full:

.PHONY: install update uninstall

install:
	cp amdgpu-fancontrol.cfg /etc
	cp amdgpu-fancontrol /usr/bin
	cp amdgpu-fancontrol.service /etc/systemd/system
	systemctl enable amdgpu-fancontrol
	systemctl start amdgpu-fancontrol

update:
	cp amdgpu-fancontrol.cfg /etc
	systemctl restart amdgpu-fancontrol

uninstall:
	rm /etc/amdgpu-fancontrol.cfg
	rm /usr/bin/amdgpu-fancontrol
	systemctl stop amdgpu-fancontrol
	systemctl disable amdgpu-fancontrol
	rm /etc/systemd/system/amdgpu-fancontrol.service

If you want to find out more about how all of the amdgpu stuff works, the Arch Linux wiki is a treasure trove of really high-quality information:

If you want to learn more about systemd files, this DigitalOcean tutorial is really well written:

  1. Because /usr is owned by root, I have to use sudo a lot here. I guess I could have activated a root shell with sudo su, but I prefer staying in my regular shell. 

  2. Note the use of sudo tee here, to append output to a write-protected file. The first time, I use it without --append, to replace the entire content of the file (in case it already exists). The second time, I --append to just add the second line onto the end of the file.