vocal_helper.accel module
vocal_helper.accel
Picking the fastest transcription path this machine can actually run.
Transcription is the one stage whose speed decides whether a live session is possible at all. In batch, a slow engine is an inconvenience; live, the sum of the stages’ real-time factors must stay under 1, or the pipeline takes on a delay it will never recover and the latency grows without bound.
So this module does not accept a default. It probes the hardware, picks the best accelerator compiled into the installed whisper.cpp, sizes the thread pool to the cores that are actually fast, and says what it chose — because an unexplained real-time factor is the hardest kind of performance problem to chase.
The ladder, measured and sourced
Measured on 60 s of real French speech, Apple Silicon, large-v3-turbo-q5
(2026-11-01, studies/asr_acceleration_rtf.md):
path |
RTF |
vs CPU |
|---|---|---|
Apple Neural Engine (ANEForge) |
— |
~1.0–1.3× over Metal, ~5× less energy |
Metal (GPU) |
0.033 |
4.9× faster |
CPU + BLAS |
0.164 |
baseline |
The ANE row carries no local measurement because the installed build has no ANE
and no CoreML compiled in; its figures are the ones reported upstream, where the
backend was merged on 2026-09-18 (ggml-org/whisper.cpp#3905). CoreML sits
between the two at roughly 2× the Metal encoder, and is likewise absent here.
Two findings worth keeping, both counter-intuitive
MLX is not the fastest thing on Apple’s own hardware. whisper.cpp with Metal beat mlx-whisper by 1.85× on the same model and audio. Where the products still use mlx-whisper, it is for its word-onset accuracy, not its speed.
“Faster-whisper” is 20× slower here. CTranslate2 has no Metal path, so it stays on the CPU: real-time factor 0.657, leaving 1.5× of headroom, which does not hold a live session. The published “up to 4× faster than whisper.cpp” claims do not survive contact with Apple Silicon.
Threads: the fast cores, not all of them
Apple Silicon has performance and efficiency cores. Handing whisper.cpp a thread count that spills onto the efficiency cores makes it slower, because the fast cores then wait at every synchronisation point for the slow ones. The default was a hard-coded 6, which on a 12-core machine with 8 performance cores leaves two of them idle, and on a 4-performance-core laptop oversubscribes by half.
Usage example
>>> hw = probe()
>>> hw.performance_threads >= 1
True
>>> plan = resolve("auto")
>>> plan.accelerator in ("ane", "coreml", "metal", "cpu")
True
>>> plan.describe().startswith(plan.accelerator)
True
>>> resolve("cpu").accelerator
'cpu'
- vocal_helper.accel.ACCELERATORS = ('auto', 'ane', 'coreml', 'metal', 'cpu')
Accepted values.
"auto"resolves to the best the machine can actually run.
- class vocal_helper.accel.AccelPlan(accelerator, threads, model_kwargs=<factory>, env=<factory>, why='')[source]
Bases:
objectA resolved decision: which accelerator, how many threads, and how to get it.
- apply_env()[source]
Set this plan’s environment variables, without clobbering a caller’s.
A variable already set by whoever started the process wins: an operator who exported something did it on purpose, and silently overriding that is how a benchmark measures the wrong thing.
- Return type:
None
- class vocal_helper.accel.Hardware(system, machine, apple_silicon, performance_threads, whisper_backends=<factory>)[source]
Bases:
objectWhat this machine offers, as far as transcription is concerned.
- Parameters:
- describe()[source]
One line naming the machine and what it can run.
- Returns:
e.g.
"Darwin/arm64, 8 fast threads, backends: metal".- Return type:
Examples
>>> "threads" in probe().describe() True
- vocal_helper.accel.probe()[source]
Describe this machine once, and remember it.
- Returns:
Cached for the process: the answer cannot change while it runs, and the probe shells out to
sysctl, which is not free in a hot loop.- Return type:
Examples
>>> probe() is probe() True >>> probe().system == platform.system() True
- vocal_helper.accel.resolve(accelerator='auto', *, threads=None)[source]
Decide the transcription acceleration path for this machine.
- Parameters:
accelerator (str, optional) – One of
ACCELERATORS."auto"(default) takes the best the installed build can run. An explicit value is honoured even when the probe says it is unavailable, because an override exists precisely for the case where the probe is wrong — but the reason recorded on the plan says so, so a disappointing result is traceable.threads (int, optional) – Override the thread count.
Nonesizes it to the fast cores.
- Returns:
What to do, and why.
- Return type:
- Raises:
ValueError – On an unknown accelerator name, at decision time rather than deep inside a model constructor.
Examples
>>> resolve("metal", threads=4).threads 4 >>> resolve("cpu").model_kwargs {'context_params': {'use_gpu': False}} >>> resolve("nvidia") Traceback (most recent call last): ... ValueError: unknown accelerator 'nvidia'; choose from ane, auto, coreml, cpu, metal