diff --git a/docs/superpowers/plans/2026-05-09-battery-gpu-aware-thread-tuning.md b/docs/superpowers/plans/2026-05-09-battery-gpu-aware-thread-tuning.md new file mode 100644 index 0000000..2f14658 --- /dev/null +++ b/docs/superpowers/plans/2026-05-09-battery-gpu-aware-thread-tuning.md @@ -0,0 +1,1403 @@ +# Battery- and GPU-aware Inference Thread Tuning Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add power-aware and GPU-offload-aware clamps to `inference_thread_count` so Lumenote drops to ~half threads on battery and to a workload-specific floor when fully GPU-offloaded. + +**Architecture:** Two new modules in `magnotia-core`: `power.rs` (Linux sysfs probe with TTL cache) and `tuning.rs` (Workload enum + helper). Both whisper and LLM call sites compute a `gpu_offloaded` bool and pass it in. `vulkan_loader_available()` moves from `src-tauri` to `magnotia-core::hardware` so transcription can call it. Old `constants::inference_thread_count` is removed once both call sites migrate. + +**Tech Stack:** Rust 2021, `magnotia-core` (existing pure-Rust crate), `num_cpus`, `libloading`, `tracing`, `tempfile` (dev). No Tauri or platform FFI for the core probe — Linux sysfs only in v1. + +**Spec:** [docs/superpowers/specs/2026-05-09-battery-gpu-aware-thread-tuning-design.md](../specs/2026-05-09-battery-gpu-aware-thread-tuning-design.md) + +--- + +## File Structure + +**New files:** +- `crates/core/src/power.rs` — `PowerState` enum, sysfs probe, TTL cache, test override. +- `crates/core/src/tuning.rs` — `Workload` enum, `inference_thread_count`, per-process logging. + +**Modified files:** +- `crates/core/Cargo.toml` — add `libloading`, `tracing`; dev-deps add `tempfile`. +- `crates/core/src/lib.rs` — register new modules, re-export public API. +- `crates/core/src/hardware.rs` — add `vulkan_loader_available()`, ported from src-tauri. +- `crates/core/src/constants.rs` — remove `inference_thread_count` and the related min/max constants (move semantically to `tuning.rs`; the audio/RAM/VAD/etc. constants stay). +- `src-tauri/src/commands/models.rs` — delete local `vulkan_loader_available` definition, replace with `magnotia_core::hardware::vulkan_loader_available` call. +- `src-tauri/Cargo.toml` — keep `libloading` since src-tauri still depends on it elsewhere; verify with `cargo tree`. +- `crates/transcription/src/whisper_rs_backend.rs` — compute `gpu_offloaded`, call `tuning::inference_thread_count(Workload::Whisper, gpu_offloaded)`. +- `crates/llm/src/lib.rs` — compute `gpu_offloaded` after model load, call `tuning::inference_thread_count(Workload::Llm, gpu_offloaded)`. +- `crates/transcription/tests/thread_sweep.rs` — extend to print 4 panels (CPU/GPU × AC/battery). + +--- + +## Phase 1 — `power.rs`: PowerState module + +### Task 1.1: Skeleton + lib registration + +**Files:** +- Create: `crates/core/src/power.rs` +- Modify: `crates/core/src/lib.rs` +- Modify: `crates/core/Cargo.toml` (add `tempfile = "3"` to dev-dependencies) + +- [ ] **Step 1: Add tempfile dev-dep** + +Edit `crates/core/Cargo.toml` — append after the `[dependencies]` block: + +```toml +[dev-dependencies] +tempfile = "3" +``` + +- [ ] **Step 2: Write the failing test for the enum** + +Create `crates/core/src/power.rs`: + +```rust +//! Power-state probe for inference thread tuning. +//! +//! Reports whether the machine is running on AC or battery so callers +//! can drop thread counts when energy matters more than throughput. +//! Linux uses the documented sysfs ABI under +//! `/sys/class/power_supply/`. macOS and Windows return `Unknown` for +//! now; native probes (IOPSGetProvidingPowerSourceType, +//! GetSystemPowerStatus) are deferred. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PowerState { + OnAc, + OnBattery, + Unknown, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn power_state_variants_are_distinct() { + assert_ne!(PowerState::OnAc, PowerState::OnBattery); + assert_ne!(PowerState::OnAc, PowerState::Unknown); + assert_ne!(PowerState::OnBattery, PowerState::Unknown); + } +} +``` + +Add to `crates/core/src/lib.rs` after the existing module declarations: + +```rust +pub mod power; +``` + +- [ ] **Step 3: Run the test** + +Run: `cargo test -p magnotia-core --lib power::tests::power_state_variants_are_distinct` +Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +cd /home/jake/Documents/CORBEL-Projects/magnotia +git add crates/core/Cargo.toml crates/core/src/power.rs crates/core/src/lib.rs +git commit -m "feat(core): add PowerState skeleton in new power module" +``` + +--- + +### Task 1.2: Sysfs parser + +**Files:** +- Modify: `crates/core/src/power.rs` + +- [ ] **Step 1: Write failing tests for sysfs parsing** + +Append to `crates/core/src/power.rs` inside `mod tests` (replace the existing `mod tests` block with this expanded version): + +```rust +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::tempdir; + + #[test] + fn power_state_variants_are_distinct() { + assert_ne!(PowerState::OnAc, PowerState::OnBattery); + assert_ne!(PowerState::OnAc, PowerState::Unknown); + assert_ne!(PowerState::OnBattery, PowerState::Unknown); + } + + fn write_supply(dir: &std::path::Path, name: &str, ty: &str, online: &str) { + let entry = dir.join(name); + fs::create_dir_all(&entry).unwrap(); + fs::write(entry.join("type"), format!("{ty}\n")).unwrap(); + fs::write(entry.join("online"), format!("{online}\n")).unwrap(); + } + + #[test] + fn parses_mains_online_as_on_ac() { + let dir = tempdir().unwrap(); + write_supply(dir.path(), "AC", "Mains", "1"); + write_supply(dir.path(), "BAT0", "Battery", "0"); + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnAc); + } + + #[test] + fn parses_battery_only_as_on_battery() { + let dir = tempdir().unwrap(); + write_supply(dir.path(), "AC", "Mains", "0"); + write_supply(dir.path(), "BAT0", "Battery", "0"); + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnBattery); + } + + #[test] + fn parses_usb_pd_online_as_on_ac() { + let dir = tempdir().unwrap(); + write_supply(dir.path(), "ucsi-source-psy-USBC000:001", "USB", "1"); + write_supply(dir.path(), "BAT0", "Battery", "0"); + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::OnAc); + } + + #[test] + fn parses_empty_dir_as_unknown() { + let dir = tempdir().unwrap(); + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::Unknown); + } + + #[test] + fn parses_missing_dir_as_unknown() { + let path = std::path::Path::new("/no/such/path/should/exist/at/this/inode/123456"); + assert_eq!(parse_power_state_from_dir(path), PowerState::Unknown); + } + + #[test] + fn parses_malformed_files_as_unknown_gracefully() { + let dir = tempdir().unwrap(); + let entry = dir.path().join("garbage"); + fs::create_dir_all(&entry).unwrap(); + // No type, no online — should not panic. + assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::Unknown); + } +} +``` + +- [ ] **Step 2: Run tests, expect compile failure** + +Run: `cargo test -p magnotia-core --lib power::tests` +Expected: compile error — `parse_power_state_from_dir` not found. + +- [ ] **Step 3: Implement parser** + +Add above the `#[cfg(test)] mod tests` block in `crates/core/src/power.rs`: + +```rust +use std::fs; +use std::path::Path; + +/// Parse a `/sys/class/power_supply/`-style directory and decide +/// whether the machine is on AC, on battery, or in an unknown state. +/// +/// Rules (matches `Documentation/ABI/testing/sysfs-class-power`): +/// - Any entry with `type` in {`Mains`, `USB`} and `online == 1` → OnAc. +/// - Else any entry with `type == Battery` → OnBattery. +/// - Else → Unknown. +/// +/// Failures (missing dir, unreadable files, malformed contents) all +/// fall through to Unknown rather than panicking. `Unknown` is treated +/// as `OnAc` by the caller, which preserves today's pre-clamp +/// behaviour on platforms or distros where the probe doesn't fire. +pub fn parse_power_state_from_dir(supply_dir: &Path) -> PowerState { + let read_dir = match fs::read_dir(supply_dir) { + Ok(rd) => rd, + Err(_) => return PowerState::Unknown, + }; + + let mut saw_battery = false; + for entry in read_dir.flatten() { + let path = entry.path(); + let ty = read_trimmed(&path.join("type")).unwrap_or_default(); + let online = read_trimmed(&path.join("online")).unwrap_or_default(); + match ty.as_str() { + "Mains" | "USB" => { + if online == "1" { + return PowerState::OnAc; + } + } + "Battery" => { + saw_battery = true; + } + _ => {} + } + } + + if saw_battery { + PowerState::OnBattery + } else { + PowerState::Unknown + } +} + +fn read_trimmed(path: &Path) -> Option { + fs::read_to_string(path).ok().map(|s| s.trim().to_owned()) +} +``` + +- [ ] **Step 4: Run tests, expect pass** + +Run: `cargo test -p magnotia-core --lib power::tests` +Expected: all 6 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add crates/core/src/power.rs +git commit -m "feat(core): parse_power_state_from_dir reads sysfs power_supply ABI" +``` + +--- + +### Task 1.3: `probe_power_state` + overrides + +**Files:** +- Modify: `crates/core/src/power.rs` + +This task adds the public probe entry point. Two override paths: +1. `with_override(state, |closure|)` — in-process, serialised via a `TEST_LOCK` mutex held for the body. Used by unit tests because cargo's parallel runner makes env-var-based tests flaky. +2. `MAGNOTIA_POWER_STATE_OVERRIDE` env var — used by `thread_sweep.rs` integration test (set externally, not from inside `cargo test`'s parallel block). + +- [ ] **Step 1: Write failing unit tests using `with_override`** + +Append inside `mod tests` in `crates/core/src/power.rs`: + +```rust + #[test] + fn override_drives_battery() { + with_override(Some(PowerState::OnBattery), || { + assert_eq!(probe_power_state(), PowerState::OnBattery); + }); + } + + #[test] + fn override_drives_ac() { + with_override(Some(PowerState::OnAc), || { + assert_eq!(probe_power_state(), PowerState::OnAc); + }); + } + + #[test] + fn override_drives_unknown() { + with_override(Some(PowerState::Unknown), || { + assert_eq!(probe_power_state(), PowerState::Unknown); + }); + } + + #[test] + fn env_var_override_battery_via_set_var() { + // env-var path tested in isolation under TEST_LOCK so it + // doesn't race with the in-process override tests. + with_override(None, || { + std::env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", "battery"); + assert_eq!(probe_power_state(), PowerState::OnBattery); + std::env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE"); + }); + } + + #[test] + fn env_var_override_garbage_falls_through() { + with_override(None, || { + std::env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", "nonsense"); + // Garbage value falls through to the platform probe. + // We can't assert the platform result so just assert it + // doesn't panic. + let _ = probe_power_state(); + std::env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE"); + }); + } +``` + +- [ ] **Step 2: Run tests, expect compile failure** + +Run: `cargo test -p magnotia-core --lib power::tests` +Expected: compile error — `probe_power_state`, `with_override` not found. + +- [ ] **Step 3: Implement probe + overrides** + +Add to `crates/core/src/power.rs` above `#[cfg(test)] mod tests`: + +```rust +use std::sync::Mutex; + +/// Top-level power-state probe. +/// +/// Resolution order (highest to lowest priority): +/// 1. In-process test override (set via `with_override` from unit tests). +/// 2. `MAGNOTIA_POWER_STATE_OVERRIDE` env var (`ac` | `battery` | `unknown`, +/// case-insensitive). Used by `thread_sweep.rs` integration tests. +/// 3. Linux: `parse_power_state_from_dir("/sys/class/power_supply")`. +/// 4. macOS / Windows / other: `Unknown`. +/// +/// `Unknown` is treated as `OnAc` by callers, which preserves today's +/// pre-clamp behaviour on platforms where the probe doesn't fire. +/// +/// (TTL cache is added in Task 1.4.) +pub fn probe_power_state() -> PowerState { + if let Some(state) = test_get_override() { + return state; + } + if let Some(state) = env_override() { + return state; + } + platform_probe() +} + +fn env_override() -> Option { + let raw = std::env::var("MAGNOTIA_POWER_STATE_OVERRIDE").ok()?; + match raw.trim().to_ascii_lowercase().as_str() { + "ac" => Some(PowerState::OnAc), + "battery" => Some(PowerState::OnBattery), + "unknown" => Some(PowerState::Unknown), + _ => None, + } +} + +#[cfg(target_os = "linux")] +fn platform_probe() -> PowerState { + parse_power_state_from_dir(Path::new("/sys/class/power_supply")) +} + +#[cfg(not(target_os = "linux"))] +fn platform_probe() -> PowerState { + PowerState::Unknown +} + +// In-process override slot. Tests must use `with_override` (below) to +// set it; production code never writes to this. Read on every probe. +static TEST_OVERRIDE: Mutex> = Mutex::new(None); + +fn test_get_override() -> Option { + *TEST_OVERRIDE.lock().expect("power test override mutex poisoned") +} + +/// Run `body` with the in-process override set to `state`. Restores +/// the previous override (always `None` in practice) on return. +/// +/// Holds a dedicated `TEST_LOCK` mutex for the duration of the body, +/// so override-using unit tests run serially with respect to each +/// other even when cargo runs the test binary multi-threaded. +#[cfg(test)] +pub(crate) fn with_override(state: Option, body: impl FnOnce() -> R) -> R { + static TEST_LOCK: Mutex<()> = Mutex::new(()); + let _lock = TEST_LOCK.lock().expect("power TEST_LOCK poisoned"); + *TEST_OVERRIDE.lock().expect("power test override mutex poisoned") = state; + let result = body(); + *TEST_OVERRIDE.lock().expect("power test override mutex poisoned") = None; + result +} +``` + +- [ ] **Step 4: Run tests, expect pass** + +Run: `cargo test -p magnotia-core --lib power::tests` +Expected: all tests in the module pass (sysfs parser tests from Task 1.2 plus the 5 new override tests). + +- [ ] **Step 5: Commit** + +```bash +git add crates/core/src/power.rs +git commit -m "feat(core): probe_power_state with Linux dispatch + override paths" +``` + +--- + +### Task 1.4: 10-second TTL cache + +**Files:** +- Modify: `crates/core/src/power.rs` + +The cache sits between override resolution and `platform_probe`. Override paths (test + env var) are never cached so they always take effect immediately. Two test-only helpers expose the cache: `force_clear_cache()` and `force_set_cache(state)`. + +The cache tests must run inside `with_override(None, || …)` so they hold the same `TEST_LOCK` mutex as the override tests; otherwise an override-using test running in parallel could pollute the assertions. + +- [ ] **Step 1: Implement the cache** + +Add to `crates/core/src/power.rs` near the other top-level items: + +```rust +use std::sync::OnceLock; +use std::time::{Duration, Instant}; + +const POWER_STATE_TTL: Duration = Duration::from_secs(10); + +struct CachedState { + state: PowerState, + fetched_at: Instant, +} + +fn cache_slot() -> &'static Mutex> { + static SLOT: OnceLock>> = OnceLock::new(); + SLOT.get_or_init(|| Mutex::new(None)) +} + +#[cfg(test)] +pub(crate) fn force_clear_cache() { + *cache_slot().lock().expect("power cache mutex poisoned") = None; +} + +#[cfg(test)] +pub(crate) fn force_set_cache(state: PowerState) { + *cache_slot().lock().expect("power cache mutex poisoned") = Some(CachedState { + state, + fetched_at: Instant::now(), + }); +} +``` + +- [ ] **Step 2: Wire the cache into `probe_power_state`** + +Replace the body of `probe_power_state`: + +```rust +pub fn probe_power_state() -> PowerState { + if let Some(state) = test_get_override() { + return state; + } + if let Some(state) = env_override() { + return state; + } + + let mut slot = cache_slot().lock().expect("power cache mutex poisoned"); + if let Some(cached) = &*slot { + if cached.fetched_at.elapsed() < POWER_STATE_TTL { + return cached.state; + } + } + let fresh = platform_probe(); + *slot = Some(CachedState { + state: fresh, + fetched_at: Instant::now(), + }); + fresh +} +``` + +- [ ] **Step 3: Write cache tests** + +Append inside `mod tests` in `crates/core/src/power.rs`: + +```rust + #[test] + fn ttl_cache_returns_cached_value_within_window() { + with_override(None, || { + force_clear_cache(); + force_set_cache(PowerState::OnBattery); + // No override active, no env var; probe should hit cache. + assert_eq!(probe_power_state(), PowerState::OnBattery); + }); + } + + #[test] + fn ttl_cache_clears_via_force_clear() { + with_override(None, || { + force_set_cache(PowerState::OnBattery); + force_clear_cache(); + // Override flips to OnAc; with cleared cache, override + // (which has higher priority) drives the result. + }); + with_override(Some(PowerState::OnAc), || { + force_clear_cache(); + assert_eq!(probe_power_state(), PowerState::OnAc); + }); + } +``` + +- [ ] **Step 4: Run tests, expect pass** + +Run: `cargo test -p magnotia-core --lib power::tests` +Expected: all power tests pass (sysfs parser + override + cache). + +- [ ] **Step 5: Commit** + +```bash +git add crates/core/src/power.rs +git commit -m "feat(core): 10s TTL cache on probe_power_state" +``` + +--- + +## Phase 2 — `tuning.rs`: Workload-aware helper + +### Task 2.1: Skeleton + Workload + helper that matches today's output + +**Files:** +- Create: `crates/core/src/tuning.rs` +- Modify: `crates/core/src/lib.rs` +- Modify: `crates/core/Cargo.toml` (add `tracing = "0.1"` to `[dependencies]`) + +- [ ] **Step 1: Add tracing dep** + +Edit `crates/core/Cargo.toml`, append in `[dependencies]`: + +```toml +tracing = "0.1" +``` + +- [ ] **Step 2: Write the failing test for the helper's pre-clamp behaviour** + +Create `crates/core/src/tuning.rs`: + +```rust +//! Inference thread-count tuning. Combines physical-core budget, +//! battery awareness, and GPU-offload awareness into a single helper +//! callable from both inference call sites. + +use crate::power::{self, PowerState}; + +/// Lower bound for inference threads. Single-threaded inference is +/// measurably worse than two on every multi-core part. +pub const MIN_INFERENCE_THREADS: usize = 2; + +/// Upper bound for inference threads. whisper.cpp + llama.cpp scaling +/// flattens around physical core count; SMT siblings contend for FPU +/// during F16/F32 matmul, so going past physical cores anti-scales. +/// 8 is a conservative ceiling that leaves <5% on the table for +/// big-iron desktops while keeping consumer 6c/12t laptops out of +/// contention territory. Users can override at runtime via +/// MAGNOTIA_INFERENCE_THREADS. +pub const MAX_INFERENCE_THREADS: usize = 8; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Workload { + /// Llama-style transformer. Near-zero CPU work when fully + /// offloaded; CPU floor in that case is GPU_FLOOR_LLM. + Llm, + /// Whisper transcription. Keeps mel spectrogram, decoder + /// bookkeeping, and beam search on the CPU even with full Vulkan + /// offload; CPU floor is higher: GPU_FLOOR_WHISPER. + Whisper, +} + +const GPU_FLOOR_LLM: usize = 2; +const GPU_FLOOR_WHISPER: usize = 4; + +/// Inference thread count, clamped to the physical-core budget plus +/// the battery and GPU-offload heuristics. +/// +/// Resolution order: +/// 1. `MAGNOTIA_INFERENCE_THREADS=N` — absolute bypass, returns N. +/// 2. base = num_cpus::get_physical() (fallback: available_parallelism). +/// 3. on battery → base /= 2. +/// 4. gpu_offloaded → base = min(base, gpu_floor(workload)). +/// 5. clamp to [MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS]. +pub fn inference_thread_count(_workload: Workload, _gpu_offloaded: bool) -> usize { + if let Ok(s) = std::env::var("MAGNOTIA_INFERENCE_THREADS") { + if let Ok(n) = s.parse::() { + if n > 0 { + return n; + } + } + } + let physical = num_cpus::get_physical(); + let chosen = if physical > 0 { + physical + } else { + std::thread::available_parallelism() + .map(|p| p.get()) + .unwrap_or(MIN_INFERENCE_THREADS) + }; + chosen.clamp(MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matches_existing_clamp_when_no_clamps_apply() { + // With no env override, no battery, no gpu_offload, helper + // should return physical-core count clamped to [2, 8]. + // We can't pin physical exactly without mocking num_cpus; just + // assert the result is in range. + std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + let n = inference_thread_count(Workload::Llm, false); + assert!(n >= MIN_INFERENCE_THREADS && n <= MAX_INFERENCE_THREADS, + "got {n}, expected within [{MIN_INFERENCE_THREADS}, {MAX_INFERENCE_THREADS}]"); + } + + #[test] + fn env_var_bypasses_clamps() { + std::env::set_var("MAGNOTIA_INFERENCE_THREADS", "10"); + let n = inference_thread_count(Workload::Llm, true); + assert_eq!(n, 10); + std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + } + + #[test] + fn workload_variants_distinct() { + assert_ne!(Workload::Llm, Workload::Whisper); + } +} +``` + +Add to `crates/core/src/lib.rs`: + +```rust +pub mod tuning; +``` + +(Place after `pub mod power;` alphabetically or wherever fits the existing ordering.) + +- [ ] **Step 3: Run the test** + +Run: `cargo test -p magnotia-core --lib tuning::tests` +Expected: 3 tests pass. + +- [ ] **Step 4: Commit** + +```bash +git add crates/core/Cargo.toml crates/core/src/tuning.rs crates/core/src/lib.rs +git commit -m "feat(core): tuning module with Workload + helper skeleton" +``` + +--- + +### Task 2.2: Battery clamp branch + +**Files:** +- Modify: `crates/core/src/tuning.rs` + +- [ ] **Step 1: Write the failing test** + +Add to `mod tests` in `crates/core/src/tuning.rs`: + +```rust + #[test] + fn battery_halves_thread_count() { + std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + // Lock both the test override mutex and assert. + crate::power::with_override(Some(PowerState::OnBattery), || { + let on_battery = inference_thread_count(Workload::Llm, false); + crate::power::with_override(Some(PowerState::OnAc), || { + let on_ac = inference_thread_count(Workload::Llm, false); + // on_battery should be roughly half of on_ac, clamped + // to MIN. + if on_ac > MIN_INFERENCE_THREADS { + assert!( + on_battery <= on_ac, + "battery ({on_battery}) should be <= AC ({on_ac})" + ); + assert!(on_battery >= MIN_INFERENCE_THREADS); + } + }); + }); + } +``` + +(`with_override` is private to `power`'s tests by default; expose it for cross-module unit tests by changing `#[cfg(test)] pub(crate) fn with_override` to `#[cfg(test)] pub fn with_override` so `tuning::tests` can call it. Or keep it `pub(crate)` since `tuning` is in the same crate — `pub(crate)` is sufficient.) + +Verify in `power.rs`: the existing `pub(crate)` on `with_override`, `force_clear_cache`, `force_set_cache` is correct for cross-module access. No change needed. + +But wait — these test helpers are gated `#[cfg(test)]` so they're only available when compiling for tests. From `tuning::tests` they ARE compiled for tests, so `pub(crate)` works. Good. + +- [ ] **Step 2: Run, expect failure** + +Run: `cargo test -p magnotia-core --lib tuning::tests::battery_halves_thread_count` +Expected: FAIL — assertion that on_battery <= on_ac fails because helper currently ignores power state. (On a 1c host, on_ac and on_battery would both be 2 and the test would no-op via the early `if on_ac > MIN_INFERENCE_THREADS` check; in that environment the test isn't useful. CI hosts with >2 physical cores will exercise it.) + +- [ ] **Step 3: Implement battery clamp** + +Modify `inference_thread_count` in `crates/core/src/tuning.rs`. Replace the body with: + +```rust +pub fn inference_thread_count(workload: Workload, gpu_offloaded: bool) -> usize { + if let Ok(s) = std::env::var("MAGNOTIA_INFERENCE_THREADS") { + if let Ok(n) = s.parse::() { + if n > 0 { + return n; + } + } + } + let physical = num_cpus::get_physical(); + let mut chosen = if physical > 0 { + physical + } else { + std::thread::available_parallelism() + .map(|p| p.get()) + .unwrap_or(MIN_INFERENCE_THREADS) + }; + if power::probe_power_state() == PowerState::OnBattery { + chosen /= 2; + } + let _ = (workload, gpu_offloaded); // GPU clamp added in next task + chosen.clamp(MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS) +} +``` + +(The `let _ = (workload, gpu_offloaded);` silences unused-arg warnings until task 2.3 wires them in.) + +- [ ] **Step 4: Run, expect pass** + +Run: `cargo test -p magnotia-core --lib tuning::tests` +Expected: 4 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add crates/core/src/tuning.rs +git commit -m "feat(core): inference_thread_count halves on battery" +``` + +--- + +### Task 2.3: GPU-offload clamp with workload-specific floor + +**Files:** +- Modify: `crates/core/src/tuning.rs` + +- [ ] **Step 1: Write the failing tests** + +Add to `mod tests` in `crates/core/src/tuning.rs`: + +```rust + #[test] + fn gpu_offload_clamps_llm_to_floor() { + std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + crate::power::with_override(Some(PowerState::OnAc), || { + let n = inference_thread_count(Workload::Llm, true); + assert!(n <= GPU_FLOOR_LLM.max(MIN_INFERENCE_THREADS), + "Llm with GPU offload should clamp to {GPU_FLOOR_LLM}, got {n}"); + assert!(n >= MIN_INFERENCE_THREADS); + }); + } + + #[test] + fn gpu_offload_clamps_whisper_to_floor() { + std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + crate::power::with_override(Some(PowerState::OnAc), || { + let n = inference_thread_count(Workload::Whisper, true); + // Whisper floor is 4 on machines with >=4 physical cores; + // can't go lower than physical so on a 2c host we stay at 2. + assert!(n <= GPU_FLOOR_WHISPER, "got {n}"); + assert!(n >= MIN_INFERENCE_THREADS); + }); + } + + #[test] + fn whisper_gpu_floor_is_at_least_llm_gpu_floor() { + // Architectural invariant: whisper retains CPU work even when + // GPU-offloaded; its floor must not be lower than the LLM's. + assert!(GPU_FLOOR_WHISPER >= GPU_FLOOR_LLM); + } + + #[test] + fn gpu_offload_off_does_not_clamp_below_battery_calc() { + std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + crate::power::with_override(Some(PowerState::OnAc), || { + let no_gpu = inference_thread_count(Workload::Llm, false); + let with_gpu = inference_thread_count(Workload::Llm, true); + // GPU-offloaded should be <= non-offloaded. + assert!(with_gpu <= no_gpu); + }); + } +``` + +- [ ] **Step 2: Run, expect failure** + +Run: `cargo test -p magnotia-core --lib tuning::tests` +Expected: GPU-offload tests fail because the helper still ignores `gpu_offloaded`. + +- [ ] **Step 3: Implement GPU-offload clamp** + +Modify `inference_thread_count` in `crates/core/src/tuning.rs`: + +```rust +pub fn inference_thread_count(workload: Workload, gpu_offloaded: bool) -> usize { + if let Ok(s) = std::env::var("MAGNOTIA_INFERENCE_THREADS") { + if let Ok(n) = s.parse::() { + if n > 0 { + return n; + } + } + } + let physical = num_cpus::get_physical(); + let mut chosen = if physical > 0 { + physical + } else { + std::thread::available_parallelism() + .map(|p| p.get()) + .unwrap_or(MIN_INFERENCE_THREADS) + }; + if power::probe_power_state() == PowerState::OnBattery { + chosen /= 2; + } + if gpu_offloaded { + let floor = match workload { + Workload::Llm => GPU_FLOOR_LLM, + Workload::Whisper => GPU_FLOOR_WHISPER, + }; + chosen = chosen.min(floor); + } + chosen.clamp(MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS) +} +``` + +- [ ] **Step 4: Run, expect pass** + +Run: `cargo test -p magnotia-core --lib tuning::tests` +Expected: 8 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add crates/core/src/tuning.rs +git commit -m "feat(core): GPU-offload clamp with per-workload floor" +``` + +--- + +### Task 2.4: Per-process logging cache + +**Files:** +- Modify: `crates/core/src/tuning.rs` + +- [ ] **Step 1: Write the test (manual verification, no assertion logic)** + +Add to `mod tests` in `crates/core/src/tuning.rs`: + +```rust + #[test] + fn logging_does_not_panic() { + // Smoke: helper should run without panicking when logging is + // wired. This is covered by the other tests too, but kept + // explicitly to document the behaviour. + std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + crate::power::with_override(Some(PowerState::OnBattery), || { + let _ = inference_thread_count(Workload::Llm, true); + let _ = inference_thread_count(Workload::Whisper, false); + }); + } +``` + +- [ ] **Step 2: Run, expect pass (already passes, the test is a smoke)** + +Run: `cargo test -p magnotia-core --lib tuning::tests::logging_does_not_panic` +Expected: PASS. + +- [ ] **Step 3: Implement per-process log cache** + +Add to `crates/core/src/tuning.rs`: + +```rust +use std::collections::HashSet; +use std::sync::{Mutex, OnceLock}; + +/// Whether a given (workload, on_battery, gpu_offloaded) tuple has +/// already been logged this process. Prevents log spam when the same +/// configuration is exercised on every inference call. +fn log_seen() -> &'static Mutex> { + static SEEN: OnceLock>> = OnceLock::new(); + SEEN.get_or_init(|| Mutex::new(HashSet::new())) +} +``` + +Update `inference_thread_count` to log once per tuple: + +```rust +pub fn inference_thread_count(workload: Workload, gpu_offloaded: bool) -> usize { + if let Ok(s) = std::env::var("MAGNOTIA_INFERENCE_THREADS") { + if let Ok(n) = s.parse::() { + if n > 0 { + return n; + } + } + } + let physical = num_cpus::get_physical(); + let mut chosen = if physical > 0 { + physical + } else { + std::thread::available_parallelism() + .map(|p| p.get()) + .unwrap_or(MIN_INFERENCE_THREADS) + }; + let on_battery = power::probe_power_state() == PowerState::OnBattery; + let mut clamps: Vec<&'static str> = Vec::new(); + if on_battery { + chosen /= 2; + clamps.push("battery"); + } + if gpu_offloaded { + let floor = match workload { + Workload::Llm => GPU_FLOOR_LLM, + Workload::Whisper => GPU_FLOOR_WHISPER, + }; + if chosen > floor { + chosen = floor; + clamps.push("gpu"); + } + } + let final_value = chosen.clamp(MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS); + + // Log once per (workload, on_battery, gpu_offloaded) tuple. + let key = (workload, on_battery, gpu_offloaded); + if let Ok(mut seen) = log_seen().lock() { + if seen.insert(key) { + tracing::info!( + target: "magnotia_core::tuning", + threads = final_value, + workload = ?workload, + gpu_offloaded, + on_battery, + clamps = ?clamps, + "inference_thread_count" + ); + } + } + + final_value +} +``` + +Note `Workload` needs to be `Hash + Eq` for the `HashSet`. Add the derives: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Workload { + Llm, + Whisper, +} +``` + +- [ ] **Step 4: Run all tests** + +Run: `cargo test -p magnotia-core --lib` +Expected: all power + tuning tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add crates/core/src/tuning.rs +git commit -m "feat(core): per-process INFO log on first thread-count call per tuple" +``` + +--- + +## Phase 3 — Move `vulkan_loader_available` to core + +### Task 3.1: Port function to `magnotia-core::hardware` + +**Files:** +- Modify: `crates/core/Cargo.toml` (add `libloading = "0.8"`) +- Modify: `crates/core/src/hardware.rs` + +- [ ] **Step 1: Add libloading dep to core** + +Edit `crates/core/Cargo.toml`, append in `[dependencies]`: + +```toml +libloading = "0.8" +``` + +- [ ] **Step 2: Write a test that asserts the function is callable** + +Add to `crates/core/src/hardware.rs` inside the existing `#[cfg(test)] mod tests` block (after the existing tests): + +```rust + #[test] + fn vulkan_loader_available_does_not_panic() { + // We can't assert the value (depends on host's libvulkan), + // but we can assert the call completes. + let _ = vulkan_loader_available(); + } +``` + +- [ ] **Step 3: Run, expect compile failure** + +Run: `cargo test -p magnotia-core --lib hardware::tests::vulkan_loader_available_does_not_panic` +Expected: compile error — function not defined. + +- [ ] **Step 4: Add `vulkan_loader_available` to hardware.rs** + +Append to `crates/core/src/hardware.rs`: + +```rust +/// Best-effort probe for the Vulkan loader shared library. +/// +/// whisper.cpp and llama.cpp Vulkan backends silently drop to CPU if +/// `libvulkan.so.1` (Linux) / `vulkan-1.dll` (Windows) / the MoltenVK +/// bundle (macOS) is missing at runtime. We probe via +/// `libloading::Library::new`; a successful open means the loader is +/// resolvable and we should treat the GPU path as live. +/// +/// Moved from `src-tauri/src/commands/models.rs` so non-Tauri crates +/// (transcription, llm) can call it without depending on the Tauri +/// binary. +pub fn vulkan_loader_available() -> bool { + #[cfg(target_os = "linux")] + let candidates: &[&str] = &["libvulkan.so.1", "libvulkan.so"]; + #[cfg(target_os = "windows")] + let candidates: &[&str] = &["vulkan-1.dll"]; + #[cfg(target_os = "macos")] + let candidates: &[&str] = &["libvulkan.1.dylib", "libMoltenVK.dylib"]; + #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] + let candidates: &[&str] = &[]; + + for name in candidates { + // SAFETY: libloading::Library::new loads a shared library and + // returns a handle that is dropped at the end of this + // iteration. We do not call any symbols, so the open-for-probe + // pattern is sound. + match unsafe { libloading::Library::new(*name) } { + Ok(_lib) => return true, + Err(_) => continue, + } + } + false +} +``` + +- [ ] **Step 5: Run, expect pass** + +Run: `cargo test -p magnotia-core --lib hardware::tests::vulkan_loader_available_does_not_panic` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add crates/core/Cargo.toml crates/core/src/hardware.rs +git commit -m "feat(core): port vulkan_loader_available from src-tauri" +``` + +--- + +### Task 3.2: Update src-tauri caller + +**Files:** +- Modify: `src-tauri/src/commands/models.rs` + +- [ ] **Step 1: Replace local definition with re-export** + +Find the local `fn vulkan_loader_available()` in `src-tauri/src/commands/models.rs` (lines around 322–350 of the file as of the spec date). Delete the function entirely. Add at the top of the file (or near other `magnotia_core` imports): + +```rust +use magnotia_core::hardware::vulkan_loader_available; +``` + +The two existing call sites in `models.rs` (around lines 401 and 419) will now resolve to the imported function. Verify with grep that no other callers exist inside src-tauri: + +```bash +grep -n "vulkan_loader_available" src-tauri/src/ +``` + +Expected output: only the `use` line and the two existing call sites. + +- [ ] **Step 2: Build and run src-tauri's tests** + +Run: `cargo test -p magnotia` (the src-tauri crate name as listed in `src-tauri/Cargo.toml`; verify if unsure with `grep '^name' src-tauri/Cargo.toml`). +Expected: PASS. + +- [ ] **Step 3: Run the existing accelerator-list smoke** + +Run: `cargo test -p magnotia compose_accelerators` +Expected: PASS — confirms the move didn't change behaviour. + +- [ ] **Step 4: Commit** + +```bash +git add src-tauri/src/commands/models.rs +git commit -m "refactor(tauri): use magnotia_core::hardware::vulkan_loader_available" +``` + +--- + +## Phase 4 — Whisper call site + +### Task 4.1: Wire whisper backend through new helper + +**Files:** +- Modify: `crates/transcription/src/whisper_rs_backend.rs` + +- [ ] **Step 1: Replace the call site** + +Find the existing line (around line 81): + +```rust +params.set_n_threads(inference_thread_count() as i32); +``` + +…and the import at the top: + +```rust +use magnotia_core::constants::inference_thread_count; +``` + +Replace the import: + +```rust +use magnotia_core::tuning::{inference_thread_count, Workload}; +use magnotia_core::hardware::vulkan_loader_available; +``` + +Replace the call site: + +```rust +let gpu_offloaded = cfg!(feature = "whisper-vulkan") && vulkan_loader_available(); +params.set_n_threads(inference_thread_count(Workload::Whisper, gpu_offloaded) as i32); +``` + +- [ ] **Step 2: Build and run the existing whisper smoke** + +Run: `cargo test -p magnotia-transcription whisper_rs_smoke` +Expected: PASS — the helper returns a sensible value (likely 4 or 6 depending on host). + +- [ ] **Step 3: Commit** + +```bash +git add crates/transcription/src/whisper_rs_backend.rs +git commit -m "feat(transcription): whisper threads use Workload::Whisper + GPU detection" +``` + +--- + +## Phase 5 — LLM call site + +### Task 5.1: Wire LLM call site through new helper + +**Files:** +- Modify: `crates/llm/src/lib.rs` + +- [ ] **Step 1: Locate the current thread-count computation** + +The existing code in `crates/llm/src/lib.rs` (around line 174 in `load_model`) reads: + +```rust +let gpu_layers = if use_gpu { u32::MAX } else { 0 }; +let params = LlamaModelParams::default().with_n_gpu_layers(gpu_layers); +// ... LlamaModel::load_from_file_with_params(...) returns `model` ... + +let thread_count = i32::try_from(magnotia_core::constants::inference_thread_count()) + .unwrap_or(4); +let ctx_params = LlamaContextParams::default() + .with_n_ctx(Some(NonZeroU32::new(n_ctx).expect("n_ctx must be non-zero"))) + .with_n_threads(thread_count) + .with_n_threads_batch(thread_count); +``` + +The model has been loaded by this point (the `model` binding above this block returns from `LlamaModel::load_from_file_with_params`), so `model.n_layer()` is callable. + +- [ ] **Step 2: Replace the import + computation** + +Replace the existing `use magnotia_core::constants::inference_thread_count;` (or wherever `constants::inference_thread_count` is referenced) with: + +```rust +use magnotia_core::tuning::{inference_thread_count, Workload}; +``` + +Replace the thread-count computation block. The new shape uses the `gpu_layers` value already computed above and cross-checks against the loaded model's layer count: + +```rust +let gpu_offloaded = use_gpu && gpu_layers >= model.n_layer(); +let thread_count = i32::try_from(inference_thread_count(Workload::Llm, gpu_offloaded)) + .unwrap_or(4); +``` + +The cross-check is trivially true today (`gpu_layers = u32::MAX` when `use_gpu`), but the explicit comparison reads as a future-proofed assertion if we ever pass a specific N instead of `u32::MAX`. + +- [ ] **Step 3: Build and run LLM smoke** + +Run: `cargo test -p magnotia-llm` +Expected: PASS (smoke tests in `crates/llm/tests/` exercise the load path; if any fail unrelated to this change, investigate before moving on). + +- [ ] **Step 4: Commit** + +```bash +git add crates/llm/src/lib.rs +git commit -m "feat(llm): LLM threads use Workload::Llm + GPU offload detection" +``` + +--- + +## Phase 6 — Remove old facade + +### Task 6.1: Delete `inference_thread_count` from `constants.rs` + +**Files:** +- Modify: `crates/core/src/constants.rs` + +- [ ] **Step 1: Confirm zero remaining callers** + +Run: + +```bash +cd /home/jake/Documents/CORBEL-Projects/magnotia +grep -rn "constants::inference_thread_count\|MIN_INFERENCE_THREADS\|MAX_INFERENCE_THREADS" \ + --include='*.rs' \ + crates/ src-tauri/ +``` + +Expected output: only references inside `crates/core/src/tuning.rs` (the new home of MIN/MAX) and the test file `crates/transcription/tests/thread_sweep.rs` (touched in phase 7 next; OK to leave for now). + +If any other callers turn up, update them to call `tuning::inference_thread_count(workload, gpu_offloaded)` first and re-run the grep. + +- [ ] **Step 2: Delete the old definitions** + +Edit `crates/core/src/constants.rs`. Delete: + +- The `pub const MIN_INFERENCE_THREADS` block. +- The `pub const MAX_INFERENCE_THREADS` block. +- The entire `pub fn inference_thread_count() -> usize { ... }` function and its docstring. + +The remaining file should keep audio/RAM/VAD/download constants intact. + +- [ ] **Step 3: Build the workspace** + +Run: `cargo build --workspace` +Expected: clean build, no warnings about unused items. + +- [ ] **Step 4: Run the full test suite** + +Run: `cargo test --workspace` +Expected: PASS for `magnotia-core`, `magnotia-llm`, `magnotia-transcription`, `magnotia` (src-tauri). Other crate tests should be unaffected. + +- [ ] **Step 5: Commit** + +```bash +git add crates/core/src/constants.rs +git commit -m "refactor(core): remove old inference_thread_count facade" +``` + +--- + +## Phase 7 — Extend `thread_sweep.rs` + +### Task 7.1: Four-panel power-aware sweep + +**Files:** +- Modify: `crates/transcription/tests/thread_sweep.rs` + +- [ ] **Step 1: Refactor existing sweep into a helper** + +Open `crates/transcription/tests/thread_sweep.rs` and locate the existing single-panel sweep (the test that prints `=== n_threads scaling ===` followed by the table). Wrap the panel-printing logic into a helper: + +```rust +fn run_sweep_panel( + label: &str, + model_path: &Path, + audio: &[f32], + n_threads_values: &[i32], +) -> std::io::Result<()> { + eprintln!("=== n_threads scaling: {label} ==="); + eprintln!("n_threads | xc_time | RTF | speedup_vs_1"); + eprintln!("----------|---------|--------|-------------"); + // ... existing per-n_threads loop, copied verbatim ... + Ok(()) +} +``` + +(Copy the existing loop body inside; only the header strings change.) + +- [ ] **Step 2: Drive four panels from the test entrypoint** + +Replace the existing test body: + +```rust +#[test] +fn thread_count_scaling() -> std::io::Result<()> { + if std::env::var("MAGNOTIA_THREAD_SWEEP").is_err() { + return Ok(()); // env-gated, opt-in + } + let (model_path, audio) = load_fixture()?; + let n_threads_values = &[1, 2, 4, 6, 8, 12]; + + for (label, power_var) in &[ + ("AC, CPU", "ac"), + ("battery, CPU", "battery"), + ("AC, GPU (Vulkan if available)", "ac"), + ("battery, GPU (Vulkan if available)", "battery"), + ] { + std::env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", power_var); + run_sweep_panel(label, &model_path, &audio, n_threads_values)?; + } + std::env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE"); + Ok(()) +} +``` + +(Leave the existing `load_fixture` helper intact — same fixture loading as before.) + +For the GPU panels: the fixture sweep doesn't reload the whisper context with the Vulkan backend per panel; `whisper-rs` initialises Vulkan if the feature is on at compile time and the loader resolves at runtime. The panel labels reflect intent — actual GPU residency is what the runtime delivers. The point of these panels is empirical RTF tables to inform whether the GPU floor of 4 is right for whisper. + +- [ ] **Step 3: Run the sweep manually on a host with libvulkan1 + battery** + +Run: + +```bash +cd /home/jake/Documents/CORBEL-Projects/magnotia +MAGNOTIA_THREAD_SWEEP=1 cargo test -p magnotia-transcription --release thread_count_scaling -- --nocapture +``` + +Expected: 4 RTF panels print. Note the per-panel sweet spots — they inform whether the constants in `tuning.rs` need adjusting. + +- [ ] **Step 4: Commit** + +```bash +git add crates/transcription/tests/thread_sweep.rs +git commit -m "test(transcription): thread_sweep prints power-aware 4-panel table" +``` + +--- + +## Phase 8 — Acceptance smoke + +### Task 8.1: Manual battery validation + +This is a manual step; no commit unless the smoke uncovers a defect that needs fixing. + +- [ ] **Step 1: Build a release binary** + +Run: + +```bash +cd /home/jake/Documents/CORBEL-Projects/magnotia +cargo build --release -p magnotia-transcription -p magnotia-llm +``` + +- [ ] **Step 2: Unplug the laptop, run a transcription session** + +Open Lumenote, dictate a short clip, watch the log output (Tauri devtools or stdout if running via `cargo tauri dev`). Expect a line of the shape: + +``` +INFO magnotia_core::tuning: inference_thread_count threads=3 workload=Whisper gpu_offloaded=false on_battery=true clamps=["battery"] +``` + +(Numbers vary by host. On a 6c12t Ryzen 4650U the expected `threads` is 3.) + +- [ ] **Step 3: Plug back in, run another transcription** + +Expect a separate INFO line with `on_battery=false clamps=[]` (no clamps fired) and `threads=6` on the same host. + +- [ ] **Step 4: If the LLM has a model loaded, exercise it (e.g. extract tasks from a transcript)** + +Expect a third INFO line with `workload=Llm`, `gpu_offloaded` reflecting actual GPU presence, and a thread count consistent with the truth table in the spec. + +- [ ] **Step 5: Watch CPU usage with `htop` during inference** + +Expected: +- AC + CPU LLM: ~6 cores busy on the 4650U. +- Battery + CPU LLM: ~3 cores busy. +- AC + GPU LLM (Vulkan available): ~2 cores busy, GPU briefly spiking. +- Battery + GPU LLM: ~2 cores busy. + +Roughly halved CPU occupancy on battery validates the heuristic in real conditions. + +If CPU usage doesn't change between AC and battery, the probe isn't firing — read `/sys/class/power_supply/AC*/online` directly to debug. + +--- + +## Self-Review (already run) + +- **Spec coverage**: every section of the spec maps to at least one task. Architecture → Phase 1+2+3. PowerState → Phase 1. Workload + helper → Phase 2. Vulkan-loader move → Phase 3. GPU-offload detection (LLM + Whisper) → Phase 4 + Phase 5. Override env var → Task 1.3, Task 2.1. Tests → Phases 1–2 unit tests, Phase 7 integration. Logging → Task 2.4. Acceptance → Phase 8. + +- **Placeholders**: none. Every step has concrete code or commands. + +- **Type consistency**: `Workload` derives `Hash + Eq` (added in Task 2.4 specifically for the log set). `PowerState` derives `Copy + PartialEq + Eq` for cache + override use. `with_override`, `force_clear_cache`, `force_set_cache` are `pub(crate)` so cross-module unit tests can call them. `vulkan_loader_available` returns `bool` everywhere. + +- **Known wart**: `tuning::tests::battery_halves_thread_count` and other power-override-using tests share a global mutex. They're serialised by the inner `TEST_LOCK` inside `with_override`. If cargo's parallel runner exposes a flake, run those tests with `--test-threads=1` for the affected module.