Files
Lumotia/docs/architecture-map/05-core-storage-hotkey-build/core-tuning.md
Jake 26c7307607
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia-rebrand — docs, scripts, root config, residuals
Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.

transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
  -> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
  as immutable audit trail). Includes architecture-map references
  to magnotia_core::*, magnotia_storage::*, etc. now pointing at
  lumotia_*; dev-setup.md tracing output examples (lumotia_startup
  target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
  audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
  hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
  crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
  crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
  doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
  ("lumotia_task_sync"); magnotia_locale i18n localStorage key
  renamed + migration shim added; CSS keyframe names
  magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
  system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
  keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
  wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
  to "lumotia era" earlier — restored).

Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
  legacy detection strings in legacy_and_target_paths() so the
  migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
  — migration call sites reference the legacy magnotia keys
  deliberately.
- docs/handovers/ — historical audit trail.

cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:38:03 +01:00

5.9 KiB

name, type, slice, last_verified
name type slice last_verified
Core inference thread tuning architecture-map-page 05-core-storage-hotkey-build 2026/05/09

Core inference thread tuning

Where you are: Architecture mapCore, Storage, Hotkey, Build → 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

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

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

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) -> usizecrates/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 batterybase /= 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.
  4. GPU offloaded → clamp to GPU_FLOOR_<workload>. 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<Mutex<HashSet<(Workload, bool, bool)>>> 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_clampsMAGNOTIA_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 Mutexes: 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