feat(core): inference_thread_count halves on battery

Adds battery-state awareness to the thread-count helper: when
probe_power_state() returns OnBattery, chosen is halved before the
[MIN, MAX] clamp.  Also removes the leading underscore from the
workload/gpu_offloaded parameters (they are silenced via let _ = ...
until the GPU-clamp task).

New test battery_halves_thread_count verifies on_battery <= on_ac and
>= MIN when the host has more than MIN physical cores.  Restructured
to sequential with_override calls (not nested) to avoid re-entrant
deadlock on power::TEST_LOCK.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jars
2026-05-09 11:49:32 +01:00
parent 02a6cb24ce
commit 847dc755ac

View File

@@ -40,7 +40,7 @@ const GPU_FLOOR_WHISPER: usize = 4;
/// 3. on battery → base /= 2. /// 3. on battery → base /= 2.
/// 4. gpu_offloaded → base = min(base, gpu_floor(workload)). /// 4. gpu_offloaded → base = min(base, gpu_floor(workload)).
/// 5. clamp to [MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS]. /// 5. clamp to [MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS].
pub fn inference_thread_count(_workload: Workload, _gpu_offloaded: bool) -> usize { pub fn inference_thread_count(workload: Workload, gpu_offloaded: bool) -> usize {
if let Ok(s) = std::env::var("MAGNOTIA_INFERENCE_THREADS") { if let Ok(s) = std::env::var("MAGNOTIA_INFERENCE_THREADS") {
if let Ok(n) = s.parse::<usize>() { if let Ok(n) = s.parse::<usize>() {
if n > 0 { if n > 0 {
@@ -49,13 +49,17 @@ pub fn inference_thread_count(_workload: Workload, _gpu_offloaded: bool) -> usiz
} }
} }
let physical = num_cpus::get_physical(); let physical = num_cpus::get_physical();
let chosen = if physical > 0 { let mut chosen = if physical > 0 {
physical physical
} else { } else {
std::thread::available_parallelism() std::thread::available_parallelism()
.map(|p| p.get()) .map(|p| p.get())
.unwrap_or(MIN_INFERENCE_THREADS) .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) chosen.clamp(MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS)
} }
@@ -101,4 +105,27 @@ mod tests {
fn workload_variants_distinct() { fn workload_variants_distinct() {
assert_ne!(Workload::Llm, Workload::Whisper); assert_ne!(Workload::Llm, Workload::Whisper);
} }
#[test]
fn battery_halves_thread_count() {
with_thread_env_lock(|| {
std::env::remove_var("MAGNOTIA_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);
}
});
}
} }