--- name: Core inference thread tuning type: architecture-map-page slice: 05-core-storage-hotkey-build last_verified: 2026/05/09 --- # Core inference thread tuning > **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core inference thread tuning **Plain English summary.** A single helper, `inference_thread_count`, that answers "how many CPU threads should we give whisper.cpp / llama.cpp on this user's machine right now?" Combines the physical-core budget, battery awareness, and GPU-offload awareness into one number. Logs the chosen value once per (workload, battery, GPU-offload) tuple so the same configuration does not flood the journal. ## At a glance - File: `crates/core/src/tuning.rs` (233 LOC, 127 of which are tests). - External deps: `num_cpus 1`, `tracing 0.1`, plus the in-crate `power` module. - Public surface: `MIN_INFERENCE_THREADS`, `MAX_INFERENCE_THREADS`, `Workload` enum, `inference_thread_count`. - Consumers: slice 3 (Whisper engine, Parakeet engine — `Workload::Whisper`); slice 4 (LLM engine — `Workload::Llm`). ## What's in here ### Constants — `crates/core/src/tuning.rs:11, 20` ```rust pub const MIN_INFERENCE_THREADS: usize = 2; pub const MAX_INFERENCE_THREADS: usize = 8; ``` Single-threaded inference is measurably worse than two on every multi-core part. Past 8 threads, `whisper.cpp` and `llama.cpp` scaling flattens because SMT siblings contend for the FPU during F16 / F32 matmul. 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. ### `Workload` enum — `crates/core/src/tuning.rs:23` ```rust pub enum Workload { Llm, Whisper } ``` The two workloads have different floors when GPU offload is on. ### Internal floors — `crates/core/src/tuning.rs:33-34` ```rust const GPU_FLOOR_LLM: usize = 2; const GPU_FLOOR_WHISPER: usize = 4; ``` Whisper retains CPU work even with full Vulkan offload (mel spectrogram, decoder bookkeeping, beam search). LLM (llama-style transformer) drops to near-zero CPU work when fully offloaded. Architectural invariant tested at `tuning.rs:200`: `GPU_FLOOR_WHISPER >= GPU_FLOOR_LLM`. ### `inference_thread_count(workload, gpu_offloaded) -> usize` — `crates/core/src/tuning.rs:53` Resolution order: 1. **`MAGNOTIA_INFERENCE_THREADS=N`** — absolute bypass, returns `N` without any clamps. 2. **Base** = `num_cpus::get_physical()`. Falls back to `std::thread::available_parallelism()`, then to `MIN_INFERENCE_THREADS = 2`. 3. **On battery** → `base /= 2`. Power state is read from `lumotia_core::power::probe_power_state()`. The 10-second cache there means the call is cheap; see [`core-power.md`](core-power.md). 4. **GPU offloaded** → clamp to `GPU_FLOOR_`. Whisper floor is 4, LLM floor is 2. 5. **Final clamp** to `[MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS]`. ### One-shot logging — `crates/core/src/tuning.rs:39, 87-101` A `static SEEN: OnceLock>>` records every distinct `(workload, on_battery, gpu_offloaded)` tuple the helper has been called with this process. The first time a tuple is seen, the helper logs at `INFO` via `tracing!` with the chosen thread count and the active clamps. Subsequent calls with the same tuple are silent. Without this guard the journal would accumulate one INFO per inference chunk. ## Data flow / contract - Pure read; no I/O beyond reading the env var (cheap) and probing power (cached 10 s). - Idempotent within a tuple. Different tuples each get one INFO line. - The env override is intentional: it lets users on unusual silicon (32-core Threadripper with a noisy thermal design, big.LITTLE asymmetric cores, etc) override the heuristic. ## Tests 8 tests in `crates/core/src/tuning.rs:106-232`. The pattern is to use `with_thread_env_lock` (a static `Mutex<()>` to serialise env-var writes) plus `power::with_override` to drive the power probe. - `matches_existing_clamp_when_no_clamps_apply` — bare call returns within `[2, 8]`. - `env_var_bypasses_clamps` — `MAGNOTIA_INFERENCE_THREADS=10` returns 10 even with battery + GPU clamps active. - `workload_variants_distinct` — sanity. - `battery_halves_thread_count` — measures both states; battery should be ≤ AC. - `gpu_offload_clamps_llm_to_floor` / `gpu_offload_clamps_whisper_to_floor` — GPU clamp. - `whisper_gpu_floor_is_at_least_llm_gpu_floor` — architectural invariant. - `gpu_offload_off_does_not_clamp_below_battery_calc` — GPU clamp does not undo the battery halve when GPU is off. - `logging_does_not_panic` — smoke for the tracing path. ## Watch-outs - **Two static `Mutex`es: `SEEN` and `TEST_LOCK` (in tests).** Holding either across a panic poisons it. The helper uses `if let Ok(mut seen) = log_seen().lock()` rather than `.expect()` so a poisoned `SEEN` mutex degrades silently to "log every call" rather than panicking. - **`MAGNOTIA_INFERENCE_THREADS` is a process-global env-var.** Per-call overrides need to wrap the call site in a temporary env-var, which is racy across threads. Tests serialise via `THREAD_ENV_LOCK`. - **Battery probe is a cross-process state.** A user toggling the dock between calls sees the change after the 10-second power cache TTL. Acceptable for thread tuning; consumers wanting near-real-time should call `force_clear_cache` (test-only) or accept the 10 s lag. - **`probe_gpu()` returning `None` does not affect this helper.** The `gpu_offloaded` flag is passed in by the caller (the LLM engine sets it from `use_gpu && gpu_layers >= n_layer()`). See the slice-4 LLM page for the call site. ## See also - [Power-state probe](core-power.md) - [Constants module](core-constants.md) (RAM thresholds) - [Slice 3 transcription engines](../03-audio-transcription/README.md) - [Slice 4 LLM engine](../04-llm-formatting-mcp/README.md) - `docs/superpowers/specs/2026-05-09-battery-gpu-aware-thread-tuning-design.md` for the full design rationale.