diff --git a/.github/workflows/joule-code-conformance.yml b/.github/workflows/joule-code-conformance.yml index e0b149410..f76ba2d89 100644 --- a/.github/workflows/joule-code-conformance.yml +++ b/.github/workflows/joule-code-conformance.yml @@ -223,6 +223,12 @@ jobs: working-directory: joule-code/examples/l3-jouleclaw run: cargo test + # Its own code only (`--no-deps`): the JouleClaw crates it builds through + # path dependencies are linted by their own workspace. + - name: L3 JouleClaw binding example lints clean + working-directory: joule-code/examples/l3-jouleclaw + run: cargo clippy --all-targets --no-deps -- -D warnings + # The MOQT relay, at draft-22 and, through its bridge, -18 and -16 (tests/drafts.rs, # tests/publish_done_streams.rs). Every test is on loopback: two publishers and three # subscribers over native QUIC, WebTransport over HTTP/3 and the WebSocket fallback; a diff --git a/.github/workflows/standards-build.yml b/.github/workflows/standards-build.yml index 46439dde2..b42184e4b 100644 --- a/.github/workflows/standards-build.yml +++ b/.github/workflows/standards-build.yml @@ -77,12 +77,12 @@ jobs: energy: ${{ steps.plan.outputs.energy }} env: WORKSPACES: | - - { name: jcp-rs, dir: jcp/jcp-rs, runner: linux, conformance: jcp-conformance, feature_tests: "-p jcp-energy --features nvml" } + - { name: jcp-rs, dir: jcp/jcp-rs, runner: linux, conformance: jcp-conformance, feature_tests: "-p jcp-energy --features nvml", meter_lint: "jcp-energy jcp-energy:nvml" } # sandbox-tee's certificate-chain verifiers sit behind features (their # X.509 and RSA dependencies are heavy), so a plain `cargo test` never # runs their tests: `amd-kds` (SEV-SNP's VCEK to AMD's root) and # `intel-pcs` (TDX's PCK to Intel's root). - - { name: sandbox-rs, dir: sandbox/sandbox-rs, runner: linux, conformance: sandbox-conformance, feature_tests: "-p sandbox-tee --features amd-kds,intel-pcs" } + - { name: sandbox-rs, dir: sandbox/sandbox-rs, runner: linux, conformance: sandbox-conformance, feature_tests: "-p sandbox-tee --features amd-kds,intel-pcs", meter_lint: "sandbox-meter" } - { name: smart-byte-rs, dir: smart-byte/smart-byte-rs, runner: linux } - { name: pool-rs, dir: pool/pool-rs, runner: linux } # `clippy: true` adds the zero-warning gate below. It is opt-in per @@ -139,7 +139,7 @@ jobs: # able to fail becomes a finding rather than staying green. - { name: proof-rs, dir: proof/proof-rs, runner: linux, z3: true, conformance: proof-conformance, applied: proof/conformance/v1/joulespec.toml } - { name: joulehook-rs, dir: joulehook/joulehook-rs, runner: linux } - - { name: eoc-rs, dir: eoc/eoc-rs, runner: linux, feature_tests: "-p eoc-meter --features cuda" } + - { name: eoc-rs, dir: eoc/eoc-rs, runner: linux, feature_tests: "-p eoc-meter --features cuda", meter_lint: "eoc-meter eoc-meter:cuda" } # mesh-openai's `serve_example` test (the reference server seals what # its meter read, never a fixture's figure) has required-features # `server`, so `cargo test --workspace` builds none of it; naming the @@ -163,7 +163,7 @@ jobs: # failing for no apparent reason, check `df -h /mnt/lima-colima-txarm` # on the txarm VM before suspecting the code: a full cache volume # fails whatever happens to be building, not whatever changed. - - { name: jouleclaw-rs, dir: jouleclaw/jouleclaw-rs, runner: linux-arm64, rustflags: "-C target-feature=+fp16", cache_suffix: "-nodebug", feature_tests: "-p jouleclaw-energy --features rapl,nvml" } + - { name: jouleclaw-rs, dir: jouleclaw/jouleclaw-rs, runner: linux-arm64, rustflags: "-C target-feature=+fp16", cache_suffix: "-nodebug", feature_tests: "-p jouleclaw-energy --features rapl,nvml", meter_lint: "jouleclaw-energy jouleclaw-energy:rapl jouleclaw-energy:nvml jouleclaw-energy:rapl,nvml" } steps: - uses: actions/checkout@v5 with: @@ -456,6 +456,28 @@ jobs: RUSTFLAGS: ${{ matrix.rustflags }} run: cargo test ${{ matrix.feature_tests }} + # The meter crates' Linux side: RAPL (powercap and the MSRs), NVML and + # CUDA are Linux code or sit behind features no workspace member enables, + # and the macOS lint step lints these crates only as macOS builds them. + # So no job linted them here, and jouleclaw-energy's `rapl` build carried + # a dead function, and four crates' NVML hardware tests a clippy error, + # for as long as they existed. Each `meter_lint` entry is a package, or + # `:`, linted on the workspace's own runner. + - name: clippy on the Linux meter code + if: matrix.meter_lint + working-directory: ${{ matrix.dir }} + shell: bash + env: + RUSTFLAGS: ${{ matrix.rustflags }} + run: | + set -eu + for config in ${{ matrix.meter_lint }}; do + package=${config%%:*} + features=() + [ "$config" = "$package" ] || features=(--features "${config#*:}") + cargo clippy -p "$package" "${features[@]}" --all-targets --no-deps -- -D warnings + done + # The page-set packages are not committed; their catalogue is, naming # each package by its content. Any change to what they are built from — # the sources, the app's translations and gloss audits, the symbol diff --git a/eoc/eoc-rs/crates/eoc-meter/tests/nvml_hardware.rs b/eoc/eoc-rs/crates/eoc-meter/tests/nvml_hardware.rs index bc1401c0a..0f328bfed 100644 --- a/eoc/eoc-rs/crates/eoc-meter/tests/nvml_hardware.rs +++ b/eoc/eoc-rs/crates/eoc-meter/tests/nvml_hardware.rs @@ -138,9 +138,13 @@ mod nvml_oracle { } } + /// One GPU as `nvidia-smi` gives it, in watts: (maximum power limit, + /// enforced limit, draw), each `None` where it gave none. + pub type SmiPower = (Option, Option, Option); + /// `nvidia-smi`'s view of every GPU: (maximum power limit, enforced limit, /// draw), in watts; `None` without `nvidia-smi`. - pub fn smi() -> Option, Option, Option)>> { + pub fn smi() -> Option> { let out = std::process::Command::new("nvidia-smi") .args(["--query-gpu=power.max_limit,enforced.power.limit,power.draw", "--format=csv,noheader,nounits"]) .output() @@ -158,7 +162,7 @@ mod nvml_oracle { /// GPU `i`'s ceiling in watts: `nvidia-smi`'s maximum power limit, else /// NVML's. - pub fn ceiling_w(nvml: &Nvml, smi: &Option, Option, Option)>>, i: usize) -> Option { + pub fn ceiling_w(nvml: &Nvml, smi: &Option>, i: usize) -> Option { smi.as_ref() .and_then(|s| s.get(i)) .and_then(|(max, _, _)| *max) @@ -319,7 +323,7 @@ mod nvml_oracle { let window = last_at; let rate_w: Vec = first.iter().zip(&prev).map(|(a, b)| b.saturating_sub(*a) as f64 / 1000.0 / window.as_secs_f64().max(1e-9)).collect(); - let draw = |s: &Option, Option, Option)>>, i: usize| s.as_ref().and_then(|s| s.get(i)).and_then(|t| t.2); + let draw = |s: &Option>, i: usize| s.as_ref().and_then(|s| s.get(i)).and_then(|t| t.2); let draws_w: Vec> = (0..n).map(|i| [draw(&before, i), draw(&after, i)].into_iter().flatten().collect()).collect(); let ceiling_w: Vec> = (0..n).map(|i| ceiling_w(&nvml, &after, i)).collect(); // The verdict that speaks for the run is the most severe of its GPUs', diff --git a/jcp/jcp-rs/crates/jcp-energy/src/lib.rs b/jcp/jcp-rs/crates/jcp-energy/src/lib.rs index 629a2c36d..f040eef8e 100644 --- a/jcp/jcp-rs/crates/jcp-energy/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-energy/src/lib.rs @@ -6312,9 +6312,9 @@ impl FabricMeter { /// Which fabrics this meter can read, and whose work each figure counts. pub fn readable(&self) -> Vec<(Fabric, Attribution)> { - let mut out = Vec::new(); #[cfg(target_os = "macos")] { + let mut out = Vec::new(); if self.process.is_some() { out.push((Fabric::Cpu, Attribution::Process)); } @@ -6324,8 +6324,12 @@ impl FabricMeter { out.push((Fabric::Npu, Attribution::Machine)); } } + out + } + #[cfg(not(target_os = "macos"))] + { + Vec::new() } - out } /// Run `work`, and report each fabric's energy over it. @@ -6352,9 +6356,9 @@ impl FabricMeter { let out = work(); let span_ns = started.elapsed().as_nanos(); - let mut readings = Vec::new(); #[cfg(target_os = "macos")] - { + let readings = { + let mut readings = Vec::new(); let cpu_after = cpu_before.and_then(|_| self.process.as_ref()?.read_energy_nj()); if let (Some(b), Some(a)) = (cpu_before, cpu_after) { readings.push(FabricEnergy { @@ -6383,8 +6387,13 @@ impl FabricMeter { }); } } - } - let _ = span_ns; + readings + }; + #[cfg(not(target_os = "macos"))] + let readings = { + let _ = span_ns; + Vec::new() + }; (out, readings) } } @@ -6461,11 +6470,8 @@ impl FabricSource { /// cannot be measured" are different facts and a survey that blurs them is /// worthless. See [`FabricStatus::NotImplemented`]. pub fn has_backend(self) -> bool { - match self { - FabricSource::ProcRusage | FabricSource::IoReport | FabricSource::Rapl => true, - FabricSource::Nvml => cfg!(feature = "nvml"), - _ => false, - } + matches!(self, FabricSource::ProcRusage | FabricSource::IoReport | FabricSource::Rapl) + || (matches!(self, FabricSource::Nvml) && cfg!(feature = "nvml")) } } diff --git a/jcp/jcp-rs/crates/jcp-energy/tests/nvml_hardware.rs b/jcp/jcp-rs/crates/jcp-energy/tests/nvml_hardware.rs index 3ccbc92db..359c95e0f 100644 --- a/jcp/jcp-rs/crates/jcp-energy/tests/nvml_hardware.rs +++ b/jcp/jcp-rs/crates/jcp-energy/tests/nvml_hardware.rs @@ -137,9 +137,13 @@ mod nvml_oracle { } } + /// One GPU as `nvidia-smi` gives it, in watts: (maximum power limit, + /// enforced limit, draw), each `None` where it gave none. + pub type SmiPower = (Option, Option, Option); + /// `nvidia-smi`'s view of every GPU: (maximum power limit, enforced limit, /// draw), in watts; `None` without `nvidia-smi`. - pub fn smi() -> Option, Option, Option)>> { + pub fn smi() -> Option> { let out = std::process::Command::new("nvidia-smi") .args(["--query-gpu=power.max_limit,enforced.power.limit,power.draw", "--format=csv,noheader,nounits"]) .output() @@ -157,7 +161,7 @@ mod nvml_oracle { /// GPU `i`'s ceiling in watts: `nvidia-smi`'s maximum power limit, else /// NVML's. - pub fn ceiling_w(nvml: &Nvml, smi: &Option, Option, Option)>>, i: usize) -> Option { + pub fn ceiling_w(nvml: &Nvml, smi: &Option>, i: usize) -> Option { smi.as_ref() .and_then(|s| s.get(i)) .and_then(|(max, _, _)| *max) @@ -318,7 +322,7 @@ mod nvml_oracle { let window = last_at; let rate_w: Vec = first.iter().zip(&prev).map(|(a, b)| b.saturating_sub(*a) as f64 / 1000.0 / window.as_secs_f64().max(1e-9)).collect(); - let draw = |s: &Option, Option, Option)>>, i: usize| s.as_ref().and_then(|s| s.get(i)).and_then(|t| t.2); + let draw = |s: &Option>, i: usize| s.as_ref().and_then(|s| s.get(i)).and_then(|t| t.2); let draws_w: Vec> = (0..n).map(|i| [draw(&before, i), draw(&after, i)].into_iter().flatten().collect()).collect(); let ceiling_w: Vec> = (0..n).map(|i| ceiling_w(&nvml, &after, i)).collect(); // The verdict that speaks for the run is the most severe of its GPUs', diff --git a/joule-code/examples/l3-jouleclaw/src/bin/run.rs b/joule-code/examples/l3-jouleclaw/src/bin/run.rs index b026f9711..7bd2c5bc1 100644 --- a/joule-code/examples/l3-jouleclaw/src/bin/run.rs +++ b/joule-code/examples/l3-jouleclaw/src/bin/run.rs @@ -724,7 +724,9 @@ mod tests { assert_eq!(note, "estimated by apple-silicon from host load"); } _ => { - assert!(!cfg!(all(target_os = "macos", target_arch = "aarch64")), "a Mac with no host counter: {name}"); + if cfg!(all(target_os = "macos", target_arch = "aarch64")) { + panic!("a Mac with no host counter: {name}"); + } assert_eq!((name, from_counter), ("none", false)); assert_eq!(e, Energy::from_tokens(7, 9)); } diff --git a/joule-code/examples/l3-jouleclaw/src/llama_backend.rs b/joule-code/examples/l3-jouleclaw/src/llama_backend.rs index bb5a9b197..99dc2b40e 100644 --- a/joule-code/examples/l3-jouleclaw/src/llama_backend.rs +++ b/joule-code/examples/l3-jouleclaw/src/llama_backend.rs @@ -245,7 +245,7 @@ fn count_before(stderr: &str, unit: &str) -> Option { for line in stderr.lines() { if let Some(idx) = line.find(&needle) { let prefix = &line[..idx]; - let token = prefix.rsplit(|c: char| c == '/' || c == ' ').find(|s| !s.is_empty()); + let token = prefix.rsplit(['/', ' ']).find(|s| !s.is_empty()); if let Some(n) = token.and_then(|t| t.trim().parse::().ok()) { return Some(n); } diff --git a/joule-code/examples/l3-jouleclaw/tests/common/mod.rs b/joule-code/examples/l3-jouleclaw/tests/common/mod.rs index 4c36d3fec..3805dbd97 100644 --- a/joule-code/examples/l3-jouleclaw/tests/common/mod.rs +++ b/joule-code/examples/l3-jouleclaw/tests/common/mod.rs @@ -43,6 +43,10 @@ impl LlmBackend for OneMatmul { /// declaring none), switch the fabric on for this process, resolve one L3 /// request, and return its receipt energy with the ledger window it was priced /// from. Call once per test process: the fabric is discovered once. +// Each test binary compiles this module on its own, and the seeded test does +// not price a row, so there this helper is unused (as `resolve_seeded` is in +// the others). +#[allow(dead_code)] pub fn resolve_priced_at(tag: &str, grade: Option) -> (Energy, Vec) { let path = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("l3-fabric-grade-{tag}.json")); let declared = grade.map(|g| format!(r#","provenance":"{g:?}""#)).unwrap_or_default(); diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/src/rapl.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/src/rapl.rs index 8b6591bf9..b3679d810 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/src/rapl.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/src/rapl.rs @@ -551,13 +551,13 @@ fn cached(set: &[PathBuf], probe: impl FnOnce() -> CounterLiveness) -> CounterLi /// caller is the cache in [`package_liveness`]. #[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn probe_packages(packages: &[Package]) -> CounterLiveness { - probe_packages_stepping(packages, || {}) + crate::liveness::probe(|| open_sampler(packages), PROBE_TICK) } /// [`probe_packages`], with `before_burn` run at the start of every tick /// ([`crate::liveness::probe_stepping`]): how a test advances a fake tree in /// step with the probe. -#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +#[cfg(test)] fn probe_packages_stepping(packages: &[Package], before_burn: impl FnMut()) -> CounterLiveness { crate::liveness::probe_stepping(|| open_sampler(packages), PROBE_TICK, before_burn) } diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/tests/nvml_hardware.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/tests/nvml_hardware.rs index 411fdfbdb..685adab67 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/tests/nvml_hardware.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-energy/tests/nvml_hardware.rs @@ -139,9 +139,13 @@ mod nvml_oracle { } } + /// One GPU as `nvidia-smi` gives it, in watts: (maximum power limit, + /// enforced limit, draw), each `None` where it gave none. + pub type SmiPower = (Option, Option, Option); + /// `nvidia-smi`'s view of every GPU: (maximum power limit, enforced limit, /// draw), in watts; `None` without `nvidia-smi`. - pub fn smi() -> Option, Option, Option)>> { + pub fn smi() -> Option> { let out = std::process::Command::new("nvidia-smi") .args(["--query-gpu=power.max_limit,enforced.power.limit,power.draw", "--format=csv,noheader,nounits"]) .output() @@ -159,7 +163,7 @@ mod nvml_oracle { /// GPU `i`'s ceiling in watts: `nvidia-smi`'s maximum power limit, else /// NVML's. - pub fn ceiling_w(nvml: &Nvml, smi: &Option, Option, Option)>>, i: usize) -> Option { + pub fn ceiling_w(nvml: &Nvml, smi: &Option>, i: usize) -> Option { smi.as_ref() .and_then(|s| s.get(i)) .and_then(|(max, _, _)| *max) @@ -320,7 +324,7 @@ mod nvml_oracle { let window = last_at; let rate_w: Vec = first.iter().zip(&prev).map(|(a, b)| b.saturating_sub(*a) as f64 / 1000.0 / window.as_secs_f64().max(1e-9)).collect(); - let draw = |s: &Option, Option, Option)>>, i: usize| s.as_ref().and_then(|s| s.get(i)).and_then(|t| t.2); + let draw = |s: &Option>, i: usize| s.as_ref().and_then(|s| s.get(i)).and_then(|t| t.2); let draws_w: Vec> = (0..n).map(|i| [draw(&before, i), draw(&after, i)].into_iter().flatten().collect()).collect(); let ceiling_w: Vec> = (0..n).map(|i| ceiling_w(&nvml, &after, i)).collect(); // The verdict that speaks for the run is the most severe of its GPUs', diff --git a/sandbox/sandbox-rs/crates/sandbox-meter/tests/nvml_hardware.rs b/sandbox/sandbox-rs/crates/sandbox-meter/tests/nvml_hardware.rs index bc5812ed0..fdd060c09 100644 --- a/sandbox/sandbox-rs/crates/sandbox-meter/tests/nvml_hardware.rs +++ b/sandbox/sandbox-rs/crates/sandbox-meter/tests/nvml_hardware.rs @@ -139,9 +139,13 @@ mod nvml_oracle { } } + /// One GPU as `nvidia-smi` gives it, in watts: (maximum power limit, + /// enforced limit, draw), each `None` where it gave none. + pub type SmiPower = (Option, Option, Option); + /// `nvidia-smi`'s view of every GPU: (maximum power limit, enforced limit, /// draw), in watts; `None` without `nvidia-smi`. - pub fn smi() -> Option, Option, Option)>> { + pub fn smi() -> Option> { let out = std::process::Command::new("nvidia-smi") .args(["--query-gpu=power.max_limit,enforced.power.limit,power.draw", "--format=csv,noheader,nounits"]) .output() @@ -159,7 +163,7 @@ mod nvml_oracle { /// GPU `i`'s ceiling in watts: `nvidia-smi`'s maximum power limit, else /// NVML's. - pub fn ceiling_w(nvml: &Nvml, smi: &Option, Option, Option)>>, i: usize) -> Option { + pub fn ceiling_w(nvml: &Nvml, smi: &Option>, i: usize) -> Option { smi.as_ref() .and_then(|s| s.get(i)) .and_then(|(max, _, _)| *max) @@ -320,7 +324,7 @@ mod nvml_oracle { let window = last_at; let rate_w: Vec = first.iter().zip(&prev).map(|(a, b)| b.saturating_sub(*a) as f64 / 1000.0 / window.as_secs_f64().max(1e-9)).collect(); - let draw = |s: &Option, Option, Option)>>, i: usize| s.as_ref().and_then(|s| s.get(i)).and_then(|t| t.2); + let draw = |s: &Option>, i: usize| s.as_ref().and_then(|s| s.get(i)).and_then(|t| t.2); let draws_w: Vec> = (0..n).map(|i| [draw(&before, i), draw(&after, i)].into_iter().flatten().collect()).collect(); let ceiling_w: Vec> = (0..n).map(|i| ceiling_w(&nvml, &after, i)).collect(); // The verdict that speaks for the run is the most severe of its GPUs',