jcp-energy: RAPL reads each package's energy-status MSR where powercap lists none #625

Merged
dcharlot merged 2 commits from jcp-energy/rapl-msr into main 2026-11-05 04:52:42 -05:00
2 changed files with 945 additions and 98 deletions

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,9 @@
//! rung must be selected, and a span over a CPU burn must be `HwShunt`,
//! `Whole` (the package counters read the CPU work they meter), its figure
//! inside the oracle's package-only bracket and clearly below package+psys
//! and package+core.
//! and package+core. Where powercap lists no package zone (a kernel without
//! its RAPL driver), the oracle reads each package's energy-status MSR itself
//! (`oracle::msr`), and the same verdict and bracket are taken over those.
#![cfg(target_os = "linux")]
@ -119,9 +121,14 @@ mod oracle {
z.dir.strip_prefix("intel-rapl:").is_some_and(digits)
}
/// A package zone: top-level, named `package-<n>`.
/// A package zone: top-level, named `package-<n>`, or `package-<n>-die-<m>`
/// (one zone per die, as the kernel lists a multi-die Intel package).
pub fn is_package(z: &Zone) -> bool {
top_level(z) && z.name.as_deref().and_then(|n| n.strip_prefix("package-")).is_some_and(digits)
top_level(z)
&& z.name.as_deref().and_then(|n| n.strip_prefix("package-")).is_some_and(|rest| match rest.split_once("-die-") {
Some((package, die)) => digits(package) && digits(die),
None => digits(rest),
})
}
/// The platform zone.
@ -195,19 +202,34 @@ mod oracle {
/// between samples (capped at 500 ms of wall time), until every package
/// has advanced in at least five intervals, at least 80% of those
/// observed, over at least 300 ms; or one second has passed with at least
/// five intervals; or five seconds have.
/// five intervals; or five seconds have. Where this host's powercap lists
/// no package zone and its packages' energy-status MSRs read
/// ([`msr::packages`]), the same run over those.
pub fn watch(root: &Path) -> Watch {
let all = zones(root);
let packages: Vec<&Zone> = all.iter().filter(|z| is_package(z)).collect();
let names: Vec<String> = packages.iter().map(|z| z.dir.clone()).collect();
let none = |verdict| Watch { verdict, packages: names.clone(), intervals: 0, advances: Vec::new(), window: Duration::ZERO };
if packages.is_empty() {
if root == Path::new(POWERCAP) {
if let Some(msrs) = msr::packages() {
let names = msrs.iter().map(|p| p.name.clone()).collect();
return watch_reads(names, || msr::read(&msrs));
}
}
let readable = all.iter().any(|z| top_level(z) && z.energy_uj.is_some());
return none(if readable { Verdict::NoPackage } else { Verdict::Unreadable });
}
watch_reads(names, || read(root, &packages))
}
/// The sustained-advance run of [`watch`] over the packages `names`,
/// sampled by `read`.
fn watch_reads(names: Vec<String>, read: impl Fn() -> Option<Vec<u64>>) -> Watch {
let none = |verdict| Watch { verdict, packages: names.clone(), intervals: 0, advances: Vec::new(), window: Duration::ZERO };
let started = Instant::now();
let Some(mut prev) = read(root, &packages) else { return none(Verdict::Unreadable) };
let mut advances = vec![0u32; packages.len()];
let Some(mut prev) = read() else { return none(Verdict::Unreadable) };
let mut advances = vec![0u32; names.len()];
let mut intervals = 0u32;
let live = |adv: &[u32], n: u32, w: Duration| {
w >= Duration::from_millis(300) && adv.iter().all(|&a| a >= 5 && u64::from(a) * 5 >= u64::from(n) * 4)
@ -218,7 +240,7 @@ mod oracle {
break;
}
burn(Duration::from_millis(25), Duration::from_millis(500));
if let Some(now) = read(root, &packages) {
if let Some(now) = read() {
intervals += 1;
for ((a, n), p) in advances.iter_mut().zip(&now).zip(&prev) {
*a += u32::from(n > p);
@ -370,6 +392,110 @@ mod oracle {
let o3 = read_all(&set);
(Bracket { set, reads: [o0, o1, o2, o3] }, m0, m1)
}
/// The tests' own reading of the packages' energy-status MSRs, for a host
/// whose powercap lists no package zone (a kernel without the powercap
/// RAPL driver): the CPU's vendor and family from `/proc/cpuinfo`, each
/// package's first CPU from `/sys/devices/system/cpu`, each read through
/// `/dev/cpu/<n>/msr` at the register's offset. Nothing in it comes from
/// the crate under test.
pub mod msr {
use std::os::unix::fs::FileExt;
use std::path::PathBuf;
/// One counter domain's energy-status MSR: a die on Intel, a package on
/// AMD and Hygon (the kernel's RAPL scope).
#[derive(Debug, Clone)]
pub struct Package {
/// Its name: `package-<n> msr`, or `package-<n>-die-<m> msr` for a
/// die of a multi-die Intel package.
pub name: String,
/// The `msr` device file of its first CPU.
pub device: PathBuf,
/// The energy-status register.
pub register: u64,
/// The energy-status unit, 2^-`shift` J per count.
pub shift: u32,
}
fn msr(device: &std::path::Path, register: u64) -> Option<u64> {
let mut raw = [0u8; 8];
std::fs::File::open(device).ok()?.read_exact_at(&mut raw, register).ok()?;
Some(u64::from_le_bytes(raw))
}
/// Every counter domain's energy-status MSR, in domain order: Intel
/// family 6 (0x606 unit, 0x611 count), each die (`topology/die_id`, 0
/// where absent) of each package; AMD family 0x17 and later or Hygon
/// (0xC0010299, 0xC001029B), each package. `None` when the CPU is
/// another, or any domain's unit does not read or reads zero.
pub fn packages() -> Option<Vec<Package>> {
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").ok()?;
let field = |key: &str| {
cpuinfo.lines().find_map(|l| l.split_once(':').filter(|(k, _)| k.trim() == key).map(|(_, v)| v.trim()))
};
let family: u32 = field("cpu family")?.parse().ok()?;
let (unit, register, per_die) = match field("vendor_id")? {
"GenuineIntel" if family == 6 => (0x606, 0x611, true),
"AuthenticAMD" | "HygonGenuine" if family >= 0x17 => (0xC001_0299, 0xC001_029B, false),
_ => return None,
};
let number = |p: PathBuf| std::fs::read_to_string(p).ok().and_then(|s| s.trim().parse::<u32>().ok());
let mut first = std::collections::BTreeMap::<(u32, u32), u32>::new();
for e in std::fs::read_dir("/sys/devices/system/cpu").ok()?.flatten() {
let name = e.file_name().to_string_lossy().into_owned();
let Some(cpu) = name.strip_prefix("cpu").and_then(|n| n.parse::<u32>().ok()) else { continue };
let Some(package) = number(e.path().join("topology/physical_package_id")) else { continue };
let die = if per_die { number(e.path().join("topology/die_id")).unwrap_or(0) } else { 0 };
let lowest = first.entry((package, die)).or_insert(cpu);
*lowest = (*lowest).min(cpu);
}
if first.is_empty() {
return None;
}
let dies = |package: u32| first.keys().filter(|(p, _)| *p == package).count();
first
.iter()
.map(|(&(package, die), &cpu)| {
let device = PathBuf::from(format!("/dev/cpu/{cpu}/msr"));
let shift = ((msr(&device, unit)? >> 8) & 0x1F) as u32;
let name =
if dies(package) > 1 { format!("package-{package}-die-{die} msr") } else { format!("package-{package} msr") };
(shift != 0).then_some(Package { name, device, register, shift })
})
.collect()
}
/// One package's count in µJ: its 32-bit count, floored at its unit.
fn uj(p: &Package, raw: u64) -> u64 {
((u128::from(raw & 0xFFFF_FFFF) * 1_000_000) >> p.shift) as u64
}
/// Every package's count in µJ, in order; `None` if one fails.
pub fn read(ps: &[Package]) -> Option<Vec<u64>> {
ps.iter().map(|p| msr(&p.device, p.register).map(|raw| uj(p, raw))).collect()
}
/// The packages' energy between two reads, in µJ, each wrapping at
/// 2^32 counts.
pub fn energy(ps: &[Package], before: &[u64], after: &[u64]) -> u64 {
ps.iter()
.zip(before.iter().zip(after))
.map(|(p, (&b, &a))| {
let range = (((1u128 << 32) * 1_000_000) >> p.shift) as u64;
if a >= b { a - b } else { range - b + a }
})
.sum()
}
/// The packages, as one line for the log.
pub fn listing(ps: &[Package]) -> String {
ps.iter()
.map(|p| format!("{} {:#x} on {} at 2^-{} J", p.name, p.register, p.device.display(), p.shift))
.collect::<Vec<_>>()
.join("; ")
}
}
}
use oracle::Verdict;
@ -382,13 +508,23 @@ fn the_rapl_status_agrees_with_the_oracle_and_gates_the_rung() {
let status = rapl_status();
let watch = oracle::watch(Path::new(oracle::POWERCAP));
let (meter, why) = Meter::detect_explained();
let msrs = oracle::msr::packages();
eprintln!(
"rapl hardware: {}; oracle {}; status={status:?} selected={:?} provenance={:?} detection={why:?}",
"rapl hardware: {}; msr: {}; oracle {}; status={status:?} access={:?} selected={:?} provenance={:?} \
detection={why:?}",
oracle::listing(Path::new(oracle::POWERCAP)),
msrs.as_deref().map_or("unreadable".to_string(), oracle::msr::listing),
watch.summary(),
jcp_energy::rapl_access(),
meter.rung(),
meter.provenance()
);
// Where powercap lists no package and the oracle reads every package's
// MSR, the crate can read them too.
let no_zone = !oracle::zones(Path::new(oracle::POWERCAP)).iter().any(oracle::is_package);
if no_zone && msrs.is_some() {
assert_eq!(jcp_energy::rapl_access(), jcp_energy::RaplAccess::Readable, "the oracle read the MSRs");
}
assert_eq!(status == CounterStatus::Live, watch.verdict == Verdict::Live, "the status disagrees with the oracle");
match watch.verdict {
Verdict::Unreadable | Verdict::NoPackage => assert_eq!(status, CounterStatus::Unavailable),
@ -419,6 +555,33 @@ fn a_cpu_burn_is_a_whole_hwshunt_reading_inside_the_package_only_bracket() {
return;
}
let all = oracle::zones(Path::new(oracle::POWERCAP));
if !all.iter().any(oracle::is_package) {
// No package zone: the rung reads the packages' MSRs, and so does the
// oracle's bracket.
let msrs = oracle::msr::packages().expect("the RAPL rung was selected with no package zone, so the MSRs read");
let read = || oracle::msr::read(&msrs).expect("an oracle read of the MSRs");
let wall = Instant::now();
let o0 = read();
let ((o1, cpu, o2), e) = meter.measure(|| {
let o1 = read();
let cpu = oracle::burn(Duration::from_millis(300), Duration::from_secs(10));
(o1, cpu, read())
});
let o3 = read();
let (inner, outer) = (oracle::msr::energy(&msrs, &o1, &o2), oracle::msr::energy(&msrs, &o0, &o3));
assert_eq!((e.provenance, e.coverage), (Provenance::HwShunt, Coverage::Whole), "a live RAPL span: {e:?}");
let uj = e.measured_uj.expect("an HwShunt figure carries its measured figure");
let line = format!("meter {uj} µJ; oracle MSR package-only inner {inner} outer {outer} µJ");
assert!(inner > 0, "the packages did not advance over the burn: {line}");
assert!(inner <= uj && uj <= outer, "the figure is outside the package-only bracket: {line}");
eprintln!(
"rapl burn: {cpu:?} of thread CPU in {:?}; energy {e:?}; {}; {line}",
wall.elapsed(),
oracle::msr::listing(&msrs)
);
assert!(cpu >= Duration::from_millis(300), "the burn got only {cpu:?} of CPU");
return;
}
let set = oracle::bracket_set(&all);
let wall = Instant::now();
let o0 = oracle::read_all(&set);