Phase 2 QC found three explicit blockers + a broader sweep needed:
Explicit (QC-named):
- crates/mcp/src/lib.rs:15 — SERVER_NAME public MCP wire identity
- crates/transcription/build.rs:59 — panic message prefix
- crates/llm/tests/content_tags_smoke.rs:7 — docstring -p flag
Swept (string literals + dev env vars + doc comments + test fixtures):
- crates/mcp/src/main.rs — eprintln log prefixes
- src-tauri/src/commands/diagnostics.rs — diagnostic filename + MAGNOTIA_VERSION
- src-tauri/src/commands/audio.rs — recording filename pattern lumotia-<secs>-...wav
- src-tauri/src/commands/fs.rs — test placeholder path
- crates/transcription/src/model_manager.rs — .lumotia-verified marker
- crates/storage/src/database.rs — lumotia-storage-ro-<pid> temp dirs + doc comments
- crates/cloud-providers/src/keystore.rs — LUMOTIA_API_KEY_<PROVIDER> env var
- crates/audio/src/{wav,decode}.rs — lumotia_test_* / lumotia_decode_* test fixtures
- crates/core/src/tuning.rs — LUMOTIA_INFERENCE_THREADS env var
- All MAGNOTIA_LLM_TEST_MODEL / MAGNOTIA_WHISPER_TEST_* env vars
- Doc comments referencing crate names
Excluded (intentional Phase 4/5 scope):
- magnotia_preferences, magnotia_history, magnotia_morning_triage_last_shown
DB setting keys (Phase 5 paths.rs migration)
- magnotia_startup tracing target (Phase 4)
- crates/core/src/paths.rs (Phase 5 wholesale rewrite + migration shim)
cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
241 lines
8.9 KiB
Rust
241 lines
8.9 KiB
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};
|
|
use std::collections::HashSet;
|
|
use std::sync::{Mutex, OnceLock};
|
|
|
|
/// 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
|
|
/// LUMOTIA_INFERENCE_THREADS.
|
|
pub const MAX_INFERENCE_THREADS: usize = 8;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
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;
|
|
|
|
/// 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<HashSet<(Workload, bool, bool)>> {
|
|
static SEEN: OnceLock<Mutex<HashSet<(Workload, bool, bool)>>> = OnceLock::new();
|
|
SEEN.get_or_init(|| Mutex::new(HashSet::new()))
|
|
}
|
|
|
|
/// Inference thread count, clamped to the physical-core budget plus
|
|
/// the battery and GPU-offload heuristics.
|
|
///
|
|
/// Resolution order:
|
|
/// 1. `LUMOTIA_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("LUMOTIA_INFERENCE_THREADS") {
|
|
if let Ok(n) = s.parse::<usize>() {
|
|
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: "lumotia_core::tuning",
|
|
threads = final_value,
|
|
workload = ?workload,
|
|
gpu_offloaded,
|
|
on_battery,
|
|
clamps = ?clamps,
|
|
"inference_thread_count"
|
|
);
|
|
}
|
|
}
|
|
|
|
final_value
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Serialises tests that read/write `LUMOTIA_INFERENCE_THREADS` so
|
|
/// they don't race under cargo's parallel test runner.
|
|
/// Mirrors the pattern used by `power::with_override`.
|
|
fn with_thread_env_lock<R>(body: impl FnOnce() -> R) -> R {
|
|
use std::sync::Mutex;
|
|
static THREAD_ENV_LOCK: Mutex<()> = Mutex::new(());
|
|
let _lock = THREAD_ENV_LOCK
|
|
.lock()
|
|
.expect("tuning thread-env lock poisoned");
|
|
body()
|
|
}
|
|
|
|
#[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.
|
|
with_thread_env_lock(|| {
|
|
std::env::remove_var("LUMOTIA_INFERENCE_THREADS");
|
|
let n = inference_thread_count(Workload::Llm, false);
|
|
assert!(
|
|
(MIN_INFERENCE_THREADS..=MAX_INFERENCE_THREADS).contains(&n),
|
|
"got {n}, expected within [{MIN_INFERENCE_THREADS}, {MAX_INFERENCE_THREADS}]"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn env_var_bypasses_clamps() {
|
|
with_thread_env_lock(|| {
|
|
std::env::set_var("LUMOTIA_INFERENCE_THREADS", "10");
|
|
let n = inference_thread_count(Workload::Llm, true);
|
|
assert_eq!(n, 10);
|
|
std::env::remove_var("LUMOTIA_INFERENCE_THREADS");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn workload_variants_distinct() {
|
|
assert_ne!(Workload::Llm, Workload::Whisper);
|
|
}
|
|
|
|
#[test]
|
|
fn battery_halves_thread_count() {
|
|
with_thread_env_lock(|| {
|
|
std::env::remove_var("LUMOTIA_INFERENCE_THREADS");
|
|
// Measure on battery, then on AC — sequential, not nested,
|
|
// to avoid re-entrant deadlock on power::TEST_LOCK.
|
|
let on_battery = crate::power::with_override(Some(PowerState::OnBattery), || {
|
|
inference_thread_count(Workload::Llm, false)
|
|
});
|
|
let on_ac = crate::power::with_override(Some(PowerState::OnAc), || {
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn gpu_offload_clamps_llm_to_floor() {
|
|
with_thread_env_lock(|| {
|
|
std::env::remove_var("LUMOTIA_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() {
|
|
with_thread_env_lock(|| {
|
|
std::env::remove_var("LUMOTIA_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);
|
|
});
|
|
});
|
|
}
|
|
|
|
const _: () = assert!(GPU_FLOOR_WHISPER >= GPU_FLOOR_LLM);
|
|
|
|
#[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.
|
|
}
|
|
|
|
#[test]
|
|
fn gpu_offload_off_does_not_clamp_below_battery_calc() {
|
|
with_thread_env_lock(|| {
|
|
std::env::remove_var("LUMOTIA_INFERENCE_THREADS");
|
|
// Sequential measurements; with_override is non-reentrant.
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[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.
|
|
with_thread_env_lock(|| {
|
|
std::env::remove_var("LUMOTIA_INFERENCE_THREADS");
|
|
crate::power::with_override(Some(PowerState::OnBattery), || {
|
|
let _ = inference_thread_count(Workload::Llm, true);
|
|
let _ = inference_thread_count(Workload::Whisper, false);
|
|
});
|
|
});
|
|
}
|
|
}
|