【調査】Linux 6.13 から turbostat でシステム全体の消費電力が取れるようになった話

結論

Linux カーネルのソースツリー内にある監視/管理ツール「turbostat」に、Linux 6.13 からシステム全体の消費電力を示す「SysWatt」という新しいカウンターが追加されます。

ただし、SysWattの取得には比較的最近のCPU(Xeonだと4世代以降)のMSRが必要なため、デフォルトで無効になっています。

対象コミット: https://github.com/torvalds/linux/commit/e5f687b8

詳細

ipmitool について

システムの消費電力を取る方法として、turbostat の他にも ipmitool で取得するという手法があります。
私も本業でサーバーの管理/運用を行っていますが、現場ではこちらの方がよく使われるイメージです。

turbostat の数値は主に CPU の MSR から、ipmitool はハードウェアのセンサーから取得しています。
実際に ipmitool sdr を strace で追ってみると、以下のようになります。

# strace ipmitool sdr
execve("/usr/bin/ipmitool", ["ipmitool", "sdr"], 0x7ffcaae22ec8 /* 14 vars */) = 0
brk(NULL)           = 0x55d68eaa5000
...
brk(0x55d68eac6000) = 0x55d68eac6000
openat(AT_FDCWD, "/dev/ipmi0", O_RDWR) = 3
ioctl(3, IPMICTL_SET_GETS_EVENTS_CMD, 0x7ffd6a7b6bc1) = 0
ioctl(3, IPMICTL_SET_MY_ADDRESS_CMD, 0x7ffd6a7b6ae4)  = 0
ioctl(3, IPMICTL_SEND_COMMAND, 0x7ffd6a7b6980)        = 0
pselect6(4, [3], NULL, NULL, {tv_sec=15, tv_nsec=0}, NULL) = 1
ioctl(3, IPMICTL_RECEIVE_MSG_TRUNC, 0x7ffd6a7b69b0) = 0
ioctl(3, IPMICTL_SEND_COMMAND, 0x7ffd6a7b6a30) = 0
pselect6(4, [3], NULL, NULL, {tv_sec=15, tv_nsec=0}, NULL) = 1
ioctl(3, IPMICTL_RECEIVE_MSG_TRUNC, 0x7ffd6a7b6a60) = 0
ioctl(3, IPMICTL_SEND_COMMAND, 0x7ffd6a7b6a30) = 0

/dev/ipmi0 に対して ioctl を発行し、デバイスからデータを取得しているのがわかります。

turbostat について

turbostat では MSR (Model Specific Registers)1 からデータを取得します。
より具体的には、デバイスファイル /dev/cpu/[n]/msr からデータを読み取ります。

現時点でCPUの消費電力(PkgWatt)は既に取れるのですが、Linux 6.13からシステムの消費電力(SysWatt)が取れるようになります。

Intel SDMから見るMSR

Intel CPU の MSR は同社が出している Software Developer Manuals で確認できます。
Intel® 64 and IA-32 Architectures Software Developer Manuals

PkgWatt



MSR_PKG_ENERGY_STATUS[31:0] に今までCPUの消費したエネルギーが記録されています。
説明文にある通り、単位はMSR_RAPL_POWER_UNITの”Energy Status Units”で表現されます。


消費電力の単位 “Energy Status Units” は MSR_RAPL_POWER_UNIT[12:8] です。
ジュール(J)で表現され、5bit の数値(ESU)を利用して 0.5^ESU したのが単位となります。

コードにするとこんな感じ

MSR_RAPL_POWER_UNIT = 0x606
MSR_PKG_ENERGY_STATUS = 0x611  # Whole package
MSR_PP0_ENERGY_STATUS = 0x639  # Core
MSR_PP1_ENERGY_STATUS = 0x641  # Uncore
MSR_DRAM_ENERGY_STATUS = 0x619

def _EnergyMultiplier():
  return 0.5 ** ((_ReadMsr(MSR_RAPL_POWER_UNIT) >> 8) & 0x1f)

def _PackageEnergyJoules():
  return _ReadMsr(MSR_PKG_ENERGY_STATUS) * _EnergyMultiplier()

def _JoulesToMilliwattHours(value_joules):
  return value_joules * 1000 / 3600.

参考: msr_power_monitor.py#110

CPUパッケージ全体の消費電力を取る MSR_PKG_ENERGY_STATUS の他にも、コアのみ/コア以外/DRAMの消費電力を取る MSR も用意されているがここでは割愛する。

SysWatt


PkgWatt と同様に MSR_PLATFORM_ENERGY_STATUS[31:0] に消費エネルギーが格納されます。
表のタイトルから、Xeonの4/5世代でサポートされているのがわかります。
また、単位については他のMSRではなく直接ジュールで記録されているようです。

一方でMSRの追加コミットを追ってみると、SkyLake以降から対応しているようです。
powercap: intel_rapl: PSys support
turbostatへの追加コミットを見ても、skl, cnl, icx, spr, srf, grr を対象としているのがわかります。
https://github.com/torvalds/linux/commit/e5f687b8

そこで、該当 MSR のアドレス(0x64D)で再度確認したところ、どうやら異なるMSR名で定義されているようです(MSR_PLATFORM_ENERGY_COUNTER)


どうやらSkylake以降で定義されているものの、実装されているかはベンダーに依存しており、確実にMSRの存在を保証できるのはXeonの4/5世代らしい…
なお、単位についてはPkgWattと同様にRAPL_POWER_UNIT.Energy_Status_Unitを利用するようです。

一応、テーブル名を見るとXeon4/5世代も含まれているように見えますが、流石に誤表記だと思います(Xeon 4/5世代とは単位が異なるため)

動作確認用プログラムを作る

Go言語でMSRからシステム消費電力を取得するプログラムを書いてみました。

package main

import (
    "bufio"
    "encoding/binary"
    "fmt"
    "log"
    "math"
    "os"
    "strings"
    "time"
)

func rdmsr(ptr int64) uint64 {
    path := fmt.Sprintf("/dev/cpu/%d/msr", 0)
    fd, err := os.OpenFile(path, os.O_RDONLY, os.ModeDevice)
    if err != nil {
        log.Printf("failed to open %s: %v", path, err)
        return 0;
    }
    defer fd.Close()
    d := make([]byte, 8)
    fd.ReadAt(d, ptr)
    ret := binary.LittleEndian.Uint64(d)
    fmt.Printf("MSR 0x%x: 0x%08x\n", ptr, ret)
    return ret
}

func getCPuModelName() (string, error) {
    file, err := os.Open("/proc/cpuinfo")
...
}

func main() {
    modelName, err := getCPUModelName()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("CPU Model Name: %s\n", modelName)
    
    energy_unit := math.Pow(0.5, float64(rdmsr(0x606)>>8)&0x1F)) // MSR_RAPL_POWER_UNIT
    fmt.Printf("energy_unit: %f\n", energy_unit)

    pkg_energy_start := rdmsr(0x611) & 0xFFFFFFFF // MSR_PKG_ENERGY_STATUS
    platform_energy_start := rdmsr(0x64D) & 0xFFFFFFFF // MSR_FLATFORM_ENERGY_STATUS

    time.Sleep(1 * time.Second)

    pkg_energy_end := rdmsr(0x611) & 0xFFFFFFFF // MSR_PKG_ENERGY_STATUS
    platform_energy_end := rdmsr(0x64D) & 0xFFFFFFFF // MSR_FLATFORM_ENERGY_STATUS

    pkg_watt := float64(pkg_energy_end - pkg_energy_start) * energy_unit
    platform_watt := platform_energy_end - platform_energy_start
    fmt.Printf("PkgWatt: %f, SysWatt: %d\n", pkg_watt, platform_watt)
}

実行結果(5世代Xeon)

正しくデータが取れているのが確認できます。
システム全体の消費電力は ipmitool に近く、PkgWattはturbostatと同じ数値が取れました。

気になる点としてはMSR経由の情報とipmi経由の情報で10Wほどズレてる点でしょうか。利用しているデータの大元が異なるようです。
CPUに入ってくるデータと実際のセンサーのデータなら後者の方が信頼性は高そうですが…

# ./main
CPU Model Name: INTEL(R) XEON(R) GOLD 5512U
MSR 0x606: 0x000a0e03
energy_unit: 0x000061
MSR 0x611: 0xb7e0a59d
MSR 0x64d: 0xffbc34000381aafa
MSR 0x611: 0xb7f60957
MSR 0x64d: 0x5adf7000381ab8b
PkgWatt: 85.558228, SysWatt: 145

# ipmitool sdr | grep Watt
Pwr Consumption  | 156 Watts | ok

# turbostat --quiet --show PkgWatt
PkgWatt
85.58
85.58

実行結果(1世代Xeon)

こちらはMSR_PLATFORM_ENERGY_STATUSからデータを取れませんでした。

# ./main
CPU Model Name: Intel(R) Xeon(R) Gold 6138 CPU @ 2.00GHz
MSR 0x606: 0x000a0e03
energy_unit: 0x000061
MSR 0x611: 0xed63f36a
MSR 0x64d: 0x00000000
MSR 0x611: 0x3d7113bb
MSR 0x64d: 0x00000000
PkgWatt: 52.504944, SysWatt: 0

# ipmitool sdr | grep Watt
PSU1 Power      | 198 Watts | ok
Total Power     | 198 Watts | ok
Total Power Out | 186 Watts | ok

# turbostat --quiet --show PkgWatt
PkgWatt
94.34
52.29
42.05

参考サイト

  1. その名の通りCPU固有のレジスタ群です。世代や型番によって追加/削除/変更されたりします。 ↩︎

シェアする

  • このエントリーをはてなブックマークに追加

フォローする