feat: HWP scaling, C1E, closed-loop thermal governor for Intel CPUs
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
|||||||
|
# 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.
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <Hal/Apic/IoApic.hpp>
|
#include <Hal/Apic/IoApic.hpp>
|
||||||
#include <Hal/Apic/Pic.hpp>
|
#include <Hal/Apic/Pic.hpp>
|
||||||
#include <Timekeeping/ApicTimer.hpp>
|
#include <Timekeeping/ApicTimer.hpp>
|
||||||
|
#include <Hal/CpuPower.hpp>
|
||||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||||
#include <Drivers/PS2/PS2Controller.hpp>
|
#include <Drivers/PS2/PS2Controller.hpp>
|
||||||
#include <Graphics/Framebuffer.hpp>
|
#include <Graphics/Framebuffer.hpp>
|
||||||
@@ -435,6 +436,9 @@ namespace Hal {
|
|||||||
}
|
}
|
||||||
reinitProgress(0x04); // PAT
|
reinitProgress(0x04); // PAT
|
||||||
Hal::InitializePAT();
|
Hal::InitializePAT();
|
||||||
|
// HWP enable and POWER_CTL reset across S3; re-apply them and
|
||||||
|
// bump the policy epoch so APs re-apply on their next tick.
|
||||||
|
Hal::CpuPower::ReapplyAfterWake();
|
||||||
reinitProgress(0x05); // PIC
|
reinitProgress(0x05); // PIC
|
||||||
Hal::DisableLegacyPic();
|
Hal::DisableLegacyPic();
|
||||||
reinitProgress(0x06); // Local APIC
|
reinitProgress(0x06); // Local APIC
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <ACPI/FADT.hpp>
|
#include <ACPI/FADT.hpp>
|
||||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||||
#include <Hal/Cpu.hpp>
|
#include <Hal/Cpu.hpp>
|
||||||
|
#include <Hal/SmpBoot.hpp>
|
||||||
#include <Io/IoPort.hpp>
|
#include <Io/IoPort.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
@@ -30,6 +31,7 @@ namespace Hal {
|
|||||||
IdleEntryKind EntryKind = IdleEntryKind::FixedHardware;
|
IdleEntryKind EntryKind = IdleEntryKind::FixedHardware;
|
||||||
uint16_t IoPort = 0;
|
uint16_t IoPort = 0;
|
||||||
uint8_t WidthBits = 0;
|
uint8_t WidthBits = 0;
|
||||||
|
uint8_t MwaitHint = 0; // FFH entries: MWAIT hint from GAS address
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr int MaxIdleStates = 8;
|
static constexpr int MaxIdleStates = 8;
|
||||||
@@ -180,6 +182,9 @@ namespace Hal {
|
|||||||
|
|
||||||
if (addressSpace == GAS_FIXED_HARDWARE) {
|
if (addressSpace == GAS_FIXED_HARDWARE) {
|
||||||
state.EntryKind = IdleEntryKind::FixedHardware;
|
state.EntryKind = IdleEntryKind::FixedHardware;
|
||||||
|
// Intel FFH convention: the GAS address low byte carries the
|
||||||
|
// MWAIT hint for this C-state (same as Linux acpi_idle).
|
||||||
|
state.MwaitHint = (uint8_t)(address & 0xFF);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,10 +387,81 @@ namespace Hal {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Deep MWAIT idle for APs
|
||||||
|
//
|
||||||
|
// The BSP must stay in C1 (HLT / MWAIT hint 0): 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 ApicTimer::IdleOnce). APs have no such duty:
|
||||||
|
// an idle AP is woken by the reschedule IPI, and its coarse tick
|
||||||
|
// simply resumes on wake. Deep core C-states (C6) power-gate the
|
||||||
|
// core entirely, which is where most of the idle-heat win lives
|
||||||
|
// on many-core parts.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static constexpr uint8_t MaxApMwaitHint = 0x2F; // cap at the C6 family
|
||||||
|
static uint8_t g_deepApHint = 0;
|
||||||
|
|
||||||
|
// CPUID.05H:EDX enumerates the number of MWAIT sub-states per
|
||||||
|
// C-state group; the group index for hint h is (h >> 4) + 1.
|
||||||
|
static bool MwaitHintSupported(uint8_t hint) {
|
||||||
|
uint32_t a, b, c, d;
|
||||||
|
asm volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d)
|
||||||
|
: "a"(0), "c"(0));
|
||||||
|
if (a < 5) return false;
|
||||||
|
asm volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d)
|
||||||
|
: "a"(5), "c"(0));
|
||||||
|
if ((c & 1) == 0) return hint == 0; // extensions not enumerated
|
||||||
|
uint32_t group = ((uint32_t)(hint >> 4) & 0xF) + 1;
|
||||||
|
uint32_t sub = hint & 0xF;
|
||||||
|
uint32_t count = (d >> (group * 4)) & 0xF;
|
||||||
|
return sub < count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ComputeDeepApHint() {
|
||||||
|
g_deepApHint = 0;
|
||||||
|
if (!Hal::HasMwait()) return;
|
||||||
|
|
||||||
|
// Prefer firmware-blessed hints from _CST FFH entries.
|
||||||
|
uint8_t best = 0;
|
||||||
|
for (int i = 0; i < g_idleStateCount; i++) {
|
||||||
|
const auto& state = g_idleStates[i];
|
||||||
|
if (!state.Valid || state.EntryKind != IdleEntryKind::FixedHardware) continue;
|
||||||
|
if (state.MwaitHint == 0 || state.MwaitHint > MaxApMwaitHint) continue;
|
||||||
|
if (!MwaitHintSupported(state.MwaitHint)) continue;
|
||||||
|
if (state.MwaitHint > best) best = state.MwaitHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No usable _CST data: derive from CPUID alone (deepest core
|
||||||
|
// state, capped at C6).
|
||||||
|
if (best == 0) {
|
||||||
|
static constexpr uint8_t candidates[] = { 0x20, 0x10, 0x01 };
|
||||||
|
for (uint8_t hint : candidates) {
|
||||||
|
if (MwaitHintSupported(hint)) { best = hint; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_deepApHint = best;
|
||||||
|
if (g_deepApHint != 0) {
|
||||||
|
KernelLogStream(OK, "CpuIdle") << "AP deep idle: MWAIT hint 0x"
|
||||||
|
<< base::hex << (uint64_t)g_deepApHint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t CurrentIdleHint() {
|
||||||
|
auto* cpu = Smp::GetCurrentCpuData();
|
||||||
|
return (cpu != nullptr && cpu->cpuIndex != 0) ? g_deepApHint : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t GetApIdleHint() {
|
||||||
|
return g_deepApHint;
|
||||||
|
}
|
||||||
|
|
||||||
static void FallbackWait(bool hasMwait, volatile uint64_t* monitorAddr) {
|
static void FallbackWait(bool hasMwait, volatile uint64_t* monitorAddr) {
|
||||||
volatile uint64_t* addr = (monitorAddr != nullptr) ? monitorAddr : &g_fallbackMonitor;
|
volatile uint64_t* addr = (monitorAddr != nullptr) ? monitorAddr : &g_fallbackMonitor;
|
||||||
if (hasMwait) {
|
if (hasMwait) {
|
||||||
Hal::IdleWait(addr);
|
Hal::IdleWait(addr, CurrentIdleHint());
|
||||||
} else {
|
} else {
|
||||||
asm volatile("hlt");
|
asm volatile("hlt");
|
||||||
}
|
}
|
||||||
@@ -395,7 +471,7 @@ namespace Hal {
|
|||||||
volatile uint64_t* monitorAddr) {
|
volatile uint64_t* monitorAddr) {
|
||||||
volatile uint64_t* addr = (monitorAddr != nullptr) ? monitorAddr : &g_fallbackMonitor;
|
volatile uint64_t* addr = (monitorAddr != nullptr) ? monitorAddr : &g_fallbackMonitor;
|
||||||
if (hasMwait) {
|
if (hasMwait) {
|
||||||
Hal::IdleWaitWithInterruptsDisabled(addr);
|
Hal::IdleWaitWithInterruptsDisabled(addr, CurrentIdleHint());
|
||||||
} else {
|
} else {
|
||||||
Hal::HaltWithInterruptsDisabled();
|
Hal::HaltWithInterruptsDisabled();
|
||||||
}
|
}
|
||||||
@@ -424,7 +500,7 @@ namespace Hal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Initialize(ACPI::CommonSDTHeader* xsdt) {
|
static void LoadIdleStates(ACPI::CommonSDTHeader* xsdt) {
|
||||||
g_idleStateCount = 0;
|
g_idleStateCount = 0;
|
||||||
for (int i = 0; i < MaxIdleStates; i++) {
|
for (int i = 0; i < MaxIdleStates; i++) {
|
||||||
g_idleStates[i] = {};
|
g_idleStates[i] = {};
|
||||||
@@ -471,6 +547,11 @@ namespace Hal {
|
|||||||
<< "ACPI _CST unavailable or unsupported - using safe MWAIT/HLT idle fallback";
|
<< "ACPI _CST unavailable or unsupported - using safe MWAIT/HLT idle fallback";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Initialize(ACPI::CommonSDTHeader* xsdt) {
|
||||||
|
LoadIdleStates(xsdt);
|
||||||
|
ComputeDeepApHint();
|
||||||
|
}
|
||||||
|
|
||||||
void Wait(uint32_t predictedIdleMs, bool hasMwait, volatile uint64_t* monitorAddr) {
|
void Wait(uint32_t predictedIdleMs, bool hasMwait, volatile uint64_t* monitorAddr) {
|
||||||
const IdleState* state = SelectIdleState(predictedIdleMs, hasMwait);
|
const IdleState* state = SelectIdleState(predictedIdleMs, hasMwait);
|
||||||
if (state == nullptr) {
|
if (state == nullptr) {
|
||||||
|
|||||||
@@ -29,5 +29,9 @@ namespace Hal {
|
|||||||
void WaitShallowWithInterruptsDisabled(bool hasMwait,
|
void WaitShallowWithInterruptsDisabled(bool hasMwait,
|
||||||
volatile uint64_t* monitorAddr = nullptr);
|
volatile uint64_t* monitorAddr = nullptr);
|
||||||
|
|
||||||
|
// The MWAIT hint APs use for deep idle (0 = plain C1). Exposed for
|
||||||
|
// SYS_POWERINFO diagnostics.
|
||||||
|
uint8_t GetApIdleHint();
|
||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 9
|
#define MONTAUK_BUILD_NUMBER 14
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
#include <Graphics/Framebuffer.hpp>
|
#include <Graphics/Framebuffer.hpp>
|
||||||
#include <ACPI/AcpiShutdown.hpp>
|
#include <ACPI/AcpiShutdown.hpp>
|
||||||
#include <ACPI/AcpiSleep.hpp>
|
#include <ACPI/AcpiSleep.hpp>
|
||||||
|
#include <ACPI/CpuIdle.hpp>
|
||||||
|
#include <Hal/CpuPower.hpp>
|
||||||
|
|
||||||
#include "Syscall.hpp"
|
#include "Syscall.hpp"
|
||||||
|
|
||||||
@@ -71,4 +73,27 @@ namespace montauk::abi {
|
|||||||
}
|
}
|
||||||
return (int64_t)Hal::AcpiSleep::Suspend();
|
return (int64_t)Hal::AcpiSleep::Suspend();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SYS_POWERINFO. Fills a CPU power/thermal snapshot. Returns -1 when the
|
||||||
|
// CPU exposes no power management features (e.g. under QEMU).
|
||||||
|
static int64_t Sys_PowerInfo(PowerInfo* out) {
|
||||||
|
Hal::CpuPower::Snapshot s{};
|
||||||
|
if (!Hal::CpuPower::GetSnapshot(s)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
out->hwpActive = s.HwpActive ? 1 : 0;
|
||||||
|
out->throttling = s.Throttling ? 1 : 0;
|
||||||
|
out->tempC = s.TempC;
|
||||||
|
out->tjMaxC = s.TjMaxC;
|
||||||
|
out->highestPerf = s.HighestPerf;
|
||||||
|
out->lowestPerf = s.LowestPerf;
|
||||||
|
out->curMaxPerf = s.CurMaxPerf;
|
||||||
|
out->epp = s.Epp;
|
||||||
|
out->baseMHz = s.BaseMHz;
|
||||||
|
out->maxMHz = s.MaxMHz;
|
||||||
|
out->effMHz = s.EffMHz;
|
||||||
|
out->apIdleHint = Hal::CpuIdle::GetApIdleHint();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -378,6 +378,9 @@ namespace montauk::abi {
|
|||||||
return Sys_SdrSetParam((int)frame->arg1, (int)frame->arg2, frame->arg3);
|
return Sys_SdrSetParam((int)frame->arg1, (int)frame->arg2, frame->arg3);
|
||||||
case SYS_SDR_GETPARAM:
|
case SYS_SDR_GETPARAM:
|
||||||
return Sys_SdrGetParam((int)frame->arg1, (int)frame->arg2);
|
return Sys_SdrGetParam((int)frame->arg1, (int)frame->arg2);
|
||||||
|
case SYS_POWERINFO:
|
||||||
|
if (!UserMemory::Writable<PowerInfo>(frame->arg1)) return -1;
|
||||||
|
return Sys_PowerInfo((PowerInfo*)frame->arg1);
|
||||||
case SYS_THREAD_SPAWN:
|
case SYS_THREAD_SPAWN:
|
||||||
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
|
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
|
||||||
case SYS_THREAD_EXIT:
|
case SYS_THREAD_EXIT:
|
||||||
|
|||||||
@@ -276,6 +276,9 @@ namespace montauk::abi {
|
|||||||
static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value)
|
static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value)
|
||||||
static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value
|
static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value
|
||||||
|
|
||||||
|
/* Power.hpp -- CPU power/thermal status */
|
||||||
|
static constexpr uint64_t SYS_POWERINFO = 149; // (PowerInfo*) -> 0, -1 unsupported
|
||||||
|
|
||||||
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
|
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
|
||||||
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
|
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
|
||||||
static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz
|
static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz
|
||||||
@@ -568,6 +571,22 @@ namespace montauk::abi {
|
|||||||
uint32_t _pad;
|
uint32_t _pad;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// CPU power/thermal snapshot (returned by SYS_POWERINFO)
|
||||||
|
struct PowerInfo {
|
||||||
|
uint8_t hwpActive; // hardware P-state scaling enabled
|
||||||
|
uint8_t throttling; // thermal governor currently limiting frequency
|
||||||
|
uint8_t tempC; // package temperature, degrees C (0 = unknown)
|
||||||
|
uint8_t tjMaxC; // hardware throttle temperature
|
||||||
|
uint8_t highestPerf; // HWP performance range (ratio units)
|
||||||
|
uint8_t lowestPerf;
|
||||||
|
uint8_t curMaxPerf; // thermal governor's current ceiling
|
||||||
|
uint8_t epp; // energy/perf preference (0=perf, 255=power)
|
||||||
|
uint32_t baseMHz; // nominal base frequency (0 = unknown)
|
||||||
|
uint32_t maxMHz; // max turbo frequency
|
||||||
|
uint32_t effMHz; // measured average active frequency
|
||||||
|
uint32_t apIdleHint; // MWAIT hint used for AP deep idle
|
||||||
|
};
|
||||||
|
|
||||||
// Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT)
|
// Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT)
|
||||||
struct CrashReportInfo {
|
struct CrashReportInfo {
|
||||||
int pid;
|
int pid;
|
||||||
|
|||||||
@@ -24,11 +24,12 @@ namespace Hal {
|
|||||||
// power and thermal efficiency than HLT (C1 only).
|
// power and thermal efficiency than HLT (C1 only).
|
||||||
// The monitored address is arbitrary -- we just need MONITOR
|
// The monitored address is arbitrary -- we just need MONITOR
|
||||||
// to arm the wake trigger; any interrupt wakes MWAIT.
|
// to arm the wake trigger; any interrupt wakes MWAIT.
|
||||||
inline void IdleWait(volatile uint64_t* monitorAddr) {
|
// `hint` selects the target C-state (0x00 = C1, 0x01 = C1E,
|
||||||
|
// 0x20 = C6, ...); callers must validate it against CPUID.05H.
|
||||||
|
inline void IdleWait(volatile uint64_t* monitorAddr, uint32_t hint = 0) {
|
||||||
// MONITOR: set up the address monitoring range
|
// MONITOR: set up the address monitoring range
|
||||||
asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0));
|
asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0));
|
||||||
// MWAIT: hint=0x00 (C1 state, platform-dependent deeper states)
|
asm volatile("mwait" :: "a"(hint), "c"(0));
|
||||||
asm volatile("mwait" :: "a"(0x00), "c"(0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atomic idle entry for paths that have already disabled interrupts.
|
// Atomic idle entry for paths that have already disabled interrupts.
|
||||||
@@ -39,9 +40,10 @@ namespace Hal {
|
|||||||
asm volatile("sti\n\thlt\n\tcli" ::: "memory");
|
asm volatile("sti\n\thlt\n\tcli" ::: "memory");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void IdleWaitWithInterruptsDisabled(volatile uint64_t* monitorAddr) {
|
inline void IdleWaitWithInterruptsDisabled(volatile uint64_t* monitorAddr,
|
||||||
|
uint32_t hint = 0) {
|
||||||
asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0) : "memory");
|
asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0) : "memory");
|
||||||
asm volatile("sti\n\tmwait\n\tcli" :: "a"(0x00), "c"(0) : "memory");
|
asm volatile("sti\n\tmwait\n\tcli" :: "a"(hint), "c"(0) : "memory");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void EnableSSE() {
|
inline void EnableSSE() {
|
||||||
|
|||||||
@@ -0,0 +1,390 @@
|
|||||||
|
/*
|
||||||
|
* CpuPower.cpp
|
||||||
|
* Intel CPU power management: HWP frequency scaling, C1E promotion,
|
||||||
|
* and the package thermal governor.
|
||||||
|
*
|
||||||
|
* Without this module the CPU runs at whatever P-state the firmware left
|
||||||
|
* it in, forever, and idle cores never drop below C1. On a modern laptop
|
||||||
|
* part (tested target: i9-13900HX, 24 cores) that means constant high
|
||||||
|
* package power, spinning fans, and eventually the EC's thermal
|
||||||
|
* protection (forced beeping / hard power-off on Clevo boards).
|
||||||
|
*
|
||||||
|
* Three mechanisms, all feature-gated by CPUID so virtual machines and
|
||||||
|
* non-Intel parts skip them safely:
|
||||||
|
*
|
||||||
|
* 1. HWP (Hardware P-states, CPUID.06H:EAX[7]): the CPU autonomously
|
||||||
|
* picks its frequency between lowest and highest performance level,
|
||||||
|
* biased by the Energy/Performance Preference byte. This is what
|
||||||
|
* intel_pstate does on Linux and is the single biggest thermal win.
|
||||||
|
*
|
||||||
|
* 2. C1E promotion (MSR_POWER_CTL[1]): lets HLT idle drop core voltage
|
||||||
|
* and frequency to minimum instead of parking at the current ratio.
|
||||||
|
*
|
||||||
|
* 3. Thermal governor: reads the package digital thermal sensor every
|
||||||
|
* 500 ms on the BSP and steps the HWP performance ceiling down when
|
||||||
|
* the package crosses PassiveTempC, hard-clamps toward lowest above
|
||||||
|
* HotTempC, and slowly recovers below ResumeTempC. This keeps the
|
||||||
|
* package away from TjMax (hardware PROCHOT) and far away from the
|
||||||
|
* EC's emergency responses.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "CpuPower.hpp"
|
||||||
|
#include "MSR.hpp"
|
||||||
|
#include "SmpBoot.hpp"
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Hal {
|
||||||
|
namespace CpuPower {
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MSR addresses (power/thermal; used only in this module)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static constexpr uint32_t IA32_MPERF = 0x0E7;
|
||||||
|
static constexpr uint32_t IA32_APERF = 0x0E8;
|
||||||
|
static constexpr uint32_t IA32_THERM_STATUS = 0x19C;
|
||||||
|
static constexpr uint32_t MSR_TEMPERATURE_TARGET = 0x1A2;
|
||||||
|
static constexpr uint32_t IA32_ENERGY_PERF_BIAS = 0x1B0;
|
||||||
|
static constexpr uint32_t IA32_PACKAGE_THERM_STATUS = 0x1B1;
|
||||||
|
static constexpr uint32_t MSR_POWER_CTL = 0x1FC;
|
||||||
|
static constexpr uint32_t IA32_PM_ENABLE = 0x770;
|
||||||
|
static constexpr uint32_t IA32_HWP_CAPABILITIES = 0x771;
|
||||||
|
static constexpr uint32_t IA32_HWP_REQUEST = 0x774;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Governor tuning
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Balanced energy/performance preference (0 = max performance,
|
||||||
|
// 255 = max power saving). 0x80 matches Linux "balance_performance".
|
||||||
|
static constexpr uint8_t BalancedEpp = 0x80;
|
||||||
|
|
||||||
|
static constexpr uint8_t PassiveTempC = 85; // start stepping down
|
||||||
|
static constexpr uint8_t HotTempC = 94; // step down aggressively
|
||||||
|
static constexpr uint8_t ResumeTempC = 78; // start stepping back up
|
||||||
|
static constexpr uint32_t GovernorIntervalMs = 500;
|
||||||
|
static constexpr uint8_t PassiveStepDown = 2; // ratio units (~200 MHz)
|
||||||
|
static constexpr uint8_t HotStepDown = 8; // ratio units (~800 MHz)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Detected features and shared policy state
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
struct Features {
|
||||||
|
bool Intel = false;
|
||||||
|
bool Dts = false; // CPUID.06H:EAX[0] digital thermal sensor
|
||||||
|
bool Ptm = false; // CPUID.06H:EAX[6] package thermal sensor
|
||||||
|
bool Hwp = false; // CPUID.06H:EAX[7] hardware P-states
|
||||||
|
bool HwpEpp = false; // CPUID.06H:EAX[10] HWP energy/perf pref
|
||||||
|
bool Epb = false; // CPUID.06H:ECX[3] energy/perf bias MSR
|
||||||
|
bool AperfMperf = false; // CPUID.06H:ECX[0] freq feedback counters
|
||||||
|
uint32_t MaxLeaf = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
static Features g_feat{};
|
||||||
|
static bool g_hwpActive = false;
|
||||||
|
static bool g_thermalReady = false;
|
||||||
|
static uint8_t g_highestPerf = 0;
|
||||||
|
static uint8_t g_lowestPerf = 0;
|
||||||
|
static uint8_t g_epp = 0;
|
||||||
|
static uint8_t g_tjMax = 100;
|
||||||
|
static uint32_t g_baseMHz = 0;
|
||||||
|
static uint32_t g_maxMHz = 0;
|
||||||
|
|
||||||
|
// Governor output: the current performance ceiling plus an epoch
|
||||||
|
// counter. Each CPU compares the epoch against the last one it
|
||||||
|
// applied and rewrites its own IA32_HWP_REQUEST when they differ
|
||||||
|
// (MSR writes only take effect on the core that executes them).
|
||||||
|
static std::atomic<uint8_t> g_curMaxPerf{0};
|
||||||
|
static std::atomic<uint32_t> g_policyEpoch{0};
|
||||||
|
static uint32_t g_appliedEpoch[Smp::MaxCPUs]{};
|
||||||
|
|
||||||
|
static bool g_throttling = false;
|
||||||
|
static std::atomic<uint8_t> g_lastTempC{0};
|
||||||
|
static std::atomic<uint32_t> g_effMHz{0};
|
||||||
|
static uint64_t g_lastGovernorMs = 0;
|
||||||
|
static uint64_t g_lastAperf = 0;
|
||||||
|
static uint64_t g_lastMperf = 0;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Helpers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void Cpuid(uint32_t leaf, uint32_t subleaf,
|
||||||
|
uint32_t& a, uint32_t& b, uint32_t& c, uint32_t& d) {
|
||||||
|
asm volatile("cpuid"
|
||||||
|
: "=a"(a), "=b"(b), "=c"(c), "=d"(d)
|
||||||
|
: "a"(leaf), "c"(subleaf));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DetectFeatures() {
|
||||||
|
uint32_t a, b, c, d;
|
||||||
|
Cpuid(0, 0, a, b, c, d);
|
||||||
|
g_feat.MaxLeaf = a;
|
||||||
|
g_feat.Intel = (b == 0x756E6547 && d == 0x49656E69 && c == 0x6C65746E);
|
||||||
|
if (!g_feat.Intel || g_feat.MaxLeaf < 6) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cpuid(6, 0, a, b, c, d);
|
||||||
|
g_feat.Dts = (a & (1u << 0)) != 0;
|
||||||
|
g_feat.Ptm = (a & (1u << 6)) != 0;
|
||||||
|
g_feat.Hwp = (a & (1u << 7)) != 0;
|
||||||
|
g_feat.HwpEpp = (a & (1u << 10)) != 0;
|
||||||
|
g_feat.AperfMperf = (c & (1u << 0)) != 0;
|
||||||
|
g_feat.Epb = (c & (1u << 3)) != 0;
|
||||||
|
|
||||||
|
if (g_feat.MaxLeaf >= 0x16) {
|
||||||
|
Cpuid(0x16, 0, a, b, c, d);
|
||||||
|
g_baseMHz = a & 0xFFFF;
|
||||||
|
g_maxMHz = b & 0xFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t ComposeHwpRequest(uint8_t minPerf, uint8_t maxPerf, uint8_t epp) {
|
||||||
|
// Desired = 0 selects fully autonomous frequency selection.
|
||||||
|
return (uint64_t)minPerf
|
||||||
|
| ((uint64_t)maxPerf << 8)
|
||||||
|
| ((uint64_t)epp << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSR_POWER_CTL is not architectural, so gate it on the digital
|
||||||
|
// thermal sensor bit: hypervisors (KVM, TCG) mask the CPUID.06H
|
||||||
|
// power bits, so DTS present means real Intel hardware where the
|
||||||
|
// MSR exists.
|
||||||
|
static void EnableC1ePromotionOnThisCore() {
|
||||||
|
if (!g_feat.Dts) return;
|
||||||
|
uint64_t v = ReadMSR(MSR_POWER_CTL);
|
||||||
|
if ((v & (1ull << 1)) == 0) {
|
||||||
|
WriteMSR(MSR_POWER_CTL, v | (1ull << 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t ReadPackageTempC() {
|
||||||
|
if (g_feat.Ptm) {
|
||||||
|
uint64_t s = ReadMSR(IA32_PACKAGE_THERM_STATUS);
|
||||||
|
uint32_t readout = (uint32_t)((s >> 16) & 0x7F);
|
||||||
|
return (readout <= g_tjMax) ? (uint8_t)(g_tjMax - readout) : 0;
|
||||||
|
}
|
||||||
|
if (g_feat.Dts) {
|
||||||
|
uint64_t s = ReadMSR(IA32_THERM_STATUS);
|
||||||
|
if (s & (1ull << 31)) { // reading valid
|
||||||
|
uint32_t readout = (uint32_t)((s >> 16) & 0x7F);
|
||||||
|
return (readout <= g_tjMax) ? (uint8_t)(g_tjMax - readout) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void UpdateEffectiveFrequency() {
|
||||||
|
if (!g_feat.AperfMperf || g_baseMHz == 0) return;
|
||||||
|
|
||||||
|
uint64_t aperf = ReadMSR(IA32_APERF);
|
||||||
|
uint64_t mperf = ReadMSR(IA32_MPERF);
|
||||||
|
uint64_t dA = aperf - g_lastAperf;
|
||||||
|
uint64_t dM = mperf - g_lastMperf;
|
||||||
|
bool primed = (g_lastAperf | g_lastMperf) != 0;
|
||||||
|
g_lastAperf = aperf;
|
||||||
|
g_lastMperf = mperf;
|
||||||
|
|
||||||
|
if (primed && dM != 0) {
|
||||||
|
g_effMHz.store((uint32_t)((uint64_t)g_baseMHz * dA / dM),
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ApplyHwpRequestOnThisCpu(uint32_t epoch) {
|
||||||
|
auto* cpu = Smp::GetCurrentCpuData();
|
||||||
|
if (cpu == nullptr || cpu->cpuIndex < 0 || cpu->cpuIndex >= Smp::MaxCPUs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_appliedEpoch[cpu->cpuIndex] = epoch;
|
||||||
|
WriteMSR(IA32_HWP_REQUEST,
|
||||||
|
ComposeHwpRequest(g_lowestPerf,
|
||||||
|
g_curMaxPerf.load(std::memory_order_relaxed),
|
||||||
|
g_epp));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Public interface
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void InitializeBsp() {
|
||||||
|
DetectFeatures();
|
||||||
|
if (!g_feat.Intel) {
|
||||||
|
KernelLogStream(INFO, "CpuPower")
|
||||||
|
<< "Not an Intel CPU - power management left to firmware defaults";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_feat.Dts || g_feat.Ptm) {
|
||||||
|
uint64_t tt = ReadMSR(MSR_TEMPERATURE_TARGET);
|
||||||
|
uint8_t tj = (uint8_t)((tt >> 16) & 0xFF);
|
||||||
|
if (tj >= 50 && tj <= 115) {
|
||||||
|
g_tjMax = tj;
|
||||||
|
}
|
||||||
|
g_thermalReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnableC1ePromotionOnThisCore();
|
||||||
|
|
||||||
|
if (g_feat.Hwp) {
|
||||||
|
// One-way until reset; harmless if firmware already set it.
|
||||||
|
WriteMSR(IA32_PM_ENABLE, 1);
|
||||||
|
if (ReadMSR(IA32_PM_ENABLE) & 1) {
|
||||||
|
uint64_t caps = ReadMSR(IA32_HWP_CAPABILITIES);
|
||||||
|
g_highestPerf = (uint8_t)(caps & 0xFF);
|
||||||
|
g_lowestPerf = (uint8_t)((caps >> 24) & 0xFF);
|
||||||
|
// The EPP byte is reserved when CPUID does not
|
||||||
|
// advertise it; 0 (max performance bias) is the only
|
||||||
|
// safe value there.
|
||||||
|
g_epp = g_feat.HwpEpp ? BalancedEpp : 0;
|
||||||
|
|
||||||
|
g_curMaxPerf.store(g_highestPerf, std::memory_order_relaxed);
|
||||||
|
uint32_t epoch = g_policyEpoch.fetch_add(1,
|
||||||
|
std::memory_order_acq_rel) + 1;
|
||||||
|
g_hwpActive = true;
|
||||||
|
ApplyHwpRequestOnThisCpu(epoch);
|
||||||
|
|
||||||
|
KernelLogStream(OK, "CpuPower") << "HWP enabled: perf range "
|
||||||
|
<< base::dec << (uint64_t)g_lowestPerf << "-"
|
||||||
|
<< (uint64_t)g_highestPerf << ", EPP=0x"
|
||||||
|
<< base::hex << (uint64_t)g_epp;
|
||||||
|
} else {
|
||||||
|
KernelLogStream(WARNING, "CpuPower")
|
||||||
|
<< "HWP advertised but IA32_PM_ENABLE did not stick";
|
||||||
|
}
|
||||||
|
} else if (g_feat.Epb) {
|
||||||
|
// Legacy knob: bias hardware decisions toward balance.
|
||||||
|
WriteMSR(IA32_ENERGY_PERF_BIAS, 6);
|
||||||
|
KernelLogStream(OK, "CpuPower") << "No HWP - set EPB to balanced";
|
||||||
|
} else if (!g_thermalReady) {
|
||||||
|
KernelLogStream(INFO, "CpuPower")
|
||||||
|
<< "No power features exposed (hypervisor or old CPU)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_thermalReady) {
|
||||||
|
UpdateEffectiveFrequency(); // prime APERF/MPERF baseline
|
||||||
|
g_lastTempC.store(ReadPackageTempC(), std::memory_order_relaxed);
|
||||||
|
KernelLogStream(OK, "CpuPower") << "Thermal governor armed: TjMax="
|
||||||
|
<< base::dec << (uint64_t)g_tjMax << "C, passive="
|
||||||
|
<< (uint64_t)PassiveTempC << "C, hot=" << (uint64_t)HotTempC
|
||||||
|
<< "C, package now " << (uint64_t)g_lastTempC.load() << "C";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitializeAp() {
|
||||||
|
if (!g_feat.Intel) return;
|
||||||
|
|
||||||
|
EnableC1ePromotionOnThisCore();
|
||||||
|
|
||||||
|
if (g_hwpActive) {
|
||||||
|
// IA32_PM_ENABLE is package-scoped on most parts, but the
|
||||||
|
// SDM allows logical scope; setting it again is harmless.
|
||||||
|
WriteMSR(IA32_PM_ENABLE, 1);
|
||||||
|
ApplyHwpRequestOnThisCpu(g_policyEpoch.load(std::memory_order_acquire));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyPolicyIfChanged() {
|
||||||
|
if (!g_hwpActive) return;
|
||||||
|
auto* cpu = Smp::GetCurrentCpuData();
|
||||||
|
if (cpu == nullptr || cpu->cpuIndex < 0 || cpu->cpuIndex >= Smp::MaxCPUs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t epoch = g_policyEpoch.load(std::memory_order_acquire);
|
||||||
|
if (g_appliedEpoch[cpu->cpuIndex] == epoch) return;
|
||||||
|
// Also re-check C1E promotion here: POWER_CTL is per-core and
|
||||||
|
// loses its contents across S3, and ReapplyAfterWake only runs
|
||||||
|
// on the BSP. The epoch bump it does routes every AP through
|
||||||
|
// this path.
|
||||||
|
EnableC1ePromotionOnThisCore();
|
||||||
|
ApplyHwpRequestOnThisCpu(epoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThermalTick(uint64_t nowMs) {
|
||||||
|
if (!g_thermalReady) return;
|
||||||
|
if (nowMs - g_lastGovernorMs < GovernorIntervalMs) return;
|
||||||
|
g_lastGovernorMs = nowMs;
|
||||||
|
|
||||||
|
UpdateEffectiveFrequency();
|
||||||
|
|
||||||
|
uint8_t temp = ReadPackageTempC();
|
||||||
|
g_lastTempC.store(temp, std::memory_order_relaxed);
|
||||||
|
if (temp == 0 || !g_hwpActive) return;
|
||||||
|
|
||||||
|
uint8_t cur = g_curMaxPerf.load(std::memory_order_relaxed);
|
||||||
|
uint8_t next = cur;
|
||||||
|
|
||||||
|
if (temp >= HotTempC) {
|
||||||
|
next = (cur > (uint8_t)(g_lowestPerf + HotStepDown))
|
||||||
|
? (uint8_t)(cur - HotStepDown) : g_lowestPerf;
|
||||||
|
} else if (temp >= PassiveTempC) {
|
||||||
|
next = (cur > (uint8_t)(g_lowestPerf + PassiveStepDown))
|
||||||
|
? (uint8_t)(cur - PassiveStepDown) : g_lowestPerf;
|
||||||
|
} else if (temp <= ResumeTempC && cur < g_highestPerf) {
|
||||||
|
next = (uint8_t)(cur + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next == cur) return;
|
||||||
|
|
||||||
|
bool wasThrottling = g_throttling;
|
||||||
|
g_throttling = (next < g_highestPerf);
|
||||||
|
g_curMaxPerf.store(next, std::memory_order_relaxed);
|
||||||
|
uint32_t epoch = g_policyEpoch.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||||
|
ApplyHwpRequestOnThisCpu(epoch); // BSP applies immediately; APs on tick
|
||||||
|
|
||||||
|
if (g_throttling && !wasThrottling) {
|
||||||
|
KernelLogStream(WARNING, "CpuPower") << "Package at " << base::dec
|
||||||
|
<< (uint64_t)temp << "C - thermal throttle engaged";
|
||||||
|
} else if (!g_throttling && wasThrottling) {
|
||||||
|
KernelLogStream(OK, "CpuPower") << "Package cooled to " << base::dec
|
||||||
|
<< (uint64_t)temp << "C - thermal throttle released";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReapplyAfterWake() {
|
||||||
|
if (!g_feat.Intel) return;
|
||||||
|
|
||||||
|
EnableC1ePromotionOnThisCore();
|
||||||
|
|
||||||
|
if (g_hwpActive) {
|
||||||
|
WriteMSR(IA32_PM_ENABLE, 1);
|
||||||
|
// Force every CPU (including this one) to re-apply the HWP
|
||||||
|
// request: the MSRs lost their contents across S3.
|
||||||
|
uint32_t epoch = g_policyEpoch.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||||
|
ApplyHwpRequestOnThisCpu(epoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the frequency-feedback baseline (counters restarted).
|
||||||
|
g_lastAperf = 0;
|
||||||
|
g_lastMperf = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetSnapshot(Snapshot& out) {
|
||||||
|
out = {};
|
||||||
|
if (!g_feat.Intel || (!g_thermalReady && !g_hwpActive)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.HwpActive = g_hwpActive;
|
||||||
|
out.Throttling = g_throttling;
|
||||||
|
out.TempC = g_lastTempC.load(std::memory_order_relaxed);
|
||||||
|
out.TjMaxC = g_tjMax;
|
||||||
|
out.HighestPerf = g_highestPerf;
|
||||||
|
out.LowestPerf = g_lowestPerf;
|
||||||
|
out.CurMaxPerf = g_curMaxPerf.load(std::memory_order_relaxed);
|
||||||
|
out.Epp = g_epp;
|
||||||
|
out.BaseMHz = g_baseMHz;
|
||||||
|
out.MaxMHz = g_maxMHz;
|
||||||
|
out.EffMHz = g_effMHz.load(std::memory_order_relaxed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* CpuPower.hpp
|
||||||
|
* Intel CPU power management: HWP frequency scaling, C1E promotion,
|
||||||
|
* and the package thermal governor.
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace Hal {
|
||||||
|
namespace CpuPower {
|
||||||
|
|
||||||
|
// Snapshot of the power/thermal state for SYS_POWERINFO and logging.
|
||||||
|
struct Snapshot {
|
||||||
|
bool HwpActive;
|
||||||
|
bool Throttling;
|
||||||
|
uint8_t TempC; // package temperature (0 = unknown)
|
||||||
|
uint8_t TjMaxC; // hardware throttle temperature
|
||||||
|
uint8_t HighestPerf; // HWP performance range (ratio units)
|
||||||
|
uint8_t LowestPerf;
|
||||||
|
uint8_t CurMaxPerf; // governor's current performance ceiling
|
||||||
|
uint8_t Epp; // energy/performance preference in effect
|
||||||
|
uint32_t BaseMHz; // CPUID base frequency (0 = unknown)
|
||||||
|
uint32_t MaxMHz; // CPUID max turbo frequency
|
||||||
|
uint32_t EffMHz; // measured average active frequency (BSP)
|
||||||
|
};
|
||||||
|
|
||||||
|
// BSP: detect features, enable HWP with a balanced energy preference,
|
||||||
|
// enable C1E promotion, and prime the thermal governor. Must run
|
||||||
|
// before the APs boot so they can pick up the shared policy.
|
||||||
|
void InitializeBsp();
|
||||||
|
|
||||||
|
// AP: per-CPU MSR setup (C1E promotion + initial HWP request).
|
||||||
|
void InitializeAp();
|
||||||
|
|
||||||
|
// Re-apply the HWP request on the calling CPU if the governor changed
|
||||||
|
// the performance ceiling since this CPU last applied it. Cheap (one
|
||||||
|
// relaxed atomic compare when nothing changed) - safe in tick paths.
|
||||||
|
void ApplyPolicyIfChanged();
|
||||||
|
|
||||||
|
// Thermal governor step. BSP-only; rate-limited internally, so it can
|
||||||
|
// be called from every maintenance pass.
|
||||||
|
void ThermalTick(uint64_t nowMs);
|
||||||
|
|
||||||
|
// Redo the BSP MSR setup after S3 wake (HWP enable and POWER_CTL do
|
||||||
|
// not survive suspend).
|
||||||
|
void ReapplyAfterWake();
|
||||||
|
|
||||||
|
// Fill a snapshot for SYS_POWERINFO. Returns false when the CPU
|
||||||
|
// exposes none of the relevant features (e.g. under QEMU).
|
||||||
|
bool GetSnapshot(Snapshot& out);
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <Hal/IDT.hpp>
|
#include <Hal/IDT.hpp>
|
||||||
#include <Hal/MSR.hpp>
|
#include <Hal/MSR.hpp>
|
||||||
#include <Hal/Cpu.hpp>
|
#include <Hal/Cpu.hpp>
|
||||||
|
#include <Hal/CpuPower.hpp>
|
||||||
#include <Memory/Paging.hpp>
|
#include <Memory/Paging.hpp>
|
||||||
#include <Memory/PageFrameAllocator.hpp>
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
#include <Memory/HHDM.hpp>
|
#include <Memory/HHDM.hpp>
|
||||||
@@ -204,6 +205,9 @@ namespace Smp {
|
|||||||
// --- Check MWAIT support ---
|
// --- Check MWAIT support ---
|
||||||
cpu->hasMwait = Hal::HasMwait();
|
cpu->hasMwait = Hal::HasMwait();
|
||||||
|
|
||||||
|
// --- Per-CPU power setup (C1E promotion, HWP request) ---
|
||||||
|
Hal::CpuPower::InitializeAp();
|
||||||
|
|
||||||
// --- Signal that we are online ---
|
// --- Signal that we are online ---
|
||||||
cpu->started = true;
|
cpu->started = true;
|
||||||
|
|
||||||
@@ -212,6 +216,8 @@ namespace Smp {
|
|||||||
|
|
||||||
static volatile uint64_t s_idleMonitor = 0;
|
static volatile uint64_t s_idleMonitor = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
// Pick up thermal-governor frequency changes decided by the BSP.
|
||||||
|
Hal::CpuPower::ApplyPolicyIfChanged();
|
||||||
Hal::CpuIdle::Wait(10, cpu->hasMwait, &s_idleMonitor);
|
Hal::CpuIdle::Wait(10, cpu->hasMwait, &s_idleMonitor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include <Graphics/Framebuffer.hpp>
|
#include <Graphics/Framebuffer.hpp>
|
||||||
#include <Hal/MSR.hpp>
|
#include <Hal/MSR.hpp>
|
||||||
#include <Hal/Cpu.hpp>
|
#include <Hal/Cpu.hpp>
|
||||||
|
#include <Hal/CpuPower.hpp>
|
||||||
#include <Fs/Boot.hpp>
|
#include <Fs/Boot.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
#include <Ipc/Ipc.hpp>
|
#include <Ipc/Ipc.hpp>
|
||||||
@@ -148,6 +149,11 @@ extern "C" void kmain() {
|
|||||||
// ISR stubs use SWAPGS which requires GS base to point to CpuData.
|
// ISR stubs use SWAPGS which requires GS base to point to CpuData.
|
||||||
Smp::InitBsp();
|
Smp::InitBsp();
|
||||||
|
|
||||||
|
// Enable hardware P-state scaling and the thermal governor.
|
||||||
|
// Needs GS base (per-CPU data) set up, and must run before the
|
||||||
|
// APs boot so they inherit the shared policy in ApEntry.
|
||||||
|
Hal::CpuPower::InitializeBsp();
|
||||||
|
|
||||||
// Now safe to enable interrupts (SWAPGS-aware ISR stubs are installed)
|
// Now safe to enable interrupts (SWAPGS-aware ISR stubs are installed)
|
||||||
asm volatile("sti");
|
asm volatile("sti");
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <Hal/Apic/Interrupts.hpp>
|
#include <Hal/Apic/Interrupts.hpp>
|
||||||
#include <Hal/GDT.hpp>
|
#include <Hal/GDT.hpp>
|
||||||
#include <Hal/SmpBoot.hpp>
|
#include <Hal/SmpBoot.hpp>
|
||||||
|
#include <Hal/CpuPower.hpp>
|
||||||
#include <Timekeeping/ApicTimer.hpp>
|
#include <Timekeeping/ApicTimer.hpp>
|
||||||
#include <Api/WinServer.hpp>
|
#include <Api/WinServer.hpp>
|
||||||
#include <Api/Heap.hpp>
|
#include <Api/Heap.hpp>
|
||||||
@@ -811,6 +812,11 @@ namespace Sched {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ReclaimTerminated();
|
ReclaimTerminated();
|
||||||
|
|
||||||
|
// Thermal governor step (rate-limited internally to 500 ms).
|
||||||
|
// Runs here because BSP maintenance is reached from both the idle
|
||||||
|
// loop and the timer tick, so it keeps running under full load.
|
||||||
|
Hal::CpuPower::ThermalTick(Timekeeping::GetMilliseconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t GetNextDeadlineTick() {
|
uint64_t GetNextDeadlineTick() {
|
||||||
@@ -922,6 +928,11 @@ namespace Sched {
|
|||||||
void Tick(uint32_t elapsedMs) {
|
void Tick(uint32_t elapsedMs) {
|
||||||
auto* cpu = Smp::GetCurrentCpuData();
|
auto* cpu = Smp::GetCurrentCpuData();
|
||||||
|
|
||||||
|
// Pick up thermal-governor frequency changes (HWP requests are
|
||||||
|
// per-core MSRs, so every CPU applies them itself). No-op unless
|
||||||
|
// the governor bumped the policy epoch since our last tick.
|
||||||
|
Hal::CpuPower::ApplyPolicyIfChanged();
|
||||||
|
|
||||||
// BSP: wake sleeping processes and reclaim terminated slots
|
// BSP: wake sleeping processes and reclaim terminated slots
|
||||||
if (cpu->cpuIndex == 0) {
|
if (cpu->cpuIndex == 0) {
|
||||||
RunBspMaintenance();
|
RunBspMaintenance();
|
||||||
|
|||||||
@@ -200,6 +200,9 @@ namespace montauk::abi {
|
|||||||
static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value)
|
static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value)
|
||||||
static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value
|
static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value
|
||||||
|
|
||||||
|
// CPU power/thermal status
|
||||||
|
static constexpr uint64_t SYS_POWERINFO = 149; // (PowerInfo*) -> 0, -1 unsupported
|
||||||
|
|
||||||
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
|
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
|
||||||
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
|
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
|
||||||
static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz
|
static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz
|
||||||
@@ -493,6 +496,22 @@ namespace montauk::abi {
|
|||||||
uint32_t _pad;
|
uint32_t _pad;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// CPU power/thermal snapshot (returned by SYS_POWERINFO)
|
||||||
|
struct PowerInfo {
|
||||||
|
uint8_t hwpActive; // hardware P-state scaling enabled
|
||||||
|
uint8_t throttling; // thermal governor currently limiting frequency
|
||||||
|
uint8_t tempC; // package temperature, degrees C (0 = unknown)
|
||||||
|
uint8_t tjMaxC; // hardware throttle temperature
|
||||||
|
uint8_t highestPerf; // HWP performance range (ratio units)
|
||||||
|
uint8_t lowestPerf;
|
||||||
|
uint8_t curMaxPerf; // thermal governor's current ceiling
|
||||||
|
uint8_t epp; // energy/perf preference (0=perf, 255=power)
|
||||||
|
uint32_t baseMHz; // nominal base frequency (0 = unknown)
|
||||||
|
uint32_t maxMHz; // max turbo frequency
|
||||||
|
uint32_t effMHz; // measured average active frequency
|
||||||
|
uint32_t apIdleHint; // MWAIT hint used for AP deep idle
|
||||||
|
};
|
||||||
|
|
||||||
struct ProcInfo {
|
struct ProcInfo {
|
||||||
int32_t pid;
|
int32_t pid;
|
||||||
int32_t parentPid;
|
int32_t parentPid;
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ extern "C" {
|
|||||||
#define MTK_SYS_SDR_READ 146
|
#define MTK_SYS_SDR_READ 146
|
||||||
#define MTK_SYS_SDR_SETPARAM 147
|
#define MTK_SYS_SDR_SETPARAM 147
|
||||||
#define MTK_SYS_SDR_GETPARAM 148
|
#define MTK_SYS_SDR_GETPARAM 148
|
||||||
|
#define MTK_SYS_POWERINFO 149
|
||||||
/* @SYSCALLS-END */
|
/* @SYSCALLS-END */
|
||||||
|
|
||||||
#define MTK_SOCK_TCP 1
|
#define MTK_SOCK_TCP 1
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* main.cpp
|
||||||
|
* power - CPU power/thermal status tool.
|
||||||
|
*
|
||||||
|
* Reads the kernel's power management snapshot (SYS_POWERINFO): package
|
||||||
|
* temperature, HWP frequency-scaling state, the thermal governor's
|
||||||
|
* current frequency ceiling, and the measured average active frequency.
|
||||||
|
* Useful for verifying that HWP and the thermal governor are doing their
|
||||||
|
* job on real hardware.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* power print the current snapshot
|
||||||
|
* power watch sample once per second until 60 s elapse
|
||||||
|
* power watch <secs> sample once per second for <secs> seconds
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <montauk/syscall.h>
|
||||||
|
|
||||||
|
using namespace montauk;
|
||||||
|
|
||||||
|
static void put_u64(uint64_t n) {
|
||||||
|
char buf[24];
|
||||||
|
int i = 0;
|
||||||
|
if (n == 0) { putchar('0'); return; }
|
||||||
|
while (n) { buf[i++] = (char)('0' + (n % 10)); n /= 10; }
|
||||||
|
while (i) putchar(buf[--i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void put_hex8(uint8_t v) {
|
||||||
|
static const char* digits = "0123456789ABCDEF";
|
||||||
|
print("0x");
|
||||||
|
putchar(digits[(v >> 4) & 0xF]);
|
||||||
|
putchar(digits[v & 0xF]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool read_info(montauk::abi::PowerInfo& info) {
|
||||||
|
return syscall1(montauk::abi::SYS_POWERINFO, (uint64_t)&info) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_snapshot(const montauk::abi::PowerInfo& info) {
|
||||||
|
print("CPU power/thermal status:\n");
|
||||||
|
|
||||||
|
print(" HWP: ");
|
||||||
|
if (info.hwpActive) {
|
||||||
|
print("active (EPP ");
|
||||||
|
put_hex8(info.epp);
|
||||||
|
print(", perf range ");
|
||||||
|
put_u64(info.lowestPerf);
|
||||||
|
print("-");
|
||||||
|
put_u64(info.highestPerf);
|
||||||
|
print(")\n");
|
||||||
|
} else {
|
||||||
|
print("not active\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
print(" Frequency: base ");
|
||||||
|
put_u64(info.baseMHz);
|
||||||
|
print(" MHz, turbo ");
|
||||||
|
put_u64(info.maxMHz);
|
||||||
|
print(" MHz, avg active ");
|
||||||
|
put_u64(info.effMHz);
|
||||||
|
print(" MHz\n");
|
||||||
|
|
||||||
|
print(" Package: ");
|
||||||
|
if (info.tempC != 0) {
|
||||||
|
put_u64(info.tempC);
|
||||||
|
print(" C (hardware throttles at ");
|
||||||
|
put_u64(info.tjMaxC);
|
||||||
|
print(" C)\n");
|
||||||
|
} else {
|
||||||
|
print("temperature unavailable\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
print(" Governor: ");
|
||||||
|
if (!info.hwpActive) {
|
||||||
|
print("inactive\n");
|
||||||
|
} else if (info.throttling) {
|
||||||
|
print("THROTTLING - ceiling ");
|
||||||
|
put_u64(info.curMaxPerf);
|
||||||
|
print(" of ");
|
||||||
|
put_u64(info.highestPerf);
|
||||||
|
print("\n");
|
||||||
|
} else {
|
||||||
|
print("not throttling (ceiling ");
|
||||||
|
put_u64(info.curMaxPerf);
|
||||||
|
print(")\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
print(" AP idle: MWAIT hint ");
|
||||||
|
put_hex8((uint8_t)info.apIdleHint);
|
||||||
|
if (info.apIdleHint == 0) {
|
||||||
|
print(" (C1 only)");
|
||||||
|
}
|
||||||
|
print("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_watch_line(const montauk::abi::PowerInfo& info, uint64_t t) {
|
||||||
|
put_u64(t);
|
||||||
|
print("s ");
|
||||||
|
put_u64(info.tempC);
|
||||||
|
print(" C avg ");
|
||||||
|
put_u64(info.effMHz);
|
||||||
|
print(" MHz ceiling ");
|
||||||
|
put_u64(info.curMaxPerf);
|
||||||
|
print("/");
|
||||||
|
put_u64(info.highestPerf);
|
||||||
|
print(info.throttling ? " THROTTLING\n" : "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t parse_u64(const char* s, uint64_t fallback) {
|
||||||
|
uint64_t v = 0;
|
||||||
|
bool any = false;
|
||||||
|
while (*s == ' ') s++;
|
||||||
|
while (*s >= '0' && *s <= '9') { v = v * 10 + (uint64_t)(*s - '0'); s++; any = true; }
|
||||||
|
return any ? v : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void _start() {
|
||||||
|
char args[64];
|
||||||
|
int alen = montauk::getargs(args, sizeof(args));
|
||||||
|
const char* rest = (alen > 0) ? args : "";
|
||||||
|
|
||||||
|
montauk::abi::PowerInfo info{};
|
||||||
|
if (!read_info(info)) {
|
||||||
|
print("power: no CPU power management available\n");
|
||||||
|
print("(non-Intel CPU or running under emulation)\n");
|
||||||
|
montauk::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool watch = rest[0] == 'w';
|
||||||
|
if (!watch) {
|
||||||
|
print_snapshot(info);
|
||||||
|
montauk::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* p = rest;
|
||||||
|
while (*p && *p != ' ') p++;
|
||||||
|
uint64_t seconds = parse_u64(p, 60);
|
||||||
|
if (seconds == 0) seconds = 60;
|
||||||
|
|
||||||
|
print("time temp freq ceiling (1 sample/s, ");
|
||||||
|
put_u64(seconds);
|
||||||
|
print(" s)\n");
|
||||||
|
|
||||||
|
for (uint64_t t = 0; t < seconds; t++) {
|
||||||
|
if (!read_info(info)) break;
|
||||||
|
print_watch_line(info, t);
|
||||||
|
montauk::sleep_ms(1000);
|
||||||
|
}
|
||||||
|
montauk::exit(0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user