From 3266aad4a212479a711cc122b3aea3027ee72d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ryan4yin=20=7C=20=E4=BA=8C=E8=8A=B1?= Date: Fri, 16 Oct 2026 16:45:08 +0800 Subject: [PATCH] fix(power): drop TuneD for native CPU scaling (#461) --- hosts/12kingdoms-shoukei/default.nix | 5 --- hosts/README.md | 12 +++++++ hosts/idols-ai/default.nix | 4 --- hosts/idols-aquamarine/default.nix | 1 + lib/genVmHostModule.nix | 7 +--- modules/nixos/desktop/default.nix | 5 +++ modules/nixos/power.nix | 32 +++-------------- outputs/README.md | 8 ++--- .../tests/cpu-power/expected.nix | 6 ++++ .../aarch64-linux/tests/cpu-power/expr.nix | 17 +++++++++ .../x86_64-linux/tests/cpu-power/expected.nix | 12 +++++++ outputs/x86_64-linux/tests/cpu-power/expr.nix | 35 +++++++++++++++++++ 12 files changed, 98 insertions(+), 46 deletions(-) create mode 100644 outputs/aarch64-linux/tests/cpu-power/expected.nix create mode 100644 outputs/aarch64-linux/tests/cpu-power/expr.nix create mode 100644 outputs/x86_64-linux/tests/cpu-power/expected.nix create mode 100644 outputs/x86_64-linux/tests/cpu-power/expr.nix diff --git a/hosts/12kingdoms-shoukei/default.nix b/hosts/12kingdoms-shoukei/default.nix index 834bc138..c5a3bac2 100644 --- a/hosts/12kingdoms-shoukei/default.nix +++ b/hosts/12kingdoms-shoukei/default.nix @@ -21,11 +21,6 @@ in # disable sunshine for security services.sunshine.enable = lib.mkForce false; - # battery-first: pin the low-power profile. apple-cpufreq exposes no EPP or - # platform_profile, so the only thing this changes is cpufreq boost. - services.tuned.recommend = { - powersave = { }; - }; # Laptop joins untrusted networks and is never scraped (see youko's # `offlineHosts`); don't expose :9100, and don't keep the smartctl exporter diff --git a/hosts/README.md b/hosts/README.md index b54aea76..96ae6b45 100644 --- a/hosts/README.md +++ b/hosts/README.md @@ -104,6 +104,18 @@ Copy the closest existing host and adapt it. The step-by-step procedure, includi networking wiring, secrets, and the eval tests a new host must pass, is in [`.agents/skills/nix-config-new-host/SKILL.md`](../.agents/skills/nix-config-new-host/SKILL.md). +## CPU Power Policy + +Physical hosts get `powersave` from [`power.nix`](../modules/nixos/power.nix); Apple hosts keep +their own `schedutil`. On the active Intel/AMD P-state drivers `powersave` still scales with load, +so this is not a frequency cap. + +TuneD is not enabled on any host. It tunes much more than CPU frequency and fits laptops, but our +laptop is a MacBook with its own native support, so it has no use here. + +The desktops also run UPower for power-supply reporting: the laptop's battery, and peripheral +batteries on a desktop. It only reports state, and changes no CPU or device setting. + ## Deploying VM Hosts The three VM hosts (`shoryu`, `shushou`, `youko`) put their VMs on the Linux bridge `br0`, with the diff --git a/hosts/idols-ai/default.nix b/hosts/idols-ai/default.nix index fd0bfeda..c448c283 100644 --- a/hosts/idols-ai/default.nix +++ b/hosts/idols-ai/default.nix @@ -58,10 +58,6 @@ in }; services.sunshine.enable = true; - # desktop PC: keep the CPU pinned to the performance profile - services.tuned.recommend = { - throughput-performance = { }; - }; modules.btrbk.enable = true; diff --git a/hosts/idols-aquamarine/default.nix b/hosts/idols-aquamarine/default.nix index de04a2c5..fe1962ef 100644 --- a/hosts/idols-aquamarine/default.nix +++ b/hosts/idols-aquamarine/default.nix @@ -20,6 +20,7 @@ in { imports = [ disko.nixosModules.default + ../../modules/nixos/power.nix # disks ./disko-fs.nix diff --git a/lib/genVmHostModule.nix b/lib/genVmHostModule.nix index d852c451..197439e5 100644 --- a/lib/genVmHostModule.nix +++ b/lib/genVmHostModule.nix @@ -10,13 +10,8 @@ let ipv4WithMask = "${ipv4}/24"; in { - # Physical machines only: these three always-on mini PCs hold their CPU at - # TuneD's power-saving profile (EPP biased to power, cpufreq boost off). A - # MicroVM guest has no EPP or boost of its own, so it never imports this module. + # CPU policy belongs to physical hosts; guests do not control hardware frequency. imports = [ ../modules/nixos/power.nix ]; - services.tuned.recommend = { - powersave = { }; - }; # supported file systems, so we can mount any removable disks with these filesystems boot.supportedFilesystems = [ diff --git a/modules/nixos/desktop/default.nix b/modules/nixos/desktop/default.nix index e03af77a..e4906b58 100644 --- a/modules/nixos/desktop/default.nix +++ b/modules/nixos/desktop/default.nix @@ -1,4 +1,9 @@ { mylib, ... }: { imports = mylib.scanPaths ./. ++ [ ../power.nix ]; + + # Power-supply reporting for the desktop shell: the laptop's battery, and + # peripheral batteries on a desktop. UPower only reports state; unlike TuneD it + # writes no CPU, network, or disk setting. + services.upower.enable = true; } diff --git a/modules/nixos/power.nix b/modules/nixos/power.nix index 342fe205..9d75cc86 100644 --- a/modules/nixos/power.nix +++ b/modules/nixos/power.nix @@ -1,29 +1,7 @@ -{ config, lib, ... }: +{ lib, pkgs, ... }: { - # TuneD - Tuning Profile Delivery Mechanism for Linux - # A modern replacement for PPD(power-profiles-daemon), and the only power manager - # here: nixpkgs' tuned module already disables power-profiles-daemon and fails - # the build if TLP, auto-cpufreq or system76-power is enabled as well. - services.tuned = { - enable = true; - settings.dynamic_tuning = true; - - # Hosts pin their profile with `services.tuned.recommend` (an empty rule - # matches any host): TuneD reads /etc/tuned/recommend.conf in its automatic - # profile mode on every boot. `ppdSettings.main.default` looks like it does - # the same, but never wins -- tuned-ppd restores the base profile it saved in - # /etc/tuned/ppd_base_profile, or TuneD's own recommendation, first. Nothing - # here uses the PPD D-Bus API, so tuned-ppd stays off instead of overwriting - # the pinned profile at startup. - ppdSupport = false; - }; - - # Battery/AC state, used by the desktops; tuned-ppd was the other consumer. - services.upower.enable = true; - - # ppdSupport = false drops /etc/tuned/ppd.conf, but the tuned package ships its - # own tuned-ppd.service (WantedBy=graphical.target) which systemd.packages links - # regardless, so activation starts it, it fails on the missing config file and - # switch-to-configuration exits 4. Mask it. - systemd.units."tuned-ppd.service".enable = lib.mkIf (!config.services.tuned.ppdSupport) false; + # These x86 hosts use active Intel/AMD P-state drivers: powersave permits + # hardware-managed scaling. Apple Silicon keeps its native schedutil policy; + # generic cpufreq powersave would instead hold the CPU at its minimum. + powerManagement.cpuFreqGovernor = lib.mkIf pkgs.stdenv.hostPlatform.isx86 "powersave"; } diff --git a/outputs/README.md b/outputs/README.md index e714a4e5..df12aaf0 100644 --- a/outputs/README.md +++ b/outputs/README.md @@ -47,10 +47,10 @@ Some tests iterate over every configuration, so a new host is covered automatica them. Others name specific hosts — or a name prefix, like `k3s-api-endpoint`, which covers every `k3s-test-1-*` configuration — and only cover a new host if you add it. -| Kind | Tests | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Every configuration | `hostname`, `kernel`, `nix-system-features`, `security-apparmor`, `security-container-groups`, `security-exporters`, `security-firewall`, `security-k3s-kubeconfig`, `security-k3s-pod-firewall`, `security-kernel`, `security-ssh-x11` | -| Named hosts only | `btrbk`, `home-manager`, `home-manager-xdg`, `computer-use-headless`, `k3s-api-endpoint`, `k3s-node-home-manager`, `idols-ai-gpu`, `idols-ai-net`, `idols-ai-voice-input`, `idols-aquamarine`, `security-postgres-auth`, `youko-metrics`, `ups-metrics`, `shoukei-logind` | +| Kind | Tests | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Every configuration | `hostname`, `kernel`, `nix-system-features`, `security-apparmor`, `security-container-groups`, `security-exporters`, `security-firewall`, `security-k3s-kubeconfig`, `security-k3s-pod-firewall`, `security-kernel`, `security-ssh-x11` | +| Named hosts only | `btrbk`, `cpu-power`, `home-manager`, `home-manager-xdg`, `computer-use-headless`, `k3s-api-endpoint`, `k3s-node-home-manager`, `idols-ai-gpu`, `idols-ai-net`, `idols-ai-voice-input`, `idols-aquamarine`, `security-postgres-auth`, `youko-metrics`, `ups-metrics`, `shoukei-logind` | `hostname` expects each configuration's `networking.hostName` to equal its name. Niri desktop configurations are the exception (`ai-niri` → `ai`, `shoukei-niri` → `shoukei`) and are listed in diff --git a/outputs/aarch64-linux/tests/cpu-power/expected.nix b/outputs/aarch64-linux/tests/cpu-power/expected.nix new file mode 100644 index 00000000..c6088692 --- /dev/null +++ b/outputs/aarch64-linux/tests/cpu-power/expected.nix @@ -0,0 +1,6 @@ +{ + governor = "schedutil"; + noTuned = true; + noFrequencyCaps = true; + batteryReporting = true; +} diff --git a/outputs/aarch64-linux/tests/cpu-power/expr.nix b/outputs/aarch64-linux/tests/cpu-power/expr.nix new file mode 100644 index 00000000..05b8b652 --- /dev/null +++ b/outputs/aarch64-linux/tests/cpu-power/expr.nix @@ -0,0 +1,17 @@ +{ + lib, + outputs, + ... +}: +let + cfg = outputs.nixosConfigurations.shoukei-niri.config; +in +{ + governor = cfg.powerManagement.cpuFreqGovernor; + noTuned = lib.all (host: !host.config.services.tuned.enable) ( + builtins.attrValues outputs.nixosConfigurations + ); + noFrequencyCaps = + cfg.powerManagement.cpufreq.min == null && cfg.powerManagement.cpufreq.max == null; + batteryReporting = cfg.services.upower.enable; +} diff --git a/outputs/x86_64-linux/tests/cpu-power/expected.nix b/outputs/x86_64-linux/tests/cpu-power/expected.nix new file mode 100644 index 00000000..e8ebcd19 --- /dev/null +++ b/outputs/x86_64-linux/tests/cpu-power/expected.nix @@ -0,0 +1,12 @@ +{ + lib, + ... +}: +{ + governors = lib.genAttrs [ "ai-niri" "aquamarine" "shoryu" "shushou" "youko" ] (_: "powersave"); + noTuned = true; + noEeeWorkaround = true; + noFrequencyCaps = true; + guestsHaveNoCpuPolicy = true; + desktopBatteryReporting = true; +} diff --git a/outputs/x86_64-linux/tests/cpu-power/expr.nix b/outputs/x86_64-linux/tests/cpu-power/expr.nix new file mode 100644 index 00000000..1e66de4a --- /dev/null +++ b/outputs/x86_64-linux/tests/cpu-power/expr.nix @@ -0,0 +1,35 @@ +{ + lib, + outputs, + ... +}: +let + configs = outputs.nixosConfigurations; + physicalHosts = [ + "ai-niri" + "aquamarine" + "shoryu" + "shushou" + "youko" + ]; +in +{ + governors = lib.genAttrs physicalHosts ( + name: configs.${name}.config.powerManagement.cpuFreqGovernor + ); + noTuned = lib.all (host: !host.config.services.tuned.enable) (builtins.attrValues configs); + noEeeWorkaround = lib.all (host: !(host.config.systemd.services ? disable-eee)) ( + builtins.attrValues configs + ); + noFrequencyCaps = lib.all ( + name: + let + cfg = configs.${name}.config.powerManagement; + in + cfg.cpufreq.min == null && cfg.cpufreq.max == null + ) physicalHosts; + guestsHaveNoCpuPolicy = lib.all (name: !(configs.${name}.config.systemd.services ? cpufreq)) ( + lib.subtractLists physicalHosts (builtins.attrNames configs) + ); + desktopBatteryReporting = configs.ai-niri.config.services.upower.enable; +}