All checks were successful
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 43s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 11s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 38s
wai-quantum WebGPU member / does this change touch it (pull_request) Successful in 10s
standards workspaces build + test / arl-rs (arl/arl-rs, arl-rs, linux) (pull_request) Successful in 1m16s
energy meters on real hardware / sink decode table · per-task energy and the GPU rail on macOS (pull_request) Successful in 2m45s
standards workspaces build + test / joulespec-proofs (joulespec/joulespec-rs, joulespec-proofs, true, macos) (pull_request) Successful in 2m9s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m9s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m18s
energy meters on real hardware / sink decode table · RAPL on jetson-hub (NVML surveyed) (pull_request) Successful in 3m1s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 2m4s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 57s
standards workspaces build + test / pool-rs (pool/pool-rs, pool-rs, linux) (pull_request) Successful in 59s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 1m23s
standards workspaces build + test / sense-rs (sense/sense-rs, sense-rs, linux) (pull_request) Successful in 48s
standards workspaces build + test / proof-rs (proof/conformance/v1/joulespec.toml, proof-conformance, proof/proof-rs, proof-rs, linux, true) (pull_request) Successful in 1m4s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 3m8s
standards workspaces build + test / eoc-rs (eoc/eoc-rs, -p eoc-meter --features cuda, eoc-rs, linux) (pull_request) Successful in 4m40s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 1m42s
standards workspaces build + test / joulespec-rs (true, joulespec/joulespec-rs, true, joulespec-rs, linux) (pull_request) Successful in 2m30s
standards workspaces build + test / smart-byte-rs (smart-byte/smart-byte-rs, smart-byte-rs, linux) (pull_request) Successful in 4m25s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, -p sandbox-tee --features amd-kds,intel-pcs, sandbox-rs, linux) (pull_request) Successful in 5m19s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness (pull_request) Successful in 6m30s
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 11m38s
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Successful in 10m56s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 11m21s
wai-quantum WebGPU member / member · GPU, Vulkan (jetson-hub) (pull_request) Successful in 17m37s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m13s
standards workspaces build + test / joulecontract-rs (joulecontract/joulecontract-rs, joulecontract-rs, linux-arm64, true) (pull_request) Successful in 1m3s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 1m1s
standards workspaces build + test / map-rs (map-conformance, map/map-rs, map-rs, linux-arm64) (pull_request) Successful in 1m45s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 3m32s
wai-quantum WebGPU member / member · GPU, Vulkan (thor) (pull_request) Successful in 24m45s
standards workspaces build + test / aac-rs (aac-conformance, aac/aac-rs, aac-rs, linux-arm64) (pull_request) Successful in 5m35s
standards workspaces build + test / openpay (openpay, openpay, linux-arm64) (pull_request) Successful in 4m5s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 6m38s
On Apple Silicon under macOS 27, IOReport's CPU rails are frozen for an unprivileged reader, so detect_meter fell back to the load-average estimator: a guess, graded Estimator. Every Mac still publishes its total power in the SMC key PSTR, which any process reads through the AppleSMCKeysEndpoint IOKit user client with no root. New feature `smc` (macOS), module apple_smc: SmcSystemCounter integrates PSTR over time. - Domain: a new EnergyDomain::System, the whole machine (everything its DC input feeds: SoC, memory, storage, fans, the rest of the board; not the power supply's AC-to-DC loss). It holds SocTotal and every other domain. - Grade: ModelBased, like NVML's power.draw integral. PSTR is a power reading, not an energy counter, so no liveness probe can vouch for its integral. - Sampling: one thread per process reads PSTR every 100 ms and integrates the value it holds, so a figure does not depend on how often its caller reads; a read between polls extends the integral at the held value, which the next poll integrates over the same time, so readings never run backwards. - Checks: PSTR must be a 4-byte float and be seen to refresh before the counter is available (the first construction waits about a second); samples at 0 W or less, above 1.5 kW, or NaN are discarded and counted; a value held 10 s gives no figure. - Minimum window 2 s: PSTR refreshes about once a second (median gap 1.0 s on an M3 Ultra Mac Studio and an M5 Max MacBook Pro, macOS 27.2) and trails a step in load by about one refresh. detect_meter puts it after a live IOReport counter (a hardware reading over a narrower domain) and before the load-average estimator. Its figure holds the work and whatever else the machine powered, so it can read high, never low. mesh-meter's exhaustive covers_whole_request counts System as covering the whole request. SPEC 1.3 lists sampled vendor power integrated over time under ModelBased. Measured with the energy_probe example (2.5 s CPU churn, unprivileged): the M3 Ultra Mac Studio 299.9 J, 120.0 W; the M5 Max MacBook Pro on AC 160.9 J, 64.4 W. On the Studio PSTR equals the DC input (PDTR); on the MacBook Pro, on AC with the battery not charging, it read 51.7 W against 54.9 W at the input. Tests: the integral, monotonic reads, implausible samples, staleness and the sampler's start are driven with scripted samples and times; a hardware test meters a CPU burn from the real SMC and prints "apple-smc: measured", which a new macOS CI step greps under --features smc and ioreport,smc. The clippy step lints both. Passing on the M3 Ultra (smc, ioreport,smc, ioreport; clippy clean in default, ioreport, smc, ioreport,smc; mesh-meter with hw-meter-apple) and on Linux x86_64 (rapl,nvml; default clippy; mesh-meter with hw-meter-rapl). Co-Authored-By: Claude Opus 5.5 <[email protected]>
176 lines
7 KiB
Rust
176 lines
7 KiB
Rust
//! Cross-platform energy probe — read the best available hardware counter on
|
|
//! this host and print its energy delta with its honest provenance.
|
|
//!
|
|
//! The point of the probe: show whether on real hardware JouleClaw reports a
|
|
//! `HwShunt` / `ModelBased` reading or an `Estimator` fallback, and label the
|
|
//! figure accordingly. Build with the feature that matches the host's counter:
|
|
//!
|
|
//! # NVIDIA discrete GPU (Linux or Windows — loads libnvidia-ml / nvml.dll):
|
|
//! cargo run -p jouleclaw-energy --release --example energy_probe --features nvml
|
|
//!
|
|
//! # Intel/AMD x86 on Linux (powercap sysfs RAPL):
|
|
//! cargo run -p jouleclaw-energy --release --example energy_probe --features rapl
|
|
//!
|
|
//! # Apple Silicon (IOReport Energy Model when its CPU rail is live, else the
|
|
//! # load-avg estimator, with the reason printed):
|
|
//! cargo run -p jouleclaw-energy --release --example energy_probe --features ioreport
|
|
//!
|
|
//! # Mac, whole machine (the SMC's `PSTR` integrated over time, ModelBased;
|
|
//! # with `ioreport` too, after a live IOReport counter):
|
|
//! cargo run -p jouleclaw-energy --release --example energy_probe --features smc
|
|
//!
|
|
//! It samples the counter across a ~2 s window (driving a small CPU workload so
|
|
//! the package/SoC is not fully idle) and prints the integrated microjoules. For
|
|
//! the integrating counters (NVML) the repeated reads during the window are what
|
|
//! build the energy integral.
|
|
|
|
use jouleclaw_energy::EnergyCounter;
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// Pick the first available real counter for the features this binary was built
|
|
/// with. Returns the counter and a short label.
|
|
fn pick_counter() -> Option<(&'static str, Box<dyn EnergyCounter>)> {
|
|
#[cfg(feature = "nvml")]
|
|
{
|
|
let c = jouleclaw_energy::nvml::NvmlCounter::new();
|
|
if c.is_available() {
|
|
// Report which NVML path reads — the cumulative-energy counter,
|
|
// only where it was observed live (HwShunt), or the power-draw
|
|
// integral (ModelBased) — and the counter's liveness.
|
|
println!(
|
|
"nvml path : {} (counter liveness: {:?})",
|
|
if c.uses_true_counter() {
|
|
"nvmlDeviceGetTotalEnergyConsumption (live cumulative counter, HwShunt)"
|
|
} else {
|
|
"power_usage() integration (ModelBased)"
|
|
},
|
|
c.liveness()
|
|
);
|
|
return Some(("nvml (NVIDIA GPU shunt)", Box::new(c)));
|
|
}
|
|
}
|
|
#[cfg(feature = "rapl")]
|
|
{
|
|
let c = jouleclaw_energy::rapl::RaplCounter::new();
|
|
println!("rapl : liveness {:?}", c.liveness());
|
|
if c.is_available() {
|
|
return Some(("rapl (x86 package MSR)", Box::new(c)));
|
|
}
|
|
}
|
|
#[cfg(all(feature = "ioreport", target_os = "macos"))]
|
|
{
|
|
// Prefer the real IOReport energy counter (HwShunt, like RAPL and NVML).
|
|
// It is unavailable unless IOReport opens AND its CPU energy rail
|
|
// advances under load — a counter that is present but not live gives no
|
|
// hardware-graded figure; say which before falling back to the load-avg
|
|
// estimator.
|
|
let c = jouleclaw_energy::apple_ioreport::AppleIoReportCounter::new();
|
|
if c.is_available() {
|
|
return Some(("apple-ioreport (Energy Model)", Box::new(c)));
|
|
}
|
|
println!("ioreport : declined ({:?})", c.status());
|
|
}
|
|
#[cfg(all(feature = "smc", target_os = "macos"))]
|
|
{
|
|
// The whole machine's power from the SMC, integrated over time: a vendor
|
|
// power reading, so ModelBased, over the System domain.
|
|
let c = jouleclaw_energy::apple_smc::SmcSystemCounter::new();
|
|
if c.is_available() {
|
|
println!("smc : PSTR {:.1} W now", c.watts().unwrap_or(0.0));
|
|
return Some(("apple-smc (SMC PSTR, whole machine)", Box::new(c)));
|
|
}
|
|
println!("smc : unavailable ({})", c.unavailable_reason().unwrap_or("?"));
|
|
}
|
|
#[cfg(all(feature = "ioreport", target_os = "macos"))]
|
|
{
|
|
let c = jouleclaw_energy::apple::AppleSiliconMeter::new();
|
|
if c.is_available() {
|
|
return Some(("apple (load-avg estimator)", Box::new(c)));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// A small CPU workload so the host is not fully idle during the window.
|
|
fn churn(deadline: Instant) -> u64 {
|
|
let mut acc: u64 = 0;
|
|
let mut x: u64 = 0x9E3779B97F4A7C15;
|
|
while Instant::now() < deadline {
|
|
for _ in 0..200_000 {
|
|
x ^= x << 13;
|
|
x ^= x >> 7;
|
|
x ^= x << 17;
|
|
acc = acc.wrapping_add(x);
|
|
}
|
|
}
|
|
acc
|
|
}
|
|
|
|
fn main() {
|
|
println!("== JouleClaw energy probe ==");
|
|
let Some((label, counter)) = pick_counter() else {
|
|
eprintln!(
|
|
"no hardware energy counter available on this host/build.\n\
|
|
rebuild with --features nvml|rapl|ioreport|smc for this platform."
|
|
);
|
|
std::process::exit(1);
|
|
};
|
|
|
|
println!("counter : {label}");
|
|
println!("provenance : {}", counter.provenance());
|
|
println!("domain : {:?}", counter.domain());
|
|
println!("resolution : {} uj", counter.resolution_uj());
|
|
println!("min window : {} ns", counter.min_window_ns());
|
|
|
|
// Prime the counter (first read seeds the integrator / cumulative baseline).
|
|
let _ = counter.read();
|
|
let before = match counter.read() {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("counter read failed: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Integrate over a ~2 s window, or the counter's minimum window if longer
|
|
// (plus a margin): churn the CPU and re-read every ~200 ms so the
|
|
// integrating counters accumulate the wall-power integral.
|
|
let floor = Duration::from_nanos(counter.min_window_ns());
|
|
let window = Duration::from_secs(2).max(floor + Duration::from_millis(500));
|
|
let start = Instant::now();
|
|
let mut checksum: u64 = 0;
|
|
while start.elapsed() < window {
|
|
let step = (Instant::now() + Duration::from_millis(200)).min(start + window);
|
|
checksum = checksum.wrapping_add(churn(step));
|
|
let _ = counter.read();
|
|
}
|
|
let after = match counter.read() {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("counter read failed: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
let elapsed = start.elapsed().as_secs_f64();
|
|
let delta_uj = after.uj.saturating_sub(before.uj);
|
|
let joules = delta_uj as f64 / 1_000_000.0;
|
|
// An estimator's figure is not a measurement; say which one this is.
|
|
if after.provenance == jouleclaw_energy::Provenance::Estimator {
|
|
println!("--- estimated ---");
|
|
} else {
|
|
println!("--- measured ---");
|
|
}
|
|
println!("window : {:.3} s", elapsed);
|
|
println!(
|
|
"energy : {} uj ({:.3} J) avg {:.2} W [{}]",
|
|
delta_uj,
|
|
joules,
|
|
if elapsed > 0.0 { joules / elapsed } else { 0.0 },
|
|
after.provenance,
|
|
);
|
|
// Keep the workload from being optimized away.
|
|
if checksum == 0 {
|
|
eprintln!("(workload checksum was zero — unexpected)");
|
|
}
|
|
}
|