Chasing coil whine into the CPU idle states
One of the machines in my homelab is an MSI GS63VR, a thin gaming laptop from the Skylake era (i7-6700HQ, GTX 1060) that now runs Proxmox around the clock. Laptops make decent small servers: they idle low, they have a built-in UPS, and this one has a GPU. This one also had a habit I couldn’t ignore once I’d noticed it: an irregular ticking from somewhere under the keyboard, like a clock with a bad escapement.
What it was
It wasn’t the fans and it wasn’t a drive. It was coil whine from the voltage regulators feeding the CPU, and it was worst when the machine was doing almost nothing.
That’s the opposite of what most people expect from coil whine, but it makes sense once you think about where the current goes. Inductors and ceramic capacitors in a VRM physically flex a tiny amount as the current through them changes. If the current changes at an audible rate, you can hear it. Under steady load the current is high but fairly constant. At idle, a modern CPU is constantly dropping into deep C-states, power-gating cores and parts of the package, then waking up again for a timer or an interrupt. Each of those transitions is a sharp step in the load the VRM has to supply.
And idle wakeups aren’t periodic. They come from whatever timers and interrupts happen to be pending, so the steps land at irregular intervals. That’s the arrhythmic part: the ticking was the VRM reacting to the CPU’s sleep pattern.
Looking at the idle states
Linux exposes everything about CPU idle in sysfs. The governor decides which state to enter each time a CPU goes idle:
cat /sys/devices/system/cpu/cpuidle/current_driver # intel_idle
cat /sys/devices/system/cpu/cpuidle/current_governor
cat /sys/devices/system/cpu/cpuidle/available_governors
Each CPU lists the states it can use, with names, exit latencies, and how often each one has been entered:
for s in /sys/devices/system/cpu/cpu0/cpuidle/state*; do
printf '%-8s latency=%-5s usage=%s\n' "$(cat $s/name)" "$(cat $s/latency)" "$(cat $s/usage)"
done
Every state also has a disable file. Writing 1 to it stops the governor from choosing that state
on that CPU, at runtime, with no reboot.
The fix
Two changes together made the ticking stop:
Switch the governor to
teo. The timer events oriented governor bases its choice on upcoming timer events and how well recent idle periods matched its predictions. Changing the governor is a single write:echo teo > /sys/devices/system/cpu/cpuidle/current_governorDisable the deep states. On this CPU that meant C3, C6, C7s, and C10. With those off, cores only drop into the shallow states, so the load steps the VRM sees are much smaller.
The cost
Deep C-states are where the idle power savings come from, so this isn’t free. With them disabled the machine idles 10 to 15 W higher. For a box that runs 24/7, that’s real money and heat over a year.
So I didn’t make it permanent. Instead of a boot-time service, it’s a small script I run when I want the machine quiet and turn off again afterwards:
#!/bin/sh
# quiet-idle: trade 10-15 W of idle power for no coil whine.
# usage: quiet-idle on | off | status
set -eu
CPUIDLE=/sys/devices/system/cpu/cpuidle
STATES="C3 C6 C7s C10" # check names in .../cpuidle/state*/name on your CPU
set_states() {
for d in /sys/devices/system/cpu/cpu[0-9]*/cpuidle/state[0-9]*; do
name=$(cat "$d/name")
for s in $STATES; do
[ "$name" = "$s" ] && echo "$1" > "$d/disable"
done
done
}
case "${1:-status}" in
on) echo teo > "$CPUIDLE/current_governor"; set_states 1 ;;
off) echo menu > "$CPUIDLE/current_governor"; set_states 0 ;;
status) ;;
*) echo "usage: $0 on|off|status" >&2; exit 2 ;;
esac
echo "governor: $(cat $CPUIDLE/current_governor)"
for d in /sys/devices/system/cpu/cpu0/cpuidle/state[0-9]*; do
printf ' %-6s disabled=%s\n' "$(cat $d/name)" "$(cat $d/disable)"
done
menu is the default governor on most kernels; check current_governor before the first run and
put back whatever yours started with.
Takeaways
- Coil whine that gets worse at idle is a power-management symptom, not a load symptom. Look at C-states before you look at the hardware.
- Everything here is adjustable at runtime through sysfs, which makes it quick to experiment: change one thing, listen, change it back.
- Measure what the fix costs. Silence was worth it for some of the day, but not for every hour of it.