Files
MontaukOS/docs/power.md
T

5.0 KiB

CPU Power and Thermal Management

MontaukOS manages CPU frequency, idle states, and package temperature on Intel hardware. Without this, a modern many-core laptop part (the reference target is a TUXEDO Gemini Gen2 with an i9-13900HX, 24 cores / 32 threads) runs at whatever P-state the firmware left it, never drops below C1 when idle, and heats until the hardware throttles at TjMax - or until the embedded controller intervenes (on Clevo boards: forced full-volume beeping, then a hard power-off).

Everything below is feature-gated by CPUID, so virtual machines (KVM masks the power bits in CPUID.06H) and non-Intel CPUs skip it safely.

Components

HWP frequency scaling (Hal/CpuPower.cpp)

Hal::CpuPower::InitializeBsp() enables Hardware P-states (IA32_PM_ENABLE), reads the performance range from IA32_HWP_CAPABILITIES, and programs IA32_HWP_REQUEST with:

  • min = lowest performance level, max = highest
  • desired = 0 (fully autonomous hardware frequency selection)
  • EPP = 0x80 (balanced; same as Linux "balance_performance")

This is the same mechanism intel_pstate uses on Linux and is the single biggest thermal win: the CPU idles near its minimum frequency and ramps only while there is work. APs apply the same request in ApEntry via InitializeAp() (HWP request MSRs are per-logical-CPU).

CPUs with EPB but no HWP get IA32_ENERGY_PERF_BIAS = 6 (balanced) instead.

C1E promotion

MSR_POWER_CTL bit 1 lets HLT-idle drop core voltage/frequency to minimum (C1E) instead of parking at the current ratio. Set per-core on the BSP and every AP. Gated on the DTS CPUID bit as a real-hardware heuristic, because MSR_POWER_CTL is not architectural and hypervisors may not emulate it.

Thermal governor (Hal/CpuPower.cpp)

A closed-loop passive-cooling controller on the BSP, stepped from Sched::RunBspMaintenance() (reached from both the idle loop and the timer tick, so it keeps running under full load). Every 500 ms it reads the package digital thermal sensor (IA32_PACKAGE_THERM_STATUS readout relative to TjMax from MSR_TEMPERATURE_TARGET) and adjusts the HWP performance ceiling:

  • >= 85 C (passive): step the ceiling down 2 ratio units (~200 MHz)
  • >= 94 C (hot): step down 8 units per interval
  • <= 78 C (resume): step back up 1 unit per interval
  • between resume and passive: hold (hysteresis)

The ceiling propagates through a policy epoch counter: each CPU compares the epoch in Sched::Tick() (and the AP idle loop) against the last one it applied and rewrites its own IA32_HWP_REQUEST when it changed, since MSR writes only affect the executing core. APs tick every 10 ms, so a throttle decision reaches all cores within one tick.

Throttle engage/release transitions are logged (WARNING/OK, "CpuPower").

Deep AP idle (ACPI/CpuIdle.cpp)

APs use deep MWAIT C-states while idle; C6 power-gates the core entirely, which is where most of the idle-heat win lives on many-core parts.

The hint comes from ACPI _CST FFH entries (the GAS address low byte is the MWAIT hint, same convention as Linux), validated against the CPUID.05H sub-state enumeration and capped at the C6 family (0x2F). With no usable _CST, the deepest CPUID-enumerated hint (up to C6) is used.

The BSP stays in C1 (HLT). On some laptops deep MWAIT states freeze the LAPIC timer even though ARAT is advertised, and the BSP's periodic 1 ms tick is the system timekeeper (see the comment in Timekeeping::IdleOnce). APs have no timekeeping duty: an idle AP is woken by the reschedule IPI, and its coarse tick resumes on wake.

S3 wake

IA32_PM_ENABLE, HWP requests, and MSR_POWER_CTL do not survive suspend. Hal::CpuPower::ReapplyAfterWake() runs in the S3 resume path, redoes the BSP setup, and bumps the policy epoch so every AP re-applies its HWP request (and re-checks C1E) on its next tick.

Diagnostics

SYS_POWERINFO (149) fills a PowerInfo struct: package temperature, TjMax, HWP state and performance range, the governor's current ceiling, CPUID base/turbo frequency, the measured average active frequency (APERF/MPERF over the 500 ms governor window), and the AP idle MWAIT hint. Returns -1 when the CPU exposes no power features (e.g. under QEMU).

The power program (programs/src/power) prints it:

power             one-shot snapshot
power watch       1 Hz samples for 60 s (temp, avg freq, ceiling)
power watch 300   same, for 300 s

Boot log lines to look for (Kernel Log app):

[CpuPower] HWP enabled: perf range 4-54, EPP=0x80
[CpuPower] Thermal governor armed: TjMax=100C, passive=85C, hot=94C, ...
[CpuIdle]  AP deep idle: MWAIT hint 0x20

Tuning

The governor constants live at the top of Hal/CpuPower.cpp (PassiveTempC, HotTempC, ResumeTempC, GovernorIntervalMs, step sizes) and were chosen to keep the package well below both TjMax (100 C on the i9-13900HX) and the Clevo EC's emergency thresholds. If the fans still spin up aggressively under sustained load, lower PassiveTempC first; if builds feel throttled while cool, raise the recovery rate instead of the trip points.