jcp-energy: RAPL reads each package's energy-status MSR where powercap lists none #625
2 changed files with 945 additions and 98 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue