From 644b5edb9f93c5029741bdb892aeee8f011b0b23 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 13 Oct 2026 02:22:44 -0400 Subject: [PATCH 1/2] quantum-meter: wai-quantum's RunMeter over this process's CPU and GPU counters wai-quantum defines RunMeter and ships no implementation: a meter belongs with the energy source it reads. This is one, in its own crate beside quantum-wgsl-check, so the counters never compile in wai-quantum's builds. The source is jouleclaw-energy's span meter: this process's CPU energy (the kernel's per-process count) and its GPU energy (the GPU rail over a span this process all but owned, or an exact zero where it spent no GPU time). Both are counters the hardware keeps, so a figure is OnChipCounter, filtering unknown. The domain is this process's CPU and the GPU, the same for the CPU reference and an accelerator member, so their figures compare. A window has no figure, never an estimate, when the counters are not live (off macOS on Apple Silicon, always), when the GPU counter gives none (the GPU shared, a client gone, the rail not current), when it is shorter than its counters resolve (1 s with GPU time, 1 ms without), or when the label misses the fabric's resolution floor. The declared uncertainty is the operator's own figure for the counters (the platform publishes none, so there is no default), plus each window's ends (a quantum of a CPU thread, a republish of the rail, at each end, over the window) and, with GPU time, the share other clients may hold. The fabric's own calibrations open one window per corpus circuit, which runs for microseconds and is always refused here. span_of_runs is the calibration this meter supports: one window around as many runs of a circuit as fill a stated time, one reading over all their operations. The quantum-meter tool calibrates the CPU reference and a WebGPU member that way and places a circuit by the figures. On an M4 Max, 20 qubits, 3 s windows: the Metal member 4 nJ per operation, the CPU reference 9, so the fabric placed the circuit on the GPU. Three repeats of the device test at 18 qubits read 4 and 9 to 10. CI: the Metal job must print what the device test read and run the tool; the software Vulkan job, where no such counter exists, must find the meter not live and its runs unmetered. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-webgpu.yml | 45 + wai/tools/quantum-meter/Cargo.lock | 1411 +++++++++++++++++++++ wai/tools/quantum-meter/Cargo.toml | 20 + wai/tools/quantum-meter/src/lib.rs | 472 +++++++ wai/tools/quantum-meter/src/main.rs | 215 ++++ wai/tools/quantum-meter/tests/hardware.rs | 149 +++ 6 files changed, 2312 insertions(+) create mode 100644 wai/tools/quantum-meter/Cargo.lock create mode 100644 wai/tools/quantum-meter/Cargo.toml create mode 100644 wai/tools/quantum-meter/src/lib.rs create mode 100644 wai/tools/quantum-meter/src/main.rs create mode 100644 wai/tools/quantum-meter/tests/hardware.rs diff --git a/.github/workflows/wai-quantum-webgpu.yml b/.github/workflows/wai-quantum-webgpu.yml index 981d3c7f8..dc3bf3178 100644 --- a/.github/workflows/wai-quantum-webgpu.yml +++ b/.github/workflows/wai-quantum-webgpu.yml @@ -23,6 +23,12 @@ name: wai-quantum WebGPU member # the four pinned digests, reproduce every vector and refuse every reject file with # its class. It is not a required status until it has been green three runs in a row. # +# The meter (wai/tools/quantum-meter) is wai-quantum's RunMeter over a process's own +# CPU and GPU energy counters. On the Metal host, where those counters exist, it meters +# a member's runs and the CPU reference's over windows they resolve, and its test must +# print what it read; on the software Vulkan host, where they do not, every window must +# come out unmetered. +# # There is no windows job while the windows runner is offline: a job queued on an # offline runner never settles. Add it back from the comment at the end when the # runner has survived a reboot. @@ -38,6 +44,11 @@ on: - 'wai/wai-quantum/**' - 'wai/quantum-conformance/**' - 'wai/tools/quantum-wgsl-check/**' + # The meter, and the counters it reads. + - 'wai/tools/quantum-meter/**' + - 'jouleclaw/jouleclaw-rs/Cargo.toml' + - 'jouleclaw/jouleclaw-rs/crates/jouleclaw-core/**' + - 'jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/**' - 'wai/wai-rs/src/bin/wai_quantum_vectors.rs' # The GPU jobs build wai-rs with the forwarded feature, --locked. - 'wai/wai-rs/Cargo.toml' @@ -57,6 +68,11 @@ on: - 'wai/wai-quantum/**' - 'wai/quantum-conformance/**' - 'wai/tools/quantum-wgsl-check/**' + # The meter, and the counters it reads. + - 'wai/tools/quantum-meter/**' + - 'jouleclaw/jouleclaw-rs/Cargo.toml' + - 'jouleclaw/jouleclaw-rs/crates/jouleclaw-core/**' + - 'jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/**' - 'wai/wai-rs/src/bin/wai_quantum_vectors.rs' # The GPU jobs build wai-rs with the forwarded feature, --locked. - 'wai/wai-rs/Cargo.toml' @@ -175,6 +191,18 @@ jobs: - name: The member's tests shell: bash run: scripts/wai-quantum-webgpu-member.sh + # No per-process energy counter exists here: the meter must find none, and a + # run of the member through it must be unmetered. + - name: The meter, where its counters do not exist + shell: bash + working-directory: wai/tools/quantum-meter + run: | + set -euo pipefail + cargo test --locked -- --nocapture 2>&1 | tee "$RUNNER_TEMP/meter.log" + grep -q '^test a_members_runs_are_metered_over_a_window_the_counters_resolve \.\.\. ok$' "$RUNNER_TEMP/meter.log" \ + || { echo "::error::the meter's device test did not run"; exit 1; } + grep -q '^quantum-meter: the counters are not live here' "$RUNNER_TEMP/meter.log" \ + || { echo "::error::the meter's device test found no adapter, so it checked nothing"; exit 1; } member-metal: name: member · Metal (macos) @@ -195,6 +223,23 @@ jobs: shell: bash working-directory: wai/wai-rs run: nice -n 10 cargo run --locked --no-default-features --features quantum_fabric_webgpu --bin wai_quantum_vectors -- verify ../quantum-conformance --webgpu + # The counters exist here. The device test prints a `quantum-meter hardware:` line + # for each window it read: a figure, or why another process's GPU use left it + # none. The tool then calibrates both members on one circuit and, where both + # windows earn a figure, places it. + - name: The meter on this process's counters + shell: bash + working-directory: wai/tools/quantum-meter + run: | + set -euo pipefail + nice -n 10 cargo test --locked --release -- --nocapture 2>&1 | tee "$RUNNER_TEMP/meter.log" + grep -q '^test a_members_runs_are_metered_over_a_window_the_counters_resolve \.\.\. ok$' "$RUNNER_TEMP/meter.log" \ + || { echo "::error::the meter's device test did not run"; exit 1; } + grep -q '^quantum-meter hardware: ' "$RUNNER_TEMP/meter.log" \ + || { echo "::error::the meter read no window: its counters are not live on this host"; exit 1; } + nice -n 10 cargo run --locked --release -- --counter-ppm 50000 2>&1 | tee "$RUNNER_TEMP/meter-tool.log" + grep -q '^quantum-meter: local/cpu-' "$RUNNER_TEMP/meter-tool.log" \ + || { echo "::error::the tool ran no window on the CPU reference"; exit 1; } member-hw: name: member · discrete GPU, Vulkan (jetson-hub) diff --git a/wai/tools/quantum-meter/Cargo.lock b/wai/tools/quantum-meter/Cargo.lock new file mode 100644 index 000000000..f4645f51d --- /dev/null +++ b/wai/tools/quantum-meter/Cargo.lock @@ -0,0 +1,1411 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror", + "windows", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jouleclaw-core" +version = "0.1.0" + +[[package]] +name = "jouleclaw-energy" +version = "0.1.0" +dependencies = [ + "jouleclaw-core", + "libc", + "thiserror", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "naga" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.17.1", + "indexmap", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash", + "spirv", + "thiserror", + "unicode-ident", +] + +[[package]] +name = "naga-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash", + "thiserror", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "ordered-float" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "quantum-meter" +version = "0.0.0" +dependencies = [ + "jouleclaw-energy", + "wai-quantum", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wai-quantum" +version = "0.4.3" +dependencies = [ + "blake3", + "ed25519-dalek", + "serde_json", + "wgpu", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +dependencies = [ + "arrayvec", + "bitflags", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "js-sys", + "log", + "naga", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga", + "naga-types", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror", + "wgpu-core-deps-apple", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags", + "block2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "gpu-allocator", + "hashbrown 0.17.1", + "libc", + "libloading", + "log", + "naga", + "naga-types", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-metal", + "objc2-quartz-core", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "static_assertions", + "thiserror", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +dependencies = [ + "bitflags", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "static_assertions", + "web-sys", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/wai/tools/quantum-meter/Cargo.toml b/wai/tools/quantum-meter/Cargo.toml new file mode 100644 index 000000000..29b7e7217 --- /dev/null +++ b/wai/tools/quantum-meter/Cargo.toml @@ -0,0 +1,20 @@ +# A meter for wai-quantum's fabric: its `RunMeter` over this process's own CPU and GPU +# energy counters, a calibration over windows long enough for those counters to resolve, +# and a tool that calibrates the CPU reference and a WebGPU member with it. +# +# Deliberately a SEPARATE crate with its own lockfile: a meter belongs with the energy +# source it reads, so the counters are a dependency here only, and never compile in +# wai-quantum's own builds or tests. +[package] +name = "quantum-meter" +version = "0.0.0" +edition = "2024" +rust-version = "1.97" +license = "Apache-2.0" +publish = false + +[dependencies] +wai-quantum = { path = "../../wai-quantum", default-features = false, features = ["quantum_fabric_webgpu"] } +# `ioreport` is the per-process counters' feature. They exist on macOS on Apple Silicon; +# elsewhere the crate builds, the counters are not live, and every run is unmetered. +jouleclaw-energy = { path = "../../../jouleclaw/jouleclaw-rs/crates/jouleclaw-energy", default-features = false, features = ["ioreport"] } diff --git a/wai/tools/quantum-meter/src/lib.rs b/wai/tools/quantum-meter/src/lib.rs new file mode 100644 index 000000000..180125e56 --- /dev/null +++ b/wai/tools/quantum-meter/src/lib.rs @@ -0,0 +1,472 @@ +//! [`RunMeter`] over this process's own energy counters, and a calibration +//! over windows those counters can resolve. +//! +//! **The source.** `jouleclaw-energy`'s span meter: this process's CPU energy +//! (the kernel's per-process count) plus its GPU energy (the GPU rail, for a +//! span over which this process was all but the GPU's only user, or an exact +//! zero where it spent no GPU time). Both are readings of counters the +//! hardware keeps about itself, so a figure is labelled +//! [`EnergyClass::OnChipCounter`]. The domain is this process's CPU cores and +//! the GPU: what a run of a fabric member in this process spends on both, and +//! so the same domain for the CPU reference (GPU part zero) and for an +//! accelerator member (the device's work, and the host's submissions, waits +//! and copies). Figures from this meter on two members can be compared. +//! Memory, and any other rail, is outside it. +//! +//! **When there is no figure.** A window is unmetered, never estimated, when: +//! +//! - the counters are not live in this process ([`ProcessRunMeter::is_live`]): +//! off macOS on Apple Silicon, always; +//! - the GPU counter put no figure on the span: another process used the GPU +//! over it, a GPU client came or went, the rail was not current or did not +//! advance. The CPU figure alone would leave the GPU's energy out, which is +//! not zero; +//! - the window is shorter than a counter can resolve ([`least_window`]): one +//! second for a span with GPU time, a millisecond for one without; +//! - the figure is zero, or its label does not reach wai-quantum's resolution +//! floor (`check_label`, applied by the fabric to every reading). +//! +//! **The declared uncertainty** ([`label`]) is the sum of three terms, each +//! relative to the figure: +//! +//! - what the caller declares for the counters themselves +//! ([`Declared::counter_ppm`]). The platform publishes no accuracy for +//! either counter, so this is the operator's own evaluation and this crate +//! supplies no default; +//! - the ends of the window. A CPU thread's count is folded when the thread +//! leaves its core, at the latest when its quantum ends, so a read can miss +//! up to a quantum (10 ms) of one thread at each end ([`CPU_EDGE`]). A GPU +//! snapshot reads the rail on a republish, so it can be off by about one +//! republish interval (30 ms) at each end ([`GPU_EDGE`]). Either is taken +//! as that much of the window at the window's mean power: `edge / window`; +//! - the GPU's other users, for a span with GPU time: the rail is apportioned +//! by GPU time, and the counter gives a figure only where this process held +//! at least [`SOLE_USER_PERCENT`] of it, so the rest is at most +//! `100 − SOLE_USER_PERCENT` percent. +//! +//! So a window's length is part of its label: three seconds of GPU work +//! declares 3% for its ends and its share, a tenth of a second would declare +//! 61%, and is refused before that as shorter than the rail resolves. +//! +//! **Calibrating.** The fabric's own calibrations open one window per corpus +//! circuit, and a corpus circuit runs for microseconds: every such window is +//! refused here. [`span_of_runs`] is the calibration this meter supports: one +//! window around as many runs of a circuit as fill a stated time, giving the +//! runs' total operations and one reading, which +//! [`EnergyCalibration::add`](wai_quantum::quantum_fabric::EnergyCalibration::add) +//! takes as it takes a single run's. + +use jouleclaw_energy::process_gpu::SOLE_USER_PERCENT; +use jouleclaw_energy::span::{ProcessSpanMeter, SpanSource, SpanStart, SpanUses}; +use std::time::{Duration, Instant}; +use wai_quantum::quantum_energy::{CounterFiltering, EnergyClass, Uncertainty}; +use wai_quantum::quantum_fabric::RunMeter; + +/// What a CPU reading can miss over the two ends of a window: up to one +/// scheduler quantum (10 ms) of one thread at each. +pub const CPU_EDGE: Duration = Duration::from_millis(20); + +/// What a GPU reading can be off by over the two ends of a window: about one +/// republish interval of the rail (30 ms) at each. +pub const GPU_EDGE: Duration = Duration::from_millis(60); + +/// The least window for a span with no GPU time: the CPU counter's own. It +/// moves about every half millisecond under load, and a window under a +/// millisecond may straddle no update at all. +pub const MIN_CPU_WINDOW: Duration = Duration::from_millis(1); + +/// The least window for a span with GPU time: the GPU counter's own, where a +/// republish interval at each end is a few percent of the figure. +pub const MIN_GPU_WINDOW: Duration = Duration::from_secs(1); + +/// What the operator declares about the counters. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Declared { + /// The standard relative uncertainty of the counters' own readings, in + /// parts per million: the operator's evaluation, since the platform + /// publishes none. Not zero. + pub counter_ppm: u32, +} + +/// One window as the counters read it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Span { + /// The window, from the start readings to the call that ended it. + pub wall: Duration, + /// This process's CPU energy over it, µJ. + pub cpu_uj: u64, + /// This process's GPU energy over it, µJ: zero where it spent no GPU time. + pub gpu_uj: u64, + /// This process's GPU time over it, ns. + pub gpu_ns: u64, +} + +impl Span { + /// The figure: CPU and GPU together, µJ. + pub fn joules_micro(&self) -> Option { + self.cpu_uj.checked_add(self.gpu_uj) + } +} + +/// Why a window has no figure. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Refused { + /// This process's counters are not live. + NotLive(String), + /// The GPU counter put no figure on the span, and why. + NoGpuFigure(String), + /// The CPU counter put no figure on the span, and why. + NoCpuFigure(String), + /// A counter failed. + Counter(String), + /// The window is shorter than a counter resolves. + TooShort { window: Duration, least: Duration }, + /// The figure is zero. + Zero, + /// The declared uncertainty is zero, or with the window's terms no longer + /// fits its field. + Uncertainty, +} + +impl std::fmt::Display for Refused { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Refused::NotLive(why) => write!(f, "this process's counters are not live: {why}"), + Refused::NoGpuFigure(why) => write!(f, "the GPU counter put no figure on the window: {why}"), + Refused::NoCpuFigure(why) => write!(f, "the CPU counter put no figure on the window: {why}"), + Refused::Counter(why) => write!(f, "a counter failed: {why}"), + Refused::TooShort { window, least } => write!(f, "a window of {window:?} is shorter than the {least:?} its counters resolve"), + Refused::Zero => write!(f, "the window's figure is zero"), + Refused::Uncertainty => write!(f, "no uncertainty can be declared for the window"), + } + } +} + +/// The least window a span is labelled over: [`MIN_GPU_WINDOW`] with GPU +/// time, [`MIN_CPU_WINDOW`] without. +pub fn least_window(span: &Span) -> Duration { + if span.gpu_ns > 0 { MIN_GPU_WINDOW } else { MIN_CPU_WINDOW } +} + +/// `part / whole` in parts per million, rounded up. +fn ppm_of(part: Duration, whole: Duration) -> Option { + let whole = whole.as_nanos(); + if whole == 0 { + return None; + } + u32::try_from((part.as_nanos() * 1_000_000).div_ceil(whole)).ok() +} + +/// A span's figure and its label, or why it has none: the rule of the +/// [module docs](self), apart from the counters, so it is tested on spans +/// written by hand. +pub fn label(span: &Span, declared: Declared) -> Result<(u64, EnergyClass), Refused> { + let least = least_window(span); + if span.wall < least { + return Err(Refused::TooShort { window: span.wall, least }); + } + let joules_micro = span.joules_micro().ok_or_else(|| Refused::Counter("the CPU and GPU figures together overflow".to_string()))?; + if joules_micro == 0 { + return Err(Refused::Zero); + } + if declared.counter_ppm == 0 { + return Err(Refused::Uncertainty); + } + let with_gpu = span.gpu_ns > 0; + let ends = ppm_of(if with_gpu { GPU_EDGE } else { CPU_EDGE }, span.wall).ok_or(Refused::Uncertainty)?; + let share = if with_gpu { (100 - SOLE_USER_PERCENT as u32) * 10_000 } else { 0 }; + let relative_ppm = declared.counter_ppm.checked_add(ends).and_then(|p| p.checked_add(share)).ok_or(Refused::Uncertainty)?; + let window_us = u64::try_from(span.wall.as_micros()).map_err(|_| Refused::Uncertainty)?; + // Whether the platform filters these counters is not something a process + // can read, so the only honest value is that it is unknown. + Ok((joules_micro, EnergyClass::OnChipCounter { uncertainty: Uncertainty { relative_ppm, window_us }, filtering: CounterFiltering::Unknown })) +} + +/// [`RunMeter`] over this process's CPU and GPU energy counters: see the +/// [module docs](self). +pub struct ProcessRunMeter { + meter: ProcessSpanMeter, + declared: Declared, + open: Option, + last: Option>, +} + +impl ProcessRunMeter { + /// The meter. The first one in a process probes the counters: about + /// 0.3 s of this thread's CPU, and `gpu_load`, which must put a few tens + /// of milliseconds of work on the GPU from this process and wait for it + /// (a run of the member to be metered does). The answer is kept for the + /// life of the process, so a first meter whose `gpu_load` does no GPU + /// work leaves every later one without a GPU counter. + pub fn new(gpu_load: impl FnMut(), declared: Declared) -> Self { + ProcessRunMeter { meter: ProcessSpanMeter::new(gpu_load), declared, open: None, last: None } + } + + /// Whether both counters are live in this process. When they are not, + /// [`RunMeter::start`] fails and every run is unmetered. + pub fn is_live(&self) -> bool { + self.meter.is_metered() + } + + /// The GPU counter's own account of whether it is live. + pub fn gpu_liveness(&self) -> String { + format!("{:?}", self.meter.gpu().liveness()) + } + + /// The last window closed: its readings, or why it had none. A span that + /// was read can still have no figure ([`label`]); `stop`'s return says. + pub fn last(&self) -> Option<&Result> { + self.last.as_ref() + } +} + +impl RunMeter for ProcessRunMeter { + fn start(&mut self) -> Result<(), String> { + if !self.is_live() { + let why = Refused::NotLive(format!("GPU counter {}", self.gpu_liveness())); + let text = why.to_string(); + self.last = Some(Err(why)); + return Err(text); + } + self.open = Some(self.meter.start()); + Ok(()) + } + + fn stop(&mut self) -> Result, String> { + let start = self.open.take().ok_or_else(|| "the meter was stopped without a start".to_string())?; + // The window is the work's own: the end readings can wait up to a + // second for the GPU rail to republish, and that wait is not the run. + let wall = start.at().elapsed(); + let span = match self.meter.finish(start, SpanUses::CpuGpu) { + Ok(energy) => match energy.source { + SpanSource::Process { cpu_uj, gpu } => Ok(Span { wall, cpu_uj, gpu_uj: gpu.uj, gpu_ns: gpu.self_ns }), + SpanSource::CpuOnly { why, .. } => Err(Refused::NoGpuFigure(why)), + SpanSource::Fallback { why, .. } => Err(Refused::NoCpuFigure(why)), + }, + Err(e) => Err(Refused::Counter(e.to_string())), + }; + let reading = match &span { + Ok(s) => label(s, self.declared).ok(), + Err(_) => None, + }; + self.last = Some(span); + Ok(reading) + } +} + +/// One window of a meter around repeated runs. +#[derive(Clone, Debug, PartialEq)] +pub struct Runs { + /// How many runs the window held. + pub runs: u64, + /// The operations they performed, together. + pub ops: u64, + /// The window's reading: zero and `None` when the meter gave none. + pub joules_micro: u64, + pub class: Option, + /// From just after the meter started to just before it was stopped. + pub wall: Duration, +} + +/// One window of `meter` around runs of `run`, repeated until `at_least` has +/// passed (once at least). `run` performs one run and returns the operations +/// it performed (`pair_updates` of its circuit). The meter is started before +/// the first run and stopped after the last; a run that fails stops it and +/// discards the window. +/// +/// The reading is as the meter gave it. It is the caller's to hold to +/// `check_label` before use, which +/// [`EnergyCalibration::add`](wai_quantum::quantum_fabric::EnergyCalibration::add) +/// does. +pub fn span_of_runs(meter: &mut dyn RunMeter, at_least: Duration, mut run: impl FnMut() -> Result) -> Result { + meter.start()?; + let began = Instant::now(); + let (mut runs, mut ops) = (0u64, 0u64); + loop { + match run() { + Ok(n) => { + runs += 1; + ops = ops.saturating_add(n); + } + Err(e) => { + let _ = meter.stop(); + return Err(e); + } + } + if began.elapsed() >= at_least { + break; + } + } + let wall = began.elapsed(); + let (joules_micro, class) = match meter.stop()? { + Some((joules_micro, class)) => (joules_micro, Some(class)), + None => (0, None), + }; + Ok(Runs { runs, ops, joules_micro, class, wall }) +} + +#[cfg(test)] +mod tests { + use super::*; + use wai_quantum::quantum_energy::check_label; + use wai_quantum::quantum_fabric::EnergyCalibration; + + const DECLARED: Declared = Declared { counter_ppm: 50_000 }; + + fn uncertainty(class: &EnergyClass) -> Uncertainty { + class.uncertainty().expect("an on-chip counter declares an uncertainty") + } + + #[test] + fn a_cpu_span_is_labelled_with_the_counter_and_its_ends() { + let span = Span { wall: Duration::from_secs(3), cpu_uj: 4_200_000, gpu_uj: 0, gpu_ns: 0 }; + let (joules_micro, class) = label(&span, DECLARED).expect("a figure"); + assert_eq!(joules_micro, 4_200_000); + assert!(matches!(class, EnergyClass::OnChipCounter { filtering: CounterFiltering::Unknown, .. }), "{class:?}"); + // 50 000 declared, and 20 ms of 3 s: 6 666.6…, rounded up. + assert_eq!(uncertainty(&class), Uncertainty { relative_ppm: 50_000 + 6_667, window_us: 3_000_000 }); + assert_eq!(check_label(joules_micro, &class), Ok(())); + } + + #[test] + fn a_gpu_span_adds_the_rails_ends_and_the_other_users_share() { + let span = Span { wall: Duration::from_secs(3), cpu_uj: 1_000_000, gpu_uj: 9_000_000, gpu_ns: 2_500_000_000 }; + let (joules_micro, class) = label(&span, DECLARED).expect("a figure"); + assert_eq!(joules_micro, 10_000_000, "CPU and GPU together"); + // 50 000 declared, 60 ms of 3 s, and the 1% other clients may hold. + assert_eq!(uncertainty(&class), Uncertainty { relative_ppm: 50_000 + 20_000 + 10_000, window_us: 3_000_000 }); + } + + #[test] + fn a_window_shorter_than_its_counters_resolve_has_no_figure() { + let gpu = Span { wall: Duration::from_millis(999), cpu_uj: 100_000, gpu_uj: 900_000, gpu_ns: 800_000_000 }; + assert_eq!(label(&gpu, DECLARED), Err(Refused::TooShort { window: Duration::from_millis(999), least: MIN_GPU_WINDOW })); + assert!(label(&Span { wall: MIN_GPU_WINDOW, ..gpu }, DECLARED).is_ok(), "the least window itself is labelled"); + // The same window with no GPU time is long enough for the CPU counter. + let cpu = Span { gpu_uj: 0, gpu_ns: 0, ..gpu }; + assert!(label(&cpu, DECLARED).is_ok()); + let brief = Span { wall: Duration::from_micros(999), ..cpu }; + assert_eq!(label(&brief, DECLARED), Err(Refused::TooShort { window: Duration::from_micros(999), least: MIN_CPU_WINDOW })); + } + + #[test] + fn the_shorter_the_window_the_wider_the_label() { + let at = |ms| { + let span = Span { wall: Duration::from_millis(ms), cpu_uj: 1_000_000, gpu_uj: 1_000_000, gpu_ns: 1 }; + uncertainty(&label(&span, DECLARED).expect("a figure").1).relative_ppm + }; + assert_eq!(at(1_000), 50_000 + 60_000 + 10_000); + assert_eq!(at(10_000), 50_000 + 6_000 + 10_000); + assert!(at(1_000) > at(2_000) && at(2_000) > at(10_000)); + } + + #[test] + fn a_zero_figure_or_a_zero_declaration_has_no_label() { + let span = Span { wall: Duration::from_secs(2), cpu_uj: 0, gpu_uj: 0, gpu_ns: 0 }; + assert_eq!(label(&span, DECLARED), Err(Refused::Zero)); + let spent = Span { cpu_uj: 4, ..span }; + assert_eq!(label(&spent, Declared { counter_ppm: 0 }), Err(Refused::Uncertainty)); + // A figure is labelled here and can still be under the fabric's + // resolution floor, which the fabric applies to every reading: 4 µJ at + // 6% (5% declared, 1% for the ends of two seconds) is 240 000 µJ·ppm, + // under 288 676; 5 µJ reaches it. + let (joules_micro, class) = label(&spent, DECLARED).expect("labelled"); + assert_eq!(uncertainty(&class).relative_ppm, 60_000); + assert!(check_label(joules_micro, &class).is_err(), "4 µJ at 6% is below the floor"); + let (joules_micro, class) = label(&Span { cpu_uj: 5, ..span }, DECLARED).expect("labelled"); + assert_eq!(check_label(joules_micro, &class), Ok(()), "5 µJ at 6% reaches it"); + } + + #[test] + fn an_uncertainty_that_does_not_fit_its_field_is_refused() { + let span = Span { wall: Duration::from_secs(2), cpu_uj: 1_000, gpu_uj: 0, gpu_ns: 0 }; + assert_eq!(label(&span, Declared { counter_ppm: u32::MAX }), Err(Refused::Uncertainty)); + assert_eq!(Span { cpu_uj: u64::MAX, gpu_uj: 1, ..span }.joules_micro(), None); + assert!(matches!(label(&Span { cpu_uj: u64::MAX, gpu_uj: 1, ..span }, DECLARED), Err(Refused::Counter(_)))); + } + + /// A meter that reads what it is told to, and counts its calls. + struct Scripted { + reading: Option<(u64, EnergyClass)>, + fail_start: bool, + starts: u32, + stops: u32, + } + + impl Scripted { + fn giving(reading: Option<(u64, EnergyClass)>) -> Scripted { + Scripted { reading, fail_start: false, starts: 0, stops: 0 } + } + } + + impl RunMeter for Scripted { + fn start(&mut self) -> Result<(), String> { + self.starts += 1; + if self.fail_start { Err("no counter".to_string()) } else { Ok(()) } + } + + fn stop(&mut self) -> Result, String> { + self.stops += 1; + Ok(self.reading.clone()) + } + } + + fn a_class() -> EnergyClass { + label(&Span { wall: Duration::from_secs(3), cpu_uj: 1, gpu_uj: 0, gpu_ns: 0 }, DECLARED).expect("a figure").1 + } + + #[test] + fn one_window_holds_every_run_and_their_operations() { + let mut meter = Scripted::giving(Some((3_000_000, a_class()))); + let mut calls = 0u64; + let runs = span_of_runs(&mut meter, Duration::from_millis(30), || { + calls += 1; + std::thread::sleep(Duration::from_millis(4)); + Ok(1_000) + }) + .expect("a window"); + assert_eq!((meter.starts, meter.stops), (1, 1), "one window, whatever the runs"); + assert!(runs.runs >= 2 && runs.runs == calls, "{runs:?}"); + assert_eq!(runs.ops, 1_000 * runs.runs); + assert!(runs.wall >= Duration::from_millis(30), "{runs:?}"); + assert_eq!((runs.joules_micro, runs.class.clone()), (3_000_000, Some(a_class()))); + // The window is one run of a calibration, over all its operations. + let mut calibration = EnergyCalibration::new(); + calibration.add(runs.ops, runs.joules_micro, runs.class.as_ref()); + let (nanojoules, _) = calibration.finish().expect("a figure"); + assert_eq!(nanojoules, (3_000_000u64 * 1_000).div_ceil(runs.ops)); + } + + #[test] + fn a_window_runs_once_at_least_and_an_unmetered_one_reads_nothing() { + let mut meter = Scripted::giving(None); + let runs = span_of_runs(&mut meter, Duration::ZERO, || Ok(7)).expect("a window"); + assert_eq!((runs.runs, runs.ops, runs.joules_micro, runs.class.clone()), (1, 7, 0, None)); + // It leaves a calibration without a figure, as any unmetered run does. + let mut calibration = EnergyCalibration::new(); + calibration.add(runs.ops, runs.joules_micro, runs.class.as_ref()); + assert_eq!(calibration.finish(), None); + } + + #[test] + fn a_run_that_fails_closes_the_window_and_a_meter_that_fails_to_start_runs_nothing() { + let mut meter = Scripted::giving(Some((1, a_class()))); + let mut calls = 0; + let failed = span_of_runs(&mut meter, Duration::from_secs(60), || { + calls += 1; + if calls == 2 { Err("the device was lost".to_string()) } else { Ok(1) } + }); + assert_eq!(failed, Err("the device was lost".to_string())); + assert_eq!((meter.starts, meter.stops), (1, 1), "the window is closed, its reading discarded"); + + let mut dead = Scripted { fail_start: true, ..Scripted::giving(None) }; + let mut ran = false; + let none = span_of_runs(&mut dead, Duration::ZERO, || { + ran = true; + Ok(1) + }); + assert_eq!(none, Err("no counter".to_string())); + assert!(!ran, "no run without a window"); + assert_eq!((dead.starts, dead.stops), (1, 0)); + } +} diff --git a/wai/tools/quantum-meter/src/main.rs b/wai/tools/quantum-meter/src/main.rs new file mode 100644 index 000000000..c1d3cda16 --- /dev/null +++ b/wai/tools/quantum-meter/src/main.rs @@ -0,0 +1,215 @@ +//! Calibrates the fabric's CPU reference and a WebGPU member with this +//! process's own energy counters, and places a circuit by the figures. +//! +//! quantum-meter --counter-ppm [--qubits ] [--seconds ] [--gateset 1|2] [--expect ] +//! +//! `--counter-ppm` is the operator's evaluation of the counters' own relative +//! uncertainty, in parts per million. The platform publishes none, so there +//! is no default. +//! +//! One circuit on `--qubits` qubits (20) is run by each member, again and +//! again inside one window of `--seconds` seconds (3) at least: a window long +//! enough for the counters to resolve, where a single run is not. Each +//! member's line gives the window's readings and the figure they earn, in +//! nanojoules per operation (one pair of amplitudes under one gate), or says +//! why the window has none. With a figure on both, the last line is the member +//! the fabric places that circuit on. +//! +//! Exit 0 when it ran, with or without figures; 1 when a member could not be +//! found or run; 2 for a bad argument. + +use quantum_meter::{Declared, ProcessRunMeter, Refused, Runs, Span, label, span_of_runs}; +use std::process::ExitCode; +use std::time::Duration; +use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset}; +use wai_quantum::quantum_energy::EnergyClass; +use wai_quantum::quantum_fabric::{EnergyCalibration, FABRIC_DEFAULT, Member, Parity, REFERENCE, apply_kernel, detect_local, pair_updates, place_for}; +use wai_quantum::quantum_fabric_webgpu::blocking; + +struct Args { + declared: Declared, + qubits: u8, + at_least: Duration, + gateset: Gateset, + expect: Option, +} + +fn usage() -> ExitCode { + eprintln!("usage: quantum-meter --counter-ppm [--qubits ] [--seconds ] [--gateset 1|2] [--expect ]"); + ExitCode::from(2) +} + +fn args() -> Option { + let (mut ppm, mut qubits, mut seconds, mut gateset, mut expect) = (None, 20u8, 3u64, Gateset::V2, None); + let mut it = std::env::args().skip(1); + while let Some(flag) = it.next() { + let value = it.next()?; + match flag.as_str() { + "--counter-ppm" => ppm = Some(value.parse::().ok().filter(|&p| p > 0)?), + "--qubits" => qubits = value.parse::().ok().filter(|n| (2..=24).contains(n))?, + "--seconds" => seconds = value.parse::().ok().filter(|s| (1..=600).contains(s))?, + "--gateset" => { + gateset = match value.as_str() { + "1" => Gateset::V1, + "2" => Gateset::V2, + _ => return None, + } + } + "--expect" => expect = Some(value), + _ => return None, + } + } + Some(Args { declared: Declared { counter_ppm: ppm? }, qubits, at_least: Duration::from_secs(seconds), gateset, expect }) +} + +/// The circuit every window runs: on each qubit a Hadamard, then a chain of +/// controlled X, then T and a dyadic phase on each, then a Hadamard on each. +/// Valid in either gateset; `5n − 1` gates. +fn circuit(n: u8, gs: Gateset) -> Circuit { + let mut c = Circuit::with_gateset(n, gs); + let one = |base, target, param| Gate { base, controls: Vec::new(), target, param }; + (0..n).for_each(|q| c.ops.push(one(BaseGate::H, q, 0))); + (0..n - 1).for_each(|q| c.ops.push(Gate { base: BaseGate::X, controls: vec![q], target: q + 1, param: 0 })); + for q in 0..n { + c.ops.push(one(BaseGate::T, q, 0)); + c.ops.push(one(BaseGate::P, q, 1 + u16::from(q % 6))); + } + (0..n).for_each(|q| c.ops.push(one(BaseGate::H, q, 0))); + c +} + +/// One member's window as a line, and the figure it earns. +fn report(member: &str, kernel: &str, runs: &Runs, last: Option<&Result>, declared: Declared) -> Option<(u64, EnergyClass)> { + let mut calibration = EnergyCalibration::new(); + calibration.add(runs.ops, runs.joules_micro, runs.class.as_ref()); + let figure = calibration.finish(); + let read = match last { + Some(Ok(s)) => format!("cpu={} uJ gpu={} uJ gpu_time={} ms", s.cpu_uj, s.gpu_uj, s.gpu_ns / 1_000_000), + _ => "no readings".to_string(), + }; + let earned = match (&figure, last) { + (Some((nanojoules, class)), _) => { + let ppm = class.uncertainty().map_or(0, |u| u.relative_ppm); + format!("{nanojoules} nJ/op, {}, {ppm} ppm", class.label()) + } + (None, Some(Err(why))) => format!("unmetered: {why}"), + // Read, and still no figure: the labelling rule says why, or the + // fabric's resolution floor refused the label. + (None, Some(Ok(s))) => match label(s, declared) { + Err(why) => format!("unmetered: {why}"), + Ok(_) => "unmetered: the reading is below the resolution floor".to_string(), + }, + (None, None) => "unmetered".to_string(), + }; + println!("quantum-meter: {member} kernel={kernel} runs={} ops={} wall={} ms {read} -> {earned}", runs.runs, runs.ops, runs.wall.as_millis()); + figure +} + +fn main() -> ExitCode { + let Some(args) = args() else { return usage() }; + let kernel = apply_kernel(args.gateset); + let wide = circuit(args.qubits, args.gateset); + let ops = pair_updates(&wide); + println!("quantum-meter: circuit qubits={} gates={} ops={ops} kernel={kernel}", wide.n_qubits, wide.ops.len()); + + let discovery = match blocking::discover() { + Ok(d) => d, + Err(e) => { + eprintln!("quantum-meter: discovery failed: {e:?}"); + return ExitCode::FAILURE; + } + }; + for r in &discovery.refused { + println!("quantum-meter: adapter {} ({}) is not a member: {:?}", r.key, r.adapter, r.error); + } + let found = match &args.expect { + Some(key) => discovery.found.iter().find(|m| m.key() == key), + None => discovery.found.first(), + }; + let Some(gpu) = found else { + eprintln!("quantum-meter: no WebGPU member{}", args.expect.as_ref().map(|k| format!(" with key {k}")).unwrap_or_default()); + return ExitCode::FAILURE; + }; + println!("quantum-meter: adapter {} member={} holds {} qubits", gpu.key(), gpu.id(), gpu.max_qubits()); + + // The first meter in a process probes the GPU counter with this load: a + // run of the member it will meter. + let mut meter = ProcessRunMeter::new( + || { + let _ = blocking::simulate_from(gpu, &wide, 0); + }, + args.declared, + ); + println!("quantum-meter: counters live={} gpu={}", meter.is_live(), meter.gpu_liveness()); + + let mut gpu_member = match blocking::member(gpu, FABRIC_DEFAULT) { + Ok(m) => m, + Err(e) => { + eprintln!("quantum-meter: {} could not be certified: {e:?}", gpu.id()); + return ExitCode::FAILURE; + } + }; + let mut cpu_member = detect_local(); + + // An unmetered window still runs, so each member is exercised either way. + let window = |meter: &mut ProcessRunMeter, run: &mut dyn FnMut() -> Result| match span_of_runs(meter, args.at_least, &mut *run) { + Ok(runs) => Ok(runs), + // The meter would not start: run once without it. + Err(_) if !meter.is_live() => run().map(|ops| Runs { runs: 1, ops, joules_micro: 0, class: None, wall: Duration::ZERO }), + Err(e) => Err(e), + }; + + let gpu_runs = window(&mut meter, &mut || { + let state = blocking::simulate_from(gpu, &wide, 0).map_err(|e| format!("{e:?}"))?; + std::hint::black_box(state); + Ok(ops) + }); + let gpu_runs = match gpu_runs { + Ok(r) => r, + Err(e) => { + eprintln!("quantum-meter: {} failed its run: {e}", gpu.id()); + return ExitCode::FAILURE; + } + }; + let gpu_figure = report(&gpu_member.id, kernel, &gpu_runs, meter.last(), args.declared); + + let cpu_runs = window(&mut meter, &mut || { + let state = wide.simulate_from(0).map_err(|e| format!("{e:?}"))?; + std::hint::black_box(state); + Ok(ops) + }); + let cpu_runs = match cpu_runs { + Ok(r) => r, + Err(e) => { + eprintln!("quantum-meter: the CPU reference failed its run: {e}"); + return ExitCode::FAILURE; + } + }; + let cpu_figure = report(&cpu_member.id, kernel, &cpu_runs, meter.last(), args.declared); + + // Figures go on both members or on neither: a figure on one alone would + // place it whatever it costs. + let (Some(gpu_figure), Some(cpu_figure)) = (gpu_figure, cpu_figure) else { + println!("quantum-meter: no placement by energy: a figure is needed on both members"); + return ExitCode::SUCCESS; + }; + let set = |member: &mut Member, implementation: &str, figure: &(u64, EnergyClass)| { + member.kernels.iter_mut().filter(|k| k.kernel == kernel && k.implementation == implementation).for_each(|k| k.energy = Some(figure.clone())); + }; + set(&mut gpu_member, gpu.implementation(), &gpu_figure); + set(&mut cpu_member, REFERENCE, &cpu_figure); + let members = [cpu_member, gpu_member]; + let predict = |_: &Member, k: &wai_quantum::quantum_fabric::KernelEntry| k.energy.as_ref().map(|(nanojoules, _)| nanojoules.saturating_mul(ops).saturating_mul(1_000)); + match place_for(&members, kernel, &Parity::BitExact, predict) { + Some((member, entry)) => println!( + "quantum-meter: placed {} ({}) for {} qubits: {} nJ/op against {} nJ/op on the other member", + member.id, + entry.implementation, + wide.n_qubits, + entry.energy.as_ref().map_or(0, |e| e.0), + if member.id == members[0].id { gpu_figure.0 } else { cpu_figure.0 } + ), + None => println!("quantum-meter: no member is certified for {kernel}"), + } + ExitCode::SUCCESS +} diff --git a/wai/tools/quantum-meter/tests/hardware.rs b/wai/tools/quantum-meter/tests/hardware.rs new file mode 100644 index 000000000..306c30739 --- /dev/null +++ b/wai/tools/quantum-meter/tests/hardware.rs @@ -0,0 +1,149 @@ +//! The meter on this host's own counters and GPU, where it has them: a WebGPU +//! member's runs and the CPU reference's, each in a window long enough to +//! resolve, and one run that is not. +//! +//! This is its own test binary because the first meter in a process probes +//! the GPU counter with the load it is given, once for the process's life. +//! +//! On a host with no WebGPU adapter, or where the counters are not live, the +//! test says so and checks what holds there: every window is unmetered. On a +//! host where they are live it prints `quantum-meter hardware:` lines, which a +//! CI job that expects live counters looks for. +//! +//! A GPU window can lose its figure to another process using the GPU over it, +//! which a shared host cannot prevent; the test then requires the refusal to +//! say so, and no figure. + +use quantum_meter::{Declared, MIN_GPU_WINDOW, ProcessRunMeter, Refused, label, span_of_runs}; +use std::time::Duration; +use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset}; +use wai_quantum::quantum_energy::{EnergyClass, check_label}; +use wai_quantum::quantum_fabric::{EnergyCalibration, RunMeter, pair_updates}; +use wai_quantum::quantum_fabric_webgpu::blocking; + +const DECLARED: Declared = Declared { counter_ppm: 50_000 }; + +/// Hadamards, a chain of controlled X, phases, Hadamards: `5n − 1` gates. +fn circuit(n: u8) -> Circuit { + let mut c = Circuit::with_gateset(n, Gateset::V2); + let one = |base, target, param| Gate { base, controls: Vec::new(), target, param }; + (0..n).for_each(|q| c.ops.push(one(BaseGate::H, q, 0))); + (0..n - 1).for_each(|q| c.ops.push(Gate { base: BaseGate::X, controls: vec![q], target: q + 1, param: 0 })); + for q in 0..n { + c.ops.push(one(BaseGate::T, q, 0)); + c.ops.push(one(BaseGate::P, q, 1 + u16::from(q % 6))); + } + (0..n).for_each(|q| c.ops.push(one(BaseGate::H, q, 0))); + c +} + +#[test] +fn a_members_runs_are_metered_over_a_window_the_counters_resolve() { + let discovery = blocking::discover().expect("discovery resolves natively"); + let Some(gpu) = discovery.found.first() else { + eprintln!("quantum-meter: no WebGPU adapter on this host ({} refused)", discovery.refused.len()); + return; + }; + let wide = circuit(gpu.max_qubits().min(18)); + let ops = pair_updates(&wide); + let mut meter = ProcessRunMeter::new( + || { + let _ = blocking::simulate_from(gpu, &wide, 0); + }, + DECLARED, + ); + let on_gpu = || blocking::simulate_from(gpu, &wide, 0).map(|_| ops).map_err(|e| format!("{e:?}")); + let on_cpu = || wide.simulate_from(0).map(|_| ops).map_err(|e| format!("{e:?}")); + + if !meter.is_live() { + eprintln!("quantum-meter: the counters are not live here (GPU counter {})", meter.gpu_liveness()); + // No window opens, and a run through the member is unmetered. + assert!(span_of_runs(&mut meter, Duration::ZERO, on_gpu).is_err()); + assert!(matches!(meter.last(), Some(Err(Refused::NotLive(_)))), "{:?}", meter.last()); + let (_, joules_micro, class) = blocking::simulate_metered(gpu, &wide, 0, &mut meter).expect("the run itself"); + assert_eq!((joules_micro, class), (0, None)); + return; + } + + // The member's runs, two seconds of them. + let runs = span_of_runs(&mut meter, MIN_GPU_WINDOW * 2, on_gpu).expect("the member runs"); + assert!(runs.runs >= 1 && runs.ops == ops * runs.runs, "{runs:?}"); + match meter.last().expect("a window was closed") { + Ok(span) => { + assert!(span.gpu_ns > 0, "the member's runs spent GPU time: {span:?}"); + assert!(span.cpu_uj > 0, "and the host's side of them CPU energy: {span:?}"); + let (joules_micro, class) = label(span, DECLARED).expect("two seconds of GPU work is labelled"); + assert_eq!((runs.joules_micro, runs.class.clone()), (joules_micro, Some(class.clone())), "stop returned the span's label"); + assert_eq!(joules_micro, span.cpu_uj + span.gpu_uj); + assert!(matches!(class, EnergyClass::OnChipCounter { .. }), "{class:?}"); + assert_eq!(check_label(joules_micro, &class), Ok(())); + let mut calibration = EnergyCalibration::new(); + calibration.add(runs.ops, runs.joules_micro, runs.class.as_ref()); + let (nanojoules, _) = calibration.finish().expect("a figure"); + eprintln!( + "quantum-meter hardware: {} on {} qubits: {} runs in {:?}, cpu {} uJ + gpu {} uJ over {} ms of GPU time -> {nanojoules} nJ/op, {} ppm", + gpu.id(), + wide.n_qubits, + runs.runs, + runs.wall, + span.cpu_uj, + span.gpu_uj, + span.gpu_ns / 1_000_000, + class.uncertainty().expect("declared").relative_ppm + ); + } + // The GPU was not this process's alone over the window, or its rail + // gave no reading: no figure, and the reason kept. + Err(why @ (Refused::NoGpuFigure(_) | Refused::Counter(_))) => { + assert_eq!((runs.joules_micro, runs.class.clone()), (0, None), "{why}"); + eprintln!("quantum-meter hardware: {} window unmetered: {why}", gpu.id()); + } + Err(other) => panic!("a two-second window of the member's runs was refused: {other}"), + } + + // The CPU reference's runs, in the same domain: no GPU time, so the + // figure is the CPU count alone. + let runs = span_of_runs(&mut meter, Duration::from_secs(1), on_cpu).expect("the reference runs"); + match meter.last().expect("a window was closed") { + Ok(span) => { + assert_eq!((span.gpu_ns, span.gpu_uj), (0, 0), "the reference spends no GPU time: {span:?}"); + assert!(span.cpu_uj > 0, "{span:?}"); + assert_eq!(runs.joules_micro, span.cpu_uj); + let class = runs.class.clone().expect("a second of CPU work is labelled"); + assert_eq!(check_label(runs.joules_micro, &class), Ok(())); + let mut calibration = EnergyCalibration::new(); + calibration.add(runs.ops, runs.joules_micro, runs.class.as_ref()); + let (nanojoules, _) = calibration.finish().expect("a figure"); + eprintln!( + "quantum-meter hardware: the CPU reference on {} qubits: {} runs in {:?}, cpu {} uJ -> {nanojoules} nJ/op, {} ppm", + wide.n_qubits, + runs.runs, + runs.wall, + span.cpu_uj, + class.uncertainty().expect("declared").relative_ppm + ); + } + // Another process's GPU client came or went over the window: the GPU + // counter then gives no figure, a zero included. + Err(why @ (Refused::NoGpuFigure(_) | Refused::Counter(_))) => { + assert_eq!((runs.joules_micro, runs.class.clone()), (0, None), "{why}"); + eprintln!("quantum-meter hardware: the CPU reference's window unmetered: {why}"); + } + Err(other) => panic!("a one-second window of the reference's runs was refused: {other}"), + } + + // One run of the member, in the window the fabric opens around it: far + // under a second, so it is unmetered, never read at a resolution the rail + // does not have. + let small = circuit(6); + let (_, joules_micro, class) = blocking::simulate_metered(gpu, &small, 0, &mut meter).expect("the run itself"); + assert_eq!((joules_micro, class), (0, None), "a run of {:?}", meter.last()); + match meter.last().expect("the run's window was closed") { + Ok(span) => assert!(matches!(label(span, DECLARED), Err(Refused::TooShort { .. })), "{span:?}"), + Err(Refused::NoGpuFigure(_) | Refused::Counter(_)) => {} + Err(other) => panic!("{other}"), + } + // Every start was matched by a stop: the meter starts again. + assert!(meter.start().is_ok()); + assert!(meter.stop().is_ok()); +} -- 2.54.0 (Apple Git-157) From 8606de265c45133d5f28314bc0fd2e25e2783795 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 13 Oct 2026 02:48:54 -0400 Subject: [PATCH 2/2] quantum-meter: the GPU probe's load runs for 50 ms each call, not one run The first meter in a process probes the GPU counter with a load, and the probe needs a few tens of milliseconds of this process's GPU work between its samples. One 18-qubit run of the member, about 2 ms on an M4 Max, passed there by margin; on the CI Mac (M3 Ultra), whose GPU counter the energy crate's own test finds live under a real burn, it was judged Frozen, and every window was rightly unmetered. probe_load wraps one run of a member so each call repeats it until 50 ms have passed. The tool and the device test use it. Co-Authored-By: Claude Opus 5.5 --- wai/tools/quantum-meter/src/lib.rs | 30 ++++++++++++++++++++--- wai/tools/quantum-meter/src/main.rs | 6 ++--- wai/tools/quantum-meter/tests/hardware.rs | 6 ++--- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/wai/tools/quantum-meter/src/lib.rs b/wai/tools/quantum-meter/src/lib.rs index 180125e56..85c4863f1 100644 --- a/wai/tools/quantum-meter/src/lib.rs +++ b/wai/tools/quantum-meter/src/lib.rs @@ -79,6 +79,29 @@ pub const MIN_CPU_WINDOW: Duration = Duration::from_millis(1); /// republish interval at each end is a few percent of the figure. pub const MIN_GPU_WINDOW: Duration = Duration::from_secs(1); +/// How long the GPU counter's liveness probe load runs each time the probe +/// calls it ([`probe_load`]). The probe needs a few tens of milliseconds of +/// this process's GPU work between its samples, and one run of a member can +/// be far less: an 18-qubit run took about 2 ms on an M4 Max, and a host with +/// a larger GPU takes less. A single run as the load passed the probe on one +/// Mac and failed it, `Frozen`, on another whose GPU counter is live. +pub const PROBE_LOAD: Duration = Duration::from_millis(50); + +/// A load for [`ProcessRunMeter::new`] from one run of a member: `run` +/// repeated until [`PROBE_LOAD`] has passed, once at least, each time the +/// probe calls it. `run` must wait for its GPU work, as a member's run does. +pub fn probe_load(mut run: impl FnMut()) -> impl FnMut() { + move || { + let began = Instant::now(); + loop { + run(); + if began.elapsed() >= PROBE_LOAD { + break; + } + } + } +} + /// What the operator declares about the counters. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Declared { @@ -195,9 +218,10 @@ impl ProcessRunMeter { /// The meter. The first one in a process probes the counters: about /// 0.3 s of this thread's CPU, and `gpu_load`, which must put a few tens /// of milliseconds of work on the GPU from this process and wait for it - /// (a run of the member to be metered does). The answer is kept for the - /// life of the process, so a first meter whose `gpu_load` does no GPU - /// work leaves every later one without a GPU counter. + /// each time it is called: [`probe_load`] around a run of the member to + /// be metered. The answer is kept for the life of the process, so a first + /// meter whose `gpu_load` does too little GPU work leaves every later one + /// without a GPU counter. pub fn new(gpu_load: impl FnMut(), declared: Declared) -> Self { ProcessRunMeter { meter: ProcessSpanMeter::new(gpu_load), declared, open: None, last: None } } diff --git a/wai/tools/quantum-meter/src/main.rs b/wai/tools/quantum-meter/src/main.rs index c1d3cda16..244e6bd35 100644 --- a/wai/tools/quantum-meter/src/main.rs +++ b/wai/tools/quantum-meter/src/main.rs @@ -18,7 +18,7 @@ //! Exit 0 when it ran, with or without figures; 1 when a member could not be //! found or run; 2 for a bad argument. -use quantum_meter::{Declared, ProcessRunMeter, Refused, Runs, Span, label, span_of_runs}; +use quantum_meter::{Declared, ProcessRunMeter, Refused, Runs, Span, label, probe_load, span_of_runs}; use std::process::ExitCode; use std::time::Duration; use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset}; @@ -135,9 +135,9 @@ fn main() -> ExitCode { // The first meter in a process probes the GPU counter with this load: a // run of the member it will meter. let mut meter = ProcessRunMeter::new( - || { + probe_load(|| { let _ = blocking::simulate_from(gpu, &wide, 0); - }, + }), args.declared, ); println!("quantum-meter: counters live={} gpu={}", meter.is_live(), meter.gpu_liveness()); diff --git a/wai/tools/quantum-meter/tests/hardware.rs b/wai/tools/quantum-meter/tests/hardware.rs index 306c30739..fd0470579 100644 --- a/wai/tools/quantum-meter/tests/hardware.rs +++ b/wai/tools/quantum-meter/tests/hardware.rs @@ -14,7 +14,7 @@ //! which a shared host cannot prevent; the test then requires the refusal to //! say so, and no figure. -use quantum_meter::{Declared, MIN_GPU_WINDOW, ProcessRunMeter, Refused, label, span_of_runs}; +use quantum_meter::{Declared, MIN_GPU_WINDOW, ProcessRunMeter, Refused, label, probe_load, span_of_runs}; use std::time::Duration; use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset}; use wai_quantum::quantum_energy::{EnergyClass, check_label}; @@ -47,9 +47,9 @@ fn a_members_runs_are_metered_over_a_window_the_counters_resolve() { let wide = circuit(gpu.max_qubits().min(18)); let ops = pair_updates(&wide); let mut meter = ProcessRunMeter::new( - || { + probe_load(|| { let _ = blocking::simulate_from(gpu, &wide, 0); - }, + }), DECLARED, ); let on_gpu = || blocking::simulate_from(gpu, &wide, 0).map(|_| ops).map_err(|e| format!("{e:?}")); -- 2.54.0 (Apple Git-157)