perf: route whisper + llm n_threads through physical-core helper
Both inference call sites previously called `num_cpus::get()` (logical
thread count). Established whisper.cpp / llama.cpp guidance is that
SMT siblings contend for shared FPU resources during heavy F16/F32
matmul, so going past physical core count anti-scales. Empirical
sweep on Whisper Tiny / 11s JFK clip / Ryzen 5 4650U (6c12t):
n_threads | xc_time | RTF | speedup_vs_1
----------|---------|--------|-------------
1 | 0.33s | 0.030 | 1.00x
2 | 0.33s | 0.030 | 1.00x
4 | 0.30s | 0.028 | 1.09x
6 | 0.32s | 0.029 | 1.04x
8 | 0.31s | 0.028 | 1.06x
12 | 0.32s | 0.029 | 1.03x
Tiny doesn't scale (work dominated by overhead) but the larger
Whisper variants and Qwen LLMs do. Sources: whisper.cpp #200, #1033,
#1252, #403; llama.cpp #3167, #572.
Changes:
- crates/core/src/constants.rs:
* MIN_INFERENCE_THREADS lowered 4 → 2 (research-derived floor)
* MAX_INFERENCE_THREADS = 8 added (research-derived ceiling)
* inference_thread_count() rewritten:
- reads MAGNOTIA_INFERENCE_THREADS env var (override)
- num_cpus::get_physical() with available_parallelism fallback
- clamped to [MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS]
- crates/core/Cargo.toml: + num_cpus = "1"
- crates/transcription/src/whisper_rs_backend.rs: call site uses helper.
- crates/llm/src/lib.rs: call site uses helper.
- crates/transcription/Cargo.toml: drop num_cpus from [features] +
[dependencies] (production no longer needs it). Move to
[dev-dependencies] for tests/thread_sweep.rs only.
- crates/llm/Cargo.toml: drop num_cpus = "1" (no longer used directly).
Per-machine maps (after this patch):
Ryzen 5 4650U 6c12t → 6
big-iron 12c24t → 8 (clamp; users can override)
cheap 2c2t laptop → 2
1c container/VM → 2
Adds crates/transcription/tests/thread_sweep.rs — env-gated like
jfk_bench, prints the table above against any model + WAV. Useful for
re-baselining on new hardware or when tuning the clamp values.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,21 @@ pub const MIN_CHUNK_SAMPLES: usize = 8000;
|
||||
/// Post-processing thresholds.
|
||||
pub const SMART_PARAGRAPH_GAP_SECS: f64 = 2.0;
|
||||
|
||||
/// Thread count for inference. Leaves headroom for the UI thread.
|
||||
pub const MIN_INFERENCE_THREADS: usize = 4;
|
||||
/// Lower bound for inference threads. Single-threaded inference is
|
||||
/// measurably worse than two on every multi-core part; we never go below
|
||||
/// 2. (Whisper Tiny is the exception where the work is so small that
|
||||
/// thread count barely matters — but the floor still costs nothing.)
|
||||
pub const MIN_INFERENCE_THREADS: usize = 2;
|
||||
|
||||
/// Upper bound for inference threads. Both whisper.cpp and llama.cpp
|
||||
/// scaling flattens around physical core count; SMT siblings contend
|
||||
/// for shared FPU resources during heavy F16/F32 matmul, which means
|
||||
/// going past physical cores often anti-scales. Empirical evidence:
|
||||
/// whisper.cpp issue #200 (sweet spot at 7t on 8c/16t Ryzen 3700X),
|
||||
/// llama.cpp #3167 ("SMT hurts inference"). 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.
|
||||
pub const MAX_INFERENCE_THREADS: usize = 8;
|
||||
|
||||
/// History limits.
|
||||
pub const HISTORY_MAX_ENTRIES: usize = 100;
|
||||
@@ -40,10 +53,37 @@ pub const VAD_SPEECH_PAD_MS: u32 = 100;
|
||||
/// Model download chunk size for progress reporting.
|
||||
pub const DOWNLOAD_CHUNK_BYTES: usize = 65_536;
|
||||
|
||||
/// Inference thread count based on available parallelism.
|
||||
/// Inference thread count, clamped to physical-core budget.
|
||||
///
|
||||
/// Returns the system's physical-core count, clamped to
|
||||
/// `[MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS]`. Uses physical
|
||||
/// rather than logical cores because SMT/hyperthreaded siblings
|
||||
/// contend for shared FPU resources during heavy matmul; counting
|
||||
/// them as additional workers is well-documented to anti-scale on
|
||||
/// both whisper.cpp and llama.cpp.
|
||||
///
|
||||
/// Falls back to `available_parallelism` only if the physical-core
|
||||
/// probe is unavailable (some non-Linux/non-Windows platforms or
|
||||
/// containerised environments).
|
||||
///
|
||||
/// Users can override at runtime by setting
|
||||
/// `MAGNOTIA_INFERENCE_THREADS=N` — useful for benchmarking and for
|
||||
/// users on big-iron desktops who want to push past the cap.
|
||||
pub fn inference_thread_count() -> usize {
|
||||
std::thread::available_parallelism()
|
||||
.map(|p| p.get().saturating_sub(1))
|
||||
.unwrap_or(MIN_INFERENCE_THREADS)
|
||||
.max(MIN_INFERENCE_THREADS)
|
||||
if let Ok(s) = std::env::var("MAGNOTIA_INFERENCE_THREADS") {
|
||||
if let Ok(n) = s.parse::<usize>() {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user