jouleclaw-energy: a Mac's whole-machine power from the SMC (EnergyDomain::System, ModelBased) #622

Merged
dcharlot merged 1 commit from jouleclaw-energy/smc-system into main 2026-11-05 04:30:59 -05:00
Owner

On Apple Silicon under macOS 27, IOReport's CPU rails are frozen for an unprivileged reader, so detect_meter falls back to the load-average estimator, a guess graded Estimator. Every Mac still publishes its total power in the SMC key PSTR, and any process can read it through the AppleSMCKeysEndpoint IOKit user client, with no root.

What changes

  • New EnergyDomain::System: the whole machine. It covers everything the machine's DC input feeds: the SoC, memory, storage, fans and the rest of the board. It does not cover the power supply's AC-to-DC loss. It contains SocTotal and every other domain.
  • New feature smc (macOS), module apple_smc. SmcSystemCounter integrates PSTR over time and grades the result ModelBased. That is the grade NVML's power.draw integral already carries: PSTR is a power reading, not an energy counter, so no liveness probe can vouch for its integral.
    • Sampling. One thread per process polls every 100 ms and integrates the value it holds, so a figure doesn't depend on how often the caller reads. Reads never run backwards.
    • Startup check. PSTR must be a 4-byte float, and the sampler must see it refresh before the counter is available. The first construction in a process waits about a second for that.
    • Bad samples. A sample at or below 0 W, above 1.5 kW, or NaN is discarded and counted.
    • Staleness. If the value holds for 10 s, reads give no figure.
    • Minimum window: 2 s. PSTR refreshes about once a second (median gap 1.0 s on both Macs below), and it lags a step in load by about one refresh.
  • detect_meter order on macOS. A live IOReport counter comes first, then the SMC, then the load-average estimator. A figure from the SMC includes whatever else the machine powered, so it can read high for the work but never low.
  • mesh-meter. Its exhaustive covers_whole_request match now treats System as covering the whole request.
  • SPEC §1.3. The ModelBased row now lists vendor power sampled and integrated over time (Apple SMC PSTR).
  • MEASURED_ENERGY.md gets a new section on this meter.
  • The energy_probe example supports --features smc and integrates over at least the counter's minimum window.

The feature is opt-in, so existing consumers behave as before until they enable smc.

Measured (macOS 27.2, unprivileged, energy_probe, 2.5 s CPU churn)

Host Reading
Mac Studio, M3 Ultra (IOReport CPU rails frozen) 299.9 J · 120.0 W, ModelBased, System
MacBook Pro, M5 Max, on AC 160.9 J · 64.4 W, ModelBased, System
  • Mac Studio: PSTR equals the DC input (PDTR).
  • MacBook Pro: on AC with the battery not charging, it read 51.7 W against 54.9 W at the DC input. So it measures the system's draw, not the input.
  • Not established: whether each value averages the preceding second or samples a single point. Other load on the dev Mac hid the test; the grade and the minimum window hold either way.

Tests

  • Unit tests drive the integral, monotonic reads, implausible samples, staleness and the sampler's startup with scripted samples and times.
  • The hardware test meters a CPU burn from the real SMC and prints apple-smc: measured. A new macOS CI step greps for that line under --features smc and ioreport,smc. The clippy step lints both configurations.
  • The existing IOReport fallback test now expects the SMC (ModelBased) where it's built and live, and the estimator otherwise.

Passing:

  • M3 Ultra: smc, ioreport,smc and ioreport (both existing CI greps). Clippy clean in default, ioreport, smc and ioreport,smc. mesh-meter with hw-meter-apple.
  • Linux x86_64: rapl,nvml, default clippy, and mesh-meter with hw-meter-rapl.

This PR is independent of #618 (RAPL MSR fallback); the two touch different lines.

🤖 Generated with Claude Code

On Apple Silicon under macOS 27, IOReport's CPU rails are frozen for an unprivileged reader, so `detect_meter` falls back to the load-average estimator, a guess graded `Estimator`. Every Mac still publishes its total power in the SMC key `PSTR`, and any process can read it through the `AppleSMCKeysEndpoint` IOKit user client, with no root. ## What changes - **New `EnergyDomain::System`: the whole machine.** It covers everything the machine's DC input feeds: the SoC, memory, storage, fans and the rest of the board. It does not cover the power supply's AC-to-DC loss. It contains `SocTotal` and every other domain. - **New feature `smc` (macOS), module `apple_smc`.** `SmcSystemCounter` integrates `PSTR` over time and grades the result **`ModelBased`**. That is the grade NVML's `power.draw` integral already carries: `PSTR` is a power reading, not an energy counter, so no liveness probe can vouch for its integral. - **Sampling.** One thread per process polls every 100 ms and integrates the value it holds, so a figure doesn't depend on how often the caller reads. Reads never run backwards. - **Startup check.** `PSTR` must be a 4-byte float, and the sampler must see it refresh before the counter is available. The first construction in a process waits about a second for that. - **Bad samples.** A sample at or below 0 W, above 1.5 kW, or NaN is discarded and counted. - **Staleness.** If the value holds for 10 s, reads give no figure. - **Minimum window: 2 s.** `PSTR` refreshes about once a second (median gap 1.0 s on both Macs below), and it lags a step in load by about one refresh. - **`detect_meter` order on macOS.** A live IOReport counter comes first, then the SMC, then the load-average estimator. A figure from the SMC includes whatever else the machine powered, so it can read high for the work but never low. - **`mesh-meter`.** Its exhaustive `covers_whole_request` match now treats `System` as covering the whole request. - **SPEC §1.3.** The `ModelBased` row now lists vendor power sampled and integrated over time (Apple SMC `PSTR`). - **`MEASURED_ENERGY.md`** gets a new section on this meter. - **The `energy_probe` example** supports `--features smc` and integrates over at least the counter's minimum window. The feature is opt-in, so existing consumers behave as before until they enable `smc`. ## Measured (macOS 27.2, unprivileged, `energy_probe`, 2.5 s CPU churn) | Host | Reading | |---|---| | Mac Studio, M3 Ultra (IOReport CPU rails frozen) | 299.9 J · 120.0 W, `ModelBased`, `System` | | MacBook Pro, M5 Max, on AC | 160.9 J · 64.4 W, `ModelBased`, `System` | - **Mac Studio:** `PSTR` equals the DC input (`PDTR`). - **MacBook Pro:** on AC with the battery not charging, it read 51.7 W against 54.9 W at the DC input. So it measures the system's draw, not the input. - **Not established:** whether each value averages the preceding second or samples a single point. Other load on the dev Mac hid the test; the grade and the minimum window hold either way. ## Tests - **Unit tests** drive the integral, monotonic reads, implausible samples, staleness and the sampler's startup with scripted samples and times. - **The hardware test** meters a CPU burn from the real SMC and prints `apple-smc: measured`. A new macOS CI step greps for that line under `--features smc` and `ioreport,smc`. The clippy step lints both configurations. - **The existing IOReport fallback test** now expects the SMC (`ModelBased`) where it's built and live, and the estimator otherwise. Passing: - **M3 Ultra:** `smc`, `ioreport,smc` and `ioreport` (both existing CI greps). Clippy clean in default, `ioreport`, `smc` and `ioreport,smc`. `mesh-meter` with `hw-meter-apple`. - **Linux x86_64:** `rapl,nvml`, default clippy, and `mesh-meter` with `hw-meter-rapl`. This PR is independent of #618 (RAPL MSR fallback); the two touch different lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jouleclaw-energy: a Mac's whole-machine power from the SMC (EnergyDomain::System, ModelBased)
All checks were successful
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
45f91fe0f6
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]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!622
No description provided.