Wie man eine Reduzierung des Stromverbrauchs und der Geräuschentwicklung einer AMD Radeon AI PRO R9700 erreicht. Es geht um Undervolting, Power Capping und Lüfter Drehzahl Regulierung – eine Balance zwischen Leistung, Stromverbrauch und Geräuschpegel. Die Ergebnisse zeigten eine deutliche Senkung des Stromverbrauchs von 300 Watt auf 210 Watt und eine Verringerung der Lüfterdrehzahl von 3269 U/min auf 2172 U/min bei gleichbleibender Leistung.
Seit Ende August hängt eine AMD Radeon AI PRO R9700 per OCuLink an meinem Proxmox-Node ist in meinem LXC-Container die Haupt GPU für llama.cpp. Sie ist schnell, aber bei maximaler Last auch laut: unter Dauerlast geht der Lüfter auf 3269 rpm, die Junction-Temperatur ging auf 97 °C, und die Karte zog dabei ihre vollen 300 Watt. Das entspricht in Deutschland bei einem preis von etwa 28ct/kWh ≈ 8,4ct
Das erste Mal merke ich also, das ein Prompt eben nicht nur Zeit braucht, sondern auch Geräusche verursacht – immerhin ist meine GPU nicht mehr im Rechenzentrum, sondern nur ein Raum weiter. Also wollte ich wissen, wie leise die GPU werden kann, bevor sie spürbar Leistung verliert.
TLDR: 2172 rpm statt 3269 rpm, 87 °C statt 97 °C , 210 Watt statt 300 Watt (5,88 ct statt 8,4 ct). Die Token-Generierung verlor dabei 1,2 Prozent. Der Weg dorthin ist interessanter als die Zahlen, weil die naheliegende Vermutung falsch war.
Warum man beim Undervolten einen Hash braucht
Der wichtigste Teil kommt vor der ersten Messung, und er hat mit Lautstärke nichts zu tun.
Wenn eine übertaktete Gaming-Karte instabil wird, stürzt der Treiber ab oder das Bild flackert. Man merkt sowas natürlich ganz schnell. Bei Compute-Last unter einem zu starken Undervolt sieht der Fehler nicht so offensichtlich aus: Die Karte rechnet weiter, sie rechnet nur falsch. Sie stürzt nicht ab und schreibt auch nichts ins Log. Ein LLM antwortet dann etwas anderes, und das fällt nicht so wirklich auf.
Dagegen hilft Greedy Decoding. Mit temperature 0, top_k 1, festem Seed und abgeschaltetem Prompt-Cache ist die Ausgabe des Modells bitgenau reproduzierbar. An jede Messung hängte ich den SHA-256 der Antwort:
r=$(curl -s -H "Content-Type: application/json" -d @req.json http://127.0.0.1:8080/completion)
h=$(printf "%s" "$r" | jq -r ".content" | sha256sum | cut -c1-16)Bleibt der Hash über alle Läufe gleich, hat die Karte identisch gerechnet. Weicht er ab, ist Schluss. Ohne diesen Test ist ein Undervolt nicht validierbar, sondern nur unbemerkt.
Die Baseline lief über 55 Iterationen bei Werkseinstellungen durch, alle 55 mit demselben Hash 8402b35b0a52413d. Damit stand fest, dass der Canary (piep piep) funktioniert.
Der Messaufbau
Zwei verschiedene Punkte, weil GPU Daten auf dem Host liegen, die Last aber in einem LXC Container (LX Container?).
Auf meinem Proxmox node schreibt /usr/local/bin/gpu-sample.sh im Zwei-Sekunden-Takt eine CSV mit Leistung, Cap, Shader- und Speichertakt, Spannung, Lüfterdrehzahl, drei Temperaturen, Auslastung und VRAM. /usr/local/bin/gpu-summary.sh wertet aus und verwirft dabei zwei Dinge: die ersten 300 Sekunden, weil die Karte thermisch noch warm läuft, und alle Zeilen mit weniger als 50 Prozent Auslastung (Leerlaufphase).
Im Container liegt unter /root/gpu-tuning/ der Rest. Der Prompt besteht aus 200 gleich gebauten Zeilen, 29 692 Bytes, exakt 6092 Token. Die feste Länge ist wichtig, sonst ist der Prefill zwischen zwei Läufen nicht vergleichbar. measure.sh feuert n Iterationen gegen den laufenden llama-Server und protokolliert Durchsatz, Draft-Acceptance und Hash.
Gemessen wird bewusst über HTTP gegen den bereits geladenen Server, nicht per llama-cli. Das 24-GB-Modell (ich habe mit einem Qwen3.8-27B-UD-Q6_K_L getestet) braucht neun Minuten zum Laden; gegen den Server dauert eine Iteration 21 Sekunden. Wichtiger noch: So misst man die echte Produktionskonfiguration mit MTP-Draft und q8_0-KV-Cache statt eines Laborsetups.
Was ein niedrigerer Power-Cap wirklich bringt
power1_cap lässt sich im laufenden Betrieb schreiben, ohne Reboot, ohne Freischaltung. Untergrenze sind 210 Watt, Werkseinestellung 300. Ich habe 260, 240 und 210 Watt getestet, je 32 Iterationen, Telemetrie nebenher.
| Cap W | Prefill t/s | Δ % | Decode t/s | Δ % | Lüfter ⌀/max U/min | Junction ⌀/max °C | sclk Hz | vddgfx mV |
|---|---|---|---|---|---|---|---|---|
| 300 | 378,1 | – | 51,89 | – | 3147 / 3269 | 92,1 / 97 | 2846 | 948 |
| 260 | 362,8 | −4,0 | 51,87 | −0,0 | 2668 / 2744 | 88,3 / 92 | 2678 | 898 |
| 240 | 352,3 | −6,8 | 51,60 | −0,6 | 2444 / 2526 | 86,3 / 91 | 2589 | 864 |
| 210 | 335,8 | −11,2 | 50,91 | −1,9 | 2132 / 2185 | 84,1 / 87 | 2422 | 819 |
151 Iterationen über alle vier Blöcke, durchgehend derselbe Hash, Draft-Acceptance immer bei 100 Prozent.
Aus dieser Tabelle kommen vier Erkenntnisse – drei davon etwas unerwartet.
Token-Generierung interessiert sich kaum für das Leistungsbudget. Bei 260 Watt ist der Verlust nicht messbar, bei 210 Watt sind es 1,9 Prozent. Decode hängt an der Speicherbandbreite, und der Speichertakt blieb über den gesamten Sweep bei 1258 MHz stehen. Nur Prefill büst für den Cap, weil Prefill rechnet.
Kühler wird die Karte dabei kaum aber leiser. Junction fällt von 92 auf 84 Grad, die Drehzahl von 3147 rpm auf 2132 rpm. Die SMU hält eine Zieltemperatur und steckt gespartes Budget in weniger Drehzahl, nicht in weniger Wärme. Später fand ich die Bestätigung im sysfs: FAN_TARGET_TEMPERATURE steht ab Werk auf 85 °C, und bei 210 Watt maß ich 84,1.
Die Speichertemperatur rührt sich überhaupt nicht: rund 81 Grad, bei jedem Cap. GDDR6-Abwärme skaliert mit Bandbreite, nicht mit GFX-Leistung. Weder Power-Cap noch Undervolt ändern daran etwas, und mit 108 Grad als kritischem Wert ist das unproblematisch. Nur eine bessere Kühlung wäre da wohl interessant.
Und für ein Lüfterziel von 2800 rpm braucht es gar keinen Lüfter-Regler. Schon 260 Watt halten die Karte unter Dauerlast bei 2744 Umdrehungen. Das war der Punkt, an dem die ursprüngliche Idee positiv veränderte: Ich wollte den Lüfter deckeln und stellte fest, dass der Power-Cap das Problem bereits gelöst hatte.
Nebenbei: Die Effizienz im Prefill steigt von 1,27 auf 1,60 Token pro Sekunde und Watt, ein Plus von 26 Prozent.
OverDrive freischalten
Undervolt und eine harte Drehzahllimitierung hängen beide am selben Gate. Sie existieren im sysfs nur, wenn amdgpu OverDrive aktiv ist, und das schaltet ausschließlich Bit 14 der Kernel-Cmdline frei:
amdgpu.ppfeaturemask=0xfff7ffffDas Modul lässt sich auf einem Proxmox-Host nicht entladen, der Konsolen-Framebuffer hängt daran – Reboot ist also notwendig.
Hier lauert die erste echte Stolperstelle. Die Proxmox node bootet UEFI über proxmox-boot-tool, aber mit GRUB. Die wirksame Kernel-Cmdline liegt damit in /etc/default/grub, nicht in /etc/kernel/cmdline. Meine anderen Nodes machen es andersherum. proxmox-boot-tool status sagt, welcher Pfad in diesem Fall richtig ist.
Die zweite Unbekannte war, ob es überhaupt funktioniert. Die R9700 ist eine Pro-Karte, und deren PPTable könnte OverDrive firmwareseitig sperren. In dem Fall wäre der Power-Cap das Ende der Fahnenstange gewesen. Dieser Befehl entscheidet das:
cat /sys/module/amdgpu/parameters/ppfeaturemask
ls /sys/class/drm/card*/device/gpu_od/Sie sperrt nicht. gpu_od/fan_ctrl/ erschien vollständig, und OD_RANGE erlaubt einen Spannungs-Offset bis −200 mV sowie einen Taktoffset von −500 bis +1000 MHz.
Die Undervolt-Leiter
pp_od_clk_voltage nimmt einen Offset in Millivolt entgegen, gefolgt von einem Commit:
echo "vo -50" > /sys/bus/pci/devices/0000:03:00.0/pp_od_clk_voltage
echo "c" > /sys/bus/pci/devices/0000:03:00.0/pp_od_clk_voltageAnders als in vielen Anleitungen behauptet, muss power_dpm_force_performance_level dafür nicht auf manual stehen. auto genügt.
Ich bin die Leiter 0, −25, −50, −65 und −80 mV abgefahren, wieder je 32 Iterationen, alles bei festem 210-Watt-Cap. Ein Fischer Reboot, damit der Vergleich nicht über den Neustart hinweg gezogen werden muss.
| Offset mV | Prefill t/s | Δ % | Decode t/s | sclk Hz | vddgfx mV | Junction ⌀/max °C | Lüfter ⌀/max U/min | Leistung W |
|---|---|---|---|---|---|---|---|---|
| 0 | 337,8 | – | 50,86 | 2429 | 820 | 82,2 / 86 | 2102 / 2129 | 210,0 |
| −25 | 341,4 | +1,1 | 51,07 | 2488 | 811 | 83,8 / 88 | 2115 / 2153 | 208,5 |
| −50 | 347,5 | +2,9 | 51,26 | 2541 | 803 | 83,7 / 87 | 2120 / 2172 | 208,7 |
| −65 | 351,3 | +4,0 | 51,41 | 2551 | 795 | 83,6 / 87 | 2123 / 2173 | 206,7 |
| −80 | 355,3 | +5,2 | 51,49 | 2586 | 794 | 83,7 / 88 | 2122 / 2167 | 207,5 |
160 Iterationen, 160 mal derselbe Hash, Draft-Acceptance durchgehend bei 100 Prozent, keine Ring-Timeouts, keine GPU-Resets, nichts im dmesg.
Das wichtigste Detail steckt in den beiden mittleren Spalten. Lüfterdrehzahl und Junction-Temperatur bleiben über die gesamte Leiter praktisch flach; an Lautstärke und Temperatur ändert der Undervolt nichts. Was sich ändert, ist der Takt, von 2429 auf 2586 MHz bei konstant 207 bis 210 Watt. Bei einem festen Power-Cap erscheint jede eingesparte Spannung als zusätzlicher Takt im selben Budget, nicht als kühlere Karte.
Das ist kein Nachteil, man muss es nur richtig herum erwarten. Für Laufruhe ist der Power-Cap zuständig. Der Undervolt holt einen Teil der Leistung zurück, die der Cap gekostet hat.
Warum −50 mV und nicht −80
Die Leiter fand keine Fehlergrenze. Auch −80 mV lief 32 von 32 sauber durch.
Meine Regel dafür lautet: eine volle Stufe unter dem tiefsten sauberen Wert. Rechnerisch wären das −55 mV. Genommen habe ich −50, weil das der nächstgelegene Wert ist, den ich tatsächlich gemessen habe.
Der Abstand ist absichtlich großzügig. Zwölf Minuten Last mit einem einzigen Workload beweisen keine Monatelange Stabilität. Der Fehlermodus ist stilles Falschrechnen unter einer ungünstigen Kombination aus Temperatur und Instruktionsmix, nicht der Absturz, den man bemerken würde. Kosten gegenüber −80 mV: 2,3 Prozentpunkte Prefill, 0,4 Punkte Decode. Dafür schlafe ich besser.
Ungenutzt bleibt übrigens OD_SCLK_OFFSET. Bei festem Power-Cap wäre das eine zweite Stellschraube in dieselbe Richtung.
Damit es den Reboot überlebt
Ein Reboot setzt alles zurück. Damit die Werte überleben, braucht es drei Dinge gleichzeitig.
Die Kernel-Cmdline schaltet OverDrive frei, sonst existieren zwei der drei Knöpfe gar nicht. Eine systemd-Oneshot-Unit schreibt Cap, Drehzahllimit und Offset bei jedem Boot neu, geordnet Before=pve-guests.service:
[Unit]
Description=Apply power, fan and voltage policy to Radeon AI PRO R9700
Before=pve-guests.service
ConditionPathIsDirectory=/sys/bus/pci/devices/0000:03:00.0
[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/usr/local/sbin/r9700-tune.shUnd drittens laufen sämtliche sysfs-Zugriffe über die PCI-Adresse, nie über /sys/class/hwmon/hwmonN. Das ist keine Stilfrage. Der hwmon-Index wanderte über zwei Reboots von hwmon6 auf hwmon7 und wieder zurück. Ein festgenagelter Pfad hätte beim ersten Neustart funktioniert, beim zweiten auch, und dazwischen still auf die falsche GPU gezeigt. Der Host hat eine iGPU, die dort ebenfalls auftaucht.
D=/sys/bus/pci/devices/0000:03:00.0
H=$(ls -d $D/hwmon/hwmon* | head -1)
echo 210000000 > "$H/power1_cap"Getestet hat sich das Ganze durch einen Unfall. Ich habe meinen Proxmox Node versehentlich neu gestartet, und weil Drehzahlbegrenzung und Undervolt bis dahin nur auf ein laufendes System geschrieben waren, war genau das der fehlende Punkt. Nach dem Boot standen ohne mein Zutun alle drei Werte wieder korrekt, der Run lieferte 12 von 12 mit demselben Hash wie alle 160 Sweep-Iterationen zuvor.
Was sich im Repo geändert hat
Ich habe alles über Ansible eingerichtet, damit der Zustand reproduzierbar bleibt, statt alles manuell zu trocken.
proxmox_node.yml hält die Werte:
ai1_gpu_power_cap_w: 210
ai1_gpu_fan_max_rpm: 2800
ai1_gpu_voltage_offset_mv: -50
ai1_amdgpu_ppfeaturemask: "0xfff7ffff"
ai1_gpu_bench_reference_hash: "8402b35b0a52413d"Der Referenz-Hash steht bewusst mit in der Konfiguration. Er ist der Sollwert, gegen den ein späterer Kontrolllauf prüft, und ohne ihn wäre die Messreihe nach ein paar Wochen wertlos.
Die Kurzfassung für dein GPU
Falls jemand das nachbauen will, in dieser Reihenfolge:
- Determinismus-Test aufsetzen und gegen Werkseinstellungen laufen lassen. Wenn der Hash schon dort schwankt, taugt die Methode für diese Last nicht, und der Rest ist Zeitverschwendung.
- Power-Cap absenken, in Stufen, mit Telemetrie. Das ist der Hebel für Lautstärke und Verbrauch, kostet kein OverDrive und ist sofort reversibel.
- Erst wenn das nicht reicht, OverDrive freischalten. Vorher prüfen, welche Cmdline-Datei der Bootloader tatsächlich liest.
- Undervolt als Leiter testen und mit Sicherheitsabstand versehen.
- Alles über die PCI-Adresse persistieren, vor dem Start der Node.
Unterm Strich, gemessen gegen den Auslieferungszustand: Der Lüfter ging von 3269 auf 2172 rpm Umdrehungen zurück, die Junction-Höchsttemperatur von 97 auf 87 Grad, die Leistungsaufnahme von 300 auf 210 Watt. Prefill kostet das 8,1 Prozent, Decode 1,2.
Das sind beim lesen natürlich jetzt nur Werte, aber die Lautstärke ist deutlich deutlich besser, bei nahezu gleicher Leistung und weniger Stromverbraucht. Win win win Situation.
