From e7403fde3bde1133c22ddb937d0bd178940fc22c Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 4 Nov 2026 22:08:37 -0500 Subject: [PATCH] mesh-meter: hw-meter-apple meters a Mac with the SMC's whole-machine power where IOReport is not live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hw-meter-apple now enables jouleclaw-energy's `smc` beside `ioreport`. On a Mac whose IOReport CPU rail is frozen for an unprivileged reader (macOS 27 on an M5 Max and an M3 Ultra), HwMeter::detect's backend becomes the SMC's whole-machine power integral (ModelBased, EnergyDomain::System) instead of the load estimator. A live IOReport counter still comes first. The minimum-window rule (#630) is what makes this sound: the SMC's figure needs 2 s, so a span at least that long is sealed ModelBased, and a shorter one (most served requests) is this crate's estimate of its own length, as before. System covers the whole request (#622), so a long span's figure stands for it: high, since it holds whatever else the machine powered, never low. mesh-core, mesh-openai and mesh-engine-gguf take it through their own hw-meter-apple and metal features. ProcessMeter's host fallback becomes the SMC on such a Mac, under the same window rule. On the M3 Ultra: the bridge picks apple-smc with a 2 s floor; a 20 ms span is Estimator (0.38 J) and a 2.2 s span ModelBased (152.6 J for the whole, shared machine); mesh-openai's serve example over hw-meter-apple seals a microsecond serve as Estimator (172 µJ), and over metal its process meter still seals HwShunt. Tests pass on macOS (mesh-meter, mesh-core with hw-meter-apple; mesh-openai with server,hw-meter-apple and server,metal) and Linux (mesh-meter with hw-meter-apple); clippy clean; mesh-engine-gguf --features metal checks. Co-Authored-By: Claude Opus 5.5 --- mesh/mesh-rs/crates/mesh-meter/Cargo.toml | 4 +- mesh/mesh-rs/crates/mesh-meter/src/lib.rs | 54 ++++++++++++++++--- .../crates/mesh-openai/examples/serve.rs | 5 +- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/mesh/mesh-rs/crates/mesh-meter/Cargo.toml b/mesh/mesh-rs/crates/mesh-meter/Cargo.toml index 16b2714b3..5c1af2b49 100644 --- a/mesh/mesh-rs/crates/mesh-meter/Cargo.toml +++ b/mesh/mesh-rs/crates/mesh-meter/Cargo.toml @@ -18,7 +18,9 @@ hw-meter = ["dep:jouleclaw-energy"] # Convenience: enable the platform backend most likely to be present. hw-meter-rapl = ["hw-meter", "jouleclaw-energy/rapl"] hw-meter-nvml = ["hw-meter", "jouleclaw-energy/nvml"] -hw-meter-apple = ["hw-meter", "jouleclaw-energy/ioreport"] +# Apple: IOReport where its CPU rail is live, else the Mac's whole-machine power +# from the SMC (ModelBased; spans shorter than its 2 s floor are estimates). +hw-meter-apple = ["hw-meter", "jouleclaw-energy/ioreport", "jouleclaw-energy/smc"] [lints] workspace = true diff --git a/mesh/mesh-rs/crates/mesh-meter/src/lib.rs b/mesh/mesh-rs/crates/mesh-meter/src/lib.rs index 4113e99ee..af54948a7 100644 --- a/mesh/mesh-rs/crates/mesh-meter/src/lib.rs +++ b/mesh/mesh-rs/crates/mesh-meter/src/lib.rs @@ -12,12 +12,14 @@ //! * [`ManualMeter`] — caller-driven, for tests and replaying recorded spans. //! //! With the `hw-meter` feature, [`HwMeter`] bridges the family's meter (RAPL / -//! NVML / Apple Silicon IOReport, else the family's estimator), so a span carries -//! the provenance of whatever actually metered it: `HwShunt` from a hardware -//! counter that is live for the domain it reads (Apple IOReport included, where -//! its CPU energy rail advances under load), `ModelBased` from a vendor energy -//! model, `Estimator` where the host exposes no live counter — as on Apple Silicon -//! whose IOReport CPU rail is frozen (macOS 27.0.1). +//! NVML / Apple Silicon IOReport / a Mac's SMC system power, else the family's +//! estimator), so a span carries the provenance of whatever actually metered it: +//! `HwShunt` from a hardware counter that is live for the domain it reads (Apple +//! IOReport included, where its CPU energy rail advances under load), +//! `ModelBased` from a vendor energy model — with `hw-meter-apple`, the SMC's +//! whole-machine power integral on a Mac whose IOReport CPU rail is frozen (macOS +//! 27 on an M5 Max and an M3 Ultra) — and `Estimator` where the host exposes +//! neither. //! //! Also with `hw-meter`, [`ProcessMeter`] meters an engine that runs in this //! process (a GGUF decode, in process) by this process's own CPU energy and its @@ -276,8 +278,11 @@ mod hw { /// estimator) into the Mesh [`Meter`] interface. The active backend's /// provenance is mapped through unchanged, so honesty is preserved across the /// bridge. On Apple Silicon whose IOReport CPU energy rail does not advance - /// (macOS 27.0.1 on an M5 Max and an M3 Ultra), the active backend is the load - /// estimator and spans say `Estimator`. + /// (macOS 27 on an M5 Max and an M3 Ultra), the active backend is, with + /// `hw-meter-apple`, the SMC's whole-machine power integral: a span at least + /// its 2 s minimum window long says `ModelBased`, a shorter one `Estimator`. + /// Without the SMC (or a Mac that does not publish it) the backend is the load + /// estimator, and spans say `Estimator`. /// /// If the backend fails a read, the bridge takes this crate's no-hardware path /// from then on: an [`EstimatorMeter`], continuing from the last good reading, @@ -684,6 +689,39 @@ mod hw { } } + /// On this Mac, the bridge over the family's meter: where IOReport's CPU + /// rail is frozen and the SMC publishes `PSTR`, the SMC's whole-machine + /// integral, whose 2 s floor makes a 20 ms span an estimate and a span + /// past it a modeled figure of the whole machine. Where IOReport is live, + /// a hardware reading. + #[cfg(all(feature = "hw-meter-apple", target_os = "macos"))] + #[test] + fn on_a_mac_the_smc_models_spans_past_its_floor_and_shorter_ones_are_estimates() { + let m = HwMeter::detect(); + let short = span(&m); + println!("mesh-meter on this Mac: meter={} floor={:?} 20 ms span {short:?}", m.name(), m.min_window()); + match m.name() { + "apple-ioreport" => assert_eq!(short.provenance, Provenance::HwShunt), + "apple-smc" => { + assert_eq!(m.min_window(), Duration::from_secs(2)); + assert_eq!((short.provenance, short.measured_uj), (Provenance::Estimator, None), "{short:?}"); + assert!(short.estimated_uj > 0); + let s = Session::start(&m); + std::thread::sleep(Duration::from_millis(2_200)); + let long = s.stop(); + println!("mesh-meter on this Mac: 2.2 s span {long:?}"); + assert_eq!((long.provenance, long.measured_uj), (Provenance::ModelBased, None), "{long:?}"); + // A running Mac draws at least ~1 W: 2.2 s is over 2 J. + assert!(long.estimated_uj > 2_000_000, "{long:?}"); + assert!(long.check_provenance().is_ok()); + } + other => { + assert_eq!(other, EstimatorMeter::default().name(), "an unexpected Apple backend"); + assert_eq!(short.provenance, Provenance::Estimator); + } + } + } + /// A backend that steps `STEP` µJ per read from `BASE`, over `domain`, at /// `grade` — a live NVML counter is `(Gpu, HwShunt)`. struct Stepping { diff --git a/mesh/mesh-rs/crates/mesh-openai/examples/serve.rs b/mesh/mesh-rs/crates/mesh-openai/examples/serve.rs index 416d6689e..c52294a31 100644 --- a/mesh/mesh-rs/crates/mesh-openai/examples/serve.rs +++ b/mesh/mesh-rs/crates/mesh-openai/examples/serve.rs @@ -29,7 +29,10 @@ //! - `--features server,hw-meter` (or `hw-meter-rapl`, `-nvml`, `-apple`): //! `mesh_meter::HwMeter::detect`, the machine's counter, a reading only //! where a live one covers the whole request (a RAPL package on Linux) and -//! `mesh_meter::EstimatorMeter` otherwise. A machine counter reads +//! `mesh_meter::EstimatorMeter` otherwise. With `-apple` on a Mac whose +//! IOReport CPU rail is frozen it is the SMC's whole-machine power: a modeled +//! figure (`ModelBased`) for a serve at least its 2 s floor long, an estimate +//! for a shorter one, as this engine's are. A machine counter reads //! everything the host does over the span, not this request alone. //! - `--features server` alone: `mesh_meter::EstimatorMeter`, wall time × an //! assumed draw, `Estimator`. -- 2.54.0 (Apple Git-157)