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>
132 lines
4.9 KiB
Rust
132 lines
4.9 KiB
Rust
//! Thread-count scaling sweep for Whisper Tiny.
|
|
//! Runs the JFK clip at n_threads = 1, 2, 4, 6, 8, 12, prints RTF tables.
|
|
//! Env-gated by `LUMOTIA_WHISPER_TEST_MODEL` + `LUMOTIA_WHISPER_TEST_AUDIO`.
|
|
//!
|
|
//! Now prints multiple panels driven by `LUMOTIA_POWER_STATE_OVERRIDE` so
|
|
//! the helper's predicted thread count for each (power, GPU) combination
|
|
//! can be compared against the empirical RTF data.
|
|
|
|
use std::env;
|
|
use std::time::Instant;
|
|
|
|
use lumotia_core::hardware::vulkan_loader_available;
|
|
use lumotia_core::tuning::{inference_thread_count, Workload};
|
|
use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters};
|
|
|
|
#[test]
|
|
fn whisper_thread_count_sweep() {
|
|
let Ok(model_path) = env::var("LUMOTIA_WHISPER_TEST_MODEL") else {
|
|
return;
|
|
};
|
|
let Ok(audio_path) = env::var("LUMOTIA_WHISPER_TEST_AUDIO") else {
|
|
return;
|
|
};
|
|
|
|
let bytes = std::fs::read(&audio_path).expect("read wav");
|
|
let sample_rate = u32::from_le_bytes(bytes[24..28].try_into().unwrap());
|
|
let pcm = &bytes[44..];
|
|
let samples: Vec<f32> = pcm
|
|
.chunks_exact(2)
|
|
.map(|c| i16::from_le_bytes([c[0], c[1]]) as f32 / 32768.0)
|
|
.collect();
|
|
let audio_secs = samples.len() as f64 / sample_rate as f64;
|
|
eprintln!("[sweep] audio: {:.2}s @ {} Hz", audio_secs, sample_rate);
|
|
|
|
let logical = num_cpus::get();
|
|
let physical = num_cpus::get_physical();
|
|
eprintln!("[sweep] CPU: physical={}, logical={}", physical, logical);
|
|
|
|
let ctx = WhisperContext::new_with_params(&model_path, WhisperContextParameters::default())
|
|
.expect("model load");
|
|
|
|
// Warm-up pass to prime caches.
|
|
{
|
|
let mut state = ctx.create_state().expect("state");
|
|
let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
|
|
params.set_language(Some("en"));
|
|
params.set_n_threads(physical as i32);
|
|
params.set_print_special(false);
|
|
params.set_print_progress(false);
|
|
params.set_print_realtime(false);
|
|
state.full(params, &samples).expect("warmup");
|
|
}
|
|
|
|
let mut targets: Vec<i32> = vec![1, 2, 4, physical as i32, logical as i32];
|
|
if logical >= 8 && !targets.contains(&8) {
|
|
targets.push(8);
|
|
}
|
|
targets.sort();
|
|
targets.dedup();
|
|
|
|
// Snapshot the runtime Vulkan loader status once. The actual whisper
|
|
// context above already initialised whichever backend it could; the
|
|
// GPU panels below differ only in label and predicted-helper-pick.
|
|
// The runtime RTF rows are produced by the same backend the warm-up
|
|
// used.
|
|
let vulkan_runtime_ok = cfg!(feature = "whisper-vulkan") && vulkan_loader_available();
|
|
eprintln!(
|
|
"[sweep] whisper-vulkan feature: {}, libvulkan resolvable at runtime: {}",
|
|
cfg!(feature = "whisper-vulkan"),
|
|
vulkan_runtime_ok
|
|
);
|
|
|
|
// Four panels: CPU and GPU axes for the predicted-helper-pick column,
|
|
// crossed with AC and battery via LUMOTIA_POWER_STATE_OVERRIDE.
|
|
let panels = [
|
|
("AC, CPU", "ac", false),
|
|
("AC, GPU (Vulkan)", "ac", true),
|
|
("battery, CPU", "battery", false),
|
|
("battery, GPU (Vulkan)", "battery", true),
|
|
];
|
|
|
|
for (label, power, gpu_offloaded_for_helper) in panels {
|
|
env::set_var("LUMOTIA_POWER_STATE_OVERRIDE", power);
|
|
let helper_pick = inference_thread_count(Workload::Whisper, gpu_offloaded_for_helper);
|
|
run_sweep_panel(label, helper_pick, &ctx, &samples, audio_secs, &targets);
|
|
}
|
|
env::remove_var("LUMOTIA_POWER_STATE_OVERRIDE");
|
|
}
|
|
|
|
fn run_sweep_panel(
|
|
label: &str,
|
|
helper_pick: usize,
|
|
ctx: &WhisperContext,
|
|
samples: &[f32],
|
|
audio_secs: f64,
|
|
targets: &[i32],
|
|
) {
|
|
eprintln!();
|
|
eprintln!("=== n_threads scaling: {label} (helper picks: {helper_pick}) ===");
|
|
eprintln!("n_threads | xc_time | RTF | speedup_vs_1");
|
|
eprintln!("----------|---------|--------|-------------");
|
|
let mut baseline_dur: Option<f64> = None;
|
|
for n in targets {
|
|
// Two runs, take the min — best-case after L2/L3 warm.
|
|
let mut best = f64::MAX;
|
|
for _ in 0..2 {
|
|
let mut state = ctx.create_state().expect("state");
|
|
let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
|
|
params.set_language(Some("en"));
|
|
params.set_n_threads(*n);
|
|
params.set_print_special(false);
|
|
params.set_print_progress(false);
|
|
params.set_print_realtime(false);
|
|
let t = Instant::now();
|
|
state.full(params, samples).expect("transcribe");
|
|
let dur = t.elapsed().as_secs_f64();
|
|
if dur < best {
|
|
best = dur;
|
|
}
|
|
}
|
|
let rtf = best / audio_secs;
|
|
let speedup = baseline_dur.map(|b| b / best).unwrap_or(1.0);
|
|
if baseline_dur.is_none() {
|
|
baseline_dur = Some(best);
|
|
}
|
|
eprintln!(
|
|
"{:>9} | {:>6.2}s | {:>6.3} | {:>6.2}x",
|
|
n, best, rtf, speedup
|
|
);
|
|
}
|
|
}
|