在 Linux 上对 CPU 功耗进行带内测量与限制

 

在高性能计算场景中,能有效地测量和控制 CPU 功耗对于优化性能和能效至关重要。硬件厂商(如 Intel、AMD)提供了一些接口和工具,允许用户在不依赖外部硬件的情况下,进行带内(inband)的 CPU 功耗测量与限制。本文将简述常用的方法和工具。

基本概念

现代 CPU 的 TDP(热设计功耗)动辄上百瓦,服务器 CPU 更是达到了几百瓦的量级(如:AMD EPYC™ 9965 和 Intel® Xeon® 6962P Processor 的 TDP 都高达 500W)。再加上 turbo boost(睿频)等技术的加持,实际的 CPU 瞬时运行功耗也可能远超标称的 TDP。在特定场景下,管理员需要对 CPU 的功耗进行监控和限制,以防止过热、节省能源或优化性能。

X86 CPU 的大部分状态信息和控制接口都通过 MSR(Model-Specific Registers,型号特定寄存器)暴露给用户。Linux 提供了 msr 内核模块,可以通过它在用户态访问这些寄存器,也可以使用 rdmsrwrmsr 等工具帮助进行读写。硬件厂商通常还会通过专门的内核模块(如 intel_pstateamd_pstate 等)暴露更高层次的接口,如在 hwmon 子系统中提供功耗传感器,以进一步简化使用。

动态频率调整

现代 CPU 通常支持动态频率调整技术,允许 CPU 根据负载动态调整频率和电压,在有必要时超频或者降频,以在性能和功耗之间取得平衡。此技术在不同厂商、不同产品上有着多样的名字,如:

  • Intel: Turbo Boost(睿频)、Speed Shift
  • AMD: Turbo Core (EPYC)、Precision Boost (Ryzen)

因此,最简单的功耗控制方式是直接关闭睿频。如此,CPU 最高只在基频下运行,功耗通常不会超过其 TDP 数值。然而,这显然也会牺牲一些原本可通过频率调整获得的性能。除了在 BIOS 的电源管理 / CPU 设置中永久关闭睿频外,还可以通过软件的方式在 Linux 中进行控制:

  • Intel:在加载 intel-pstate 模块后,可通过向 /sys/devices/system/cpu/intel_pstate/no_turbo 写入 1 来关闭睿频,写入 0 则开启。
  • AMD:不同代 CPU 的控制方式不同。部分家用 Ryzen CPU 可通过 amd-pstateamd_pstate_epp 等模块控制,并提供 /sys/devices/system/cpu/cpufreq/boost 的接口;但我在多个 AMD EPYC 处理器上,均无法使用此方法。

此外,控制 CPU 调速器(governor)也能在一定程度上影响功耗表现。常见的调速器有 performance(性能优先)、powersave(节能优先)、ondemand(按需调整)等,具体可通过 cpupower 工具进行查询和设置。此方式当然也无法直接限制功耗,也是通过影响频率来间接调节。

RAPL 技术

显然,上述的方法都并不令人满意,因此现代 X86 CPU 引入了 RAPL (Running Average Power Limit) 技术,可通过硬件直接对功耗进行测量与限制,并能区分多个电源域(power domain)。RAPL 最初由 Intel 在 Sandy Bridge 架构中引入,随后 AMD 也在 Zen 2 架构中实现了类似的功能。RAPL 也通过 MSR 寄存器暴露给用户,可以读取各个功耗域的能耗数据,并设置功耗限制。

虽然 Intel 的文档厚得可怕,还是有不少文章对 RAPL 进行了很不错的介绍,例如《RAPL Interface 介绍》《CPU架构——RAPL,愿功耗被温柔以待》 等。简而言之,RAPL 将单个 CPU 抽象为多个组成部分,包括核心(Core)、内存(DRAM)、SoC 等,每个部分都可以单独进行功耗测量和限制,当然也可以以整个 socket 为粒度。功耗通常分为三个级别:PL1、PL2、PL3,分别对应长期、短期和超短期的功耗限制。通过设置不同的 PL 值,可以实现对 CPU 功耗的精细控制。最简单地,如果将三个值设置为 CPU 的 TDP,则可预期 CPU 的功耗不会超过其标称值,但在允许范围内依旧具有睿频的能力。

注:作为已经叛逃的 XPS 受害者和 ThrottleStop 用户,我对 PL1、PL2 这些名词自然是不能更熟悉。不过直到在摸到服务器 CPU 后,我才真正理解了这些概念。

RAPL on Intel

Linux 内核从 3.13 开始加入了 intel_rapl 模块,支持 Intel CPU 的 RAPL 功能。加载该模块后,可以在 /sys/devices/virtual/powercap 目录下找到相关的接口。每个功耗域都有一个对应的子目录,里面包含了多个文件,用于读取能耗数据和设置功耗限制。关于功耗域的更多讨论,可以阅读 Explanation on RAPL / Running Average Power Limit domains: what we (think we) know so far

例如,在一台有双路 Intel(R) Xeon(R) Gold 6252 CPU 的服务器上,这个目录的结构如下(方括号中是文件内容,省略了一些不重要的内容):

/sys/devices/virtual/powercap/intel-rapl
├── enabled
├── intel-rapl:0
│   ├── constraint_0_max_power_uw   [150000000]
│   ├── constraint_0_name           [long_term]
│   ├── constraint_0_power_limit_uw [150000000]
│   ├── constraint_0_time_window_us [55967744]
│   ├── constraint_1_max_power_uw   [376000000]
│   ├── constraint_1_name           [short_term]
│   ├── constraint_1_power_limit_uw [180000000]
│   ├── constraint_1_time_window_us [20468203520]
│   ├── device -> ../../intel-rapl
│   ├── enabled                     [1]
│   ├── energy_uj                   [252257431123]
│   ├── max_energy_range_uj         [262143328850]
│   ├── name                        [package-0]
│   ├── intel-rapl:0:0
│   │   ├── constraint_0_max_power_uw    [41250000]
│   │   ├── constraint_0_name            [long_term]
│   │   ├── constraint_0_power_limit_uw  [0]
│   │   ├── constraint_0_time_window_us  [976]
│   │   ├── device -> ../../intel-rapl:0
│   │   ├── enabled                      [1]
│   │   ├── energy_uj                    [63753574314]
│   │   ├── max_energy_range_uj          [65712999613]
│   │   ├── name                         [dram]
│   │   ├── power/ (...)
│   │   ├── subsystem -> ../../../../../../class/powercap
│   │   └── uevent
│   ├── power/ (...)
│   ├── subsystem -> ../../../../../class/powercap
│   └── uevent
├── intel-rapl:1/ (...)

可以看到,每个 CPU socket(package)都有对应的目录(如 intel-rapl:0),里面包含了该 socket 的功耗限制和能耗数据。此外,还有一个子目录(如 intel-rapl:0:0)表示 DRAM 功耗域。通过读取 energy_uj 文件,可以获取该域的累计能耗(单位为微焦耳)。大部份文件是只读的,可读写的文件有两个:

  • constraint_X_power_limit_uw:对应级别的功耗限制(单位为微瓦)。
  • constraint_X_time_window_us:对应级别的时间窗口(单位为微秒),但并非支持任意值,读取后再写入可以看到生效的值。

更具体的介绍可见 Linux 内核 powercap 文档。

从上面的输出看,我的 CPU 支持两个级别的功耗限制:PL1 为每 56s 窗口内允许最大 150W 的平均功耗,目前也设置为 150W;PL2 为最大 376W,目前设置为 180W(读到的 time_window_us 值比较奇怪,实际这个值不会高于处理器的 tau time,通常只有若干秒)。从计数器重置以来,这个 CPU socket 已经消耗了约 252kJ 的能量,其中内存使用了约 63.7kJ。

在更新的 CPU 上,RAPL 功耗域和可调整的级别都可能更多,如 Xeon 5 (Sapphire Rapids) 支持 PL3 级别,能控制更短期的功耗峰值(通常是若干微秒)。调整这些数值时,需要确保:

  • 数值不能超过硬件支持的最大值(由 constraint_X_max_power_uw 可知);
  • 数值符合逻辑(即 PL1 ≤ PL2 ≤ PL3,并且任意子组件的功耗限制不高于整体的限制);
  • 整个系统的散热能力充足;

此外需要注意的是,RAPL 也可以被 BIOS 的电源管理相关特性所锁定,只允许用户读取能量,而不能控制功耗。这在 Dell 的服务器上尤其常见,此时相关 enabled 文件的值是 0,并且无法通过写入 1 来启用;此外各个 constraint_* 文件也会消失。此时,也可能在内核日志中发现类似如下的警告:

[  +0.018363] intel_rapl_common: Found RAPL domain package
[  +0.000467] intel_rapl_common: Found RAPL domain dram
[  +0.000407] intel_rapl_common: DRAM domain energy unit 15300pj
[  +0.000002] intel_rapl_common: RAPL package-0 domain package locked by BIOS

有趣的是,RAPL 由于会暴露 CPU 的结构和负载情况(精确能量消耗),并且读取无需特权,也构成了一种有效的侧信道。例如,PLATYPUS 就是一种利用 RAPL 读数对 SGX 负载进行侧信道攻击的方法。为此,Intel 发布了相关的安全公告,也有相关的 CVE 编号(CVE-2020-8694, CVE-2020-8695)。对应的缓解措施包括在启用 SGX 时,降低能量更新频率、增加随机噪声等。果然任何东西只要能测量得越精确,安全问题也会越多。

RAPL on AMD

正如上面所述,AMD 从 Zen2 开始也加入了 RAPL 支持。然而,毫无意外地,AMD 对此的支持也非常碎片化。我将尝试总结各路文档,进行简要的说明。

AMD 的主要工具是 e_smi_lib,全称是 EPYC™ System Management Interface。根据此项目的 README,RAPL 在不同代处理器上的支持情况如下:

  • AMD family 17h, model 30h (Zen2); AMD family 19h, model 00-0fh and 30-3fh (Zen3): 仅支持能量计数器读取,不支持功耗限制设置。并且此时处理器中的能量计数器是 32 位的 MSR,很容易溢出;通过安装 amd_energy 模块(树外模块,需要通过 DKMS 编译加载),可以在 hwmon 子系统中提供准确的能量传感器读数(模块会定期处理溢出回绕,从而实现正确统计)。
  • AMD family 19h, model 10-1fh (Zen4), a0-afh (Zen4C) and 90-9fh (MI300A APU): 能量计数器变为 64 位 MSR,无需额外处理即可准确统计。因此依旧可以使用 amd_energy 模块为 hwmon 提供读数,也可以直接读取对应 MSR(msr 或者 msr_safe 模块,发行版内核通常存在)获取数值。
  • AMD family 0x1A, model 00-1fh (Zen5), 50-5fh (Zen6): 支持上述的 64 位 MSR,同时支持通过 HSMP Mailbox 读取功耗和设置限制,此时需要 amd_hsmp 模块的支持。尽管此模块从 Linux 5.18 起已经包含于上游,很多发行版并未启用,因此也可以使用 DKMS 版本。
    • HSMP 功能需要在 BIOS 中启用后才能使用,选项为 Advanced > AMD CBS > NBIO Common Options > SMU Common Options > HSMP Support。部分 BIOS 可能隐藏了 AMD CBS 菜单,可以通过杰哥的奇妙小工具 uefitools 尝试进入,风险自负

上文中提到的相关 MSR 为:

  • MSR_RAPL_POWER_UNIT (0xC0010299): 用于获取功耗单位、能量单位。
  • MSR_CORE_ENERGY_STATUS (0xC001029A): 用于读取 core 级别的能量计数器。
  • MSR_PACKAGE_ENERGY_STATUS (0xC001029B): 用于读取 socket 级别的能量计数器。

具体从读数到能耗的换算方式可见 amd_energy 模块的文档。在加载此模块后,可以在 /sys/class/hwmon/hwmonX/ 目录下找到相关的能量传感器文件。如在具有双路 EYPC 7763 处理器的服务器上,可以看到:

/sys/class/hwmon/hwmon8
├── device -> ../../../amd_energy.0
├── energy1_input   [41467652191]
├── energy1_label   [Ecore000]
├── ...
├── energy128_input [50239882568]
├── energy128_label [Ecore127]
├── energy129_input [9981788288726]
├── energy129_label [Esocket0]
├── energy130_input [9467966267730]
├── energy130_label [Esocket1]
├── name            [amd_energy]
├── power
│   ├── async
│   ├── autosuspend_delay_ms
│   ├── control
│   ├── runtime_active_kids
│   ├── runtime_active_time
│   ├── runtime_enabled
│   ├── runtime_status
│   ├── runtime_suspended_time
│   └── runtime_usage
├── subsystem -> ../../../../../class/hwmon
└── uevent

可见此机器的 socket0 已经消耗了约 9.98MJ 的能量(约等于 2.77kWh),socket1 消耗了约 9.47MJ(约等于 2.63kWh)。

此外,e_smi_lib 中的工具也可以读取:

# ./e_smi_tool

============================= E-SMI ===================================

Error in initialising HSMP version sepcific info, Only energy data can be obtained...
Err[3]: HSMP driver not present

--------------------------------------
| CPU Family            | 0x19 (25 ) |
| CPU Model             | 0x1  (1  ) |
| NR_CPUS               | 128        |
| NR_SOCKETS            | 2          |
| THREADS PER CORE      | 1 (SMT OFF)|
--------------------------------------

------------------------------------------------------------------------
| Sensor Name                    | Socket 0         | Socket 1         |
------------------------------------------------------------------------
| Energy (K Joules)              | 10027.166        | 9511.913         |
| Power (Watts)                  | NA (Err: 20)     | NA (Err: 20)     |
| PowerLimit (Watts)             | NA (Err: 20)     | NA (Err: 20)     |
| PowerLimitMax (Watts)          | NA (Err: 20)     | NA (Err: 20)     |
| C0 Residency (%)               | NA (Err: 20)     | NA (Err: 20)     |
------------------------------------------------------------------------

由于与功耗控制相关的 HSMP Mailbox 在 7763 (Zen3) 上尚不可用,因此工具只能读取能量数据,无法读取功耗和设置限制。在更新的处理器上(如 Zen5),只要正确配置了 HSMP 驱动,这些功能也是可用的。AMD 的 RAPL 设置的功耗限制并没有 Intel 精细,只有单一的功耗限制值,没有区分级别。

与 Intel 的情况类似,直接读取精确的能耗数据也可能带来侧信道攻击的风险。AMD 的选择是让这些文件默认只有 root 可读,管理员可以为其他用户/组设置合适的权限。

其他

awesome-energy 项目收集了大量与能耗测量和管理相关的工具和资源,感兴趣的读者也可以前往查看。