Skip to content

perf: optimize BatchedInferencePipeline — VAD on GPU, feature pipelining, buffer pre-allocation - #1452

Open
RozhinKh wants to merge 1 commit into
SYSTRAN:masterfrom
RozhinKh:pr/pipeline-optimization
Open

perf: optimize BatchedInferencePipeline — VAD on GPU, feature pipelining, buffer pre-allocation#1452
RozhinKh wants to merge 1 commit into
SYSTRAN:masterfrom
RozhinKh:pr/pipeline-optimization

Conversation

@RozhinKh

@RozhinKh RozhinKh commented Jun 3, 2026

Copy link
Copy Markdown

Summary

End-to-end latency improvements for BatchedInferencePipeline via CPU pipeline restructuring and GPU placement fixes. Config held constant on both branches — code changes only, no model or decoder modifications.

Results (cold-pass, cache disabled, same config on both branches):

Hardware Config Latency Throughput
RTX 3090 24 GB int8_float16 / beam=5 / batch=32 -15.7 to -21.6% +27.6%
A100-SXM4-80GB 80 GB bfloat16 / beam=5 / batch=32 -18.2 to -22.9% --

SHA1-identical outputs across runs. 6/6 noise robustness conditions pass 3% WER regression threshold. All 9 Artemis ASR scenarios pass validity gate.

Changes

faster_whisper/vad.py

VAD on GPUSileroVADModel previously hardcoded CPUExecutionProvider. Profiling showed the 1 MB VAD model was consuming ~1.5 s per request due to sequential CPU execution across hundreds of 512-sample windows. The session now prefers CUDAExecutionProvider (falling back to CPU when unavailable), reducing VAD time to ~0.05 s per request.

Single-allocation VAD buffer__call__ previously required an already-padded array (a full np.pad copy). The new implementation accepts raw audio of any length and constructs the segmented buffer in a single np.empty allocation, zeroing only the segment-0 context strip and the last-segment tail padding. Eliminates ~80 MB of data movement for 21-minute audio. Verified bit-identical to the original across 11 audio lengths (0 to 20,160,001 samples).

faster_whisper/transcribe.py

Feature extraction pipeliningBatchedInferencePipeline previously blocked GPU inference waiting for CPU feature extraction to finish. A ThreadPoolExecutor(max_workers=1) now submits all batch extraction futures upfront. A background thread extracts batch N+1 features while CTranslate2 processes batch N on the GPU. CTranslate2 releases the GIL during CUDA operations, so the background thread runs freely with no synchronization overhead.

Feature buffer pre-allocation — replaced np.stack([pad_or_trim(f) for f in feats]) with a pre-allocated np.zeros((batch_size, n_mels, 3000), dtype=np.float32) result buffer, eliminating two intermediate allocations per batch.

Single-batch executor skip — for audio that fits in a single batch, bypasses the executor entirely since there is nothing to pipeline. Avoids thread creation overhead for short audio without affecting multi-batch workloads.

Eliminated duplicate tokenizer decodetokenizer.decode(tokens) was called twice per subsegment in forward(). Walrus operator reduces this to one call.

Test plan

  • Run benchmark/speed_benchmark.py on baseline vs optimized at matching config
  • Verify transcript output is unchanged (SHA1 parity across runs)
  • Confirm VAD fallback to CPU when CUDA unavailable
  • Confirm single-batch path produces identical output to multi-batch path

…ing, buffer pre-allocation

Profiling showed CPU preprocessing was the dominant bottleneck in
BatchedInferencePipeline on modern GPUs. This commit addresses it with
six targeted changes to transcribe.py and vad.py.

faster_whisper/vad.py:
- SileroVADModel: prefer CUDAExecutionProvider over hardcoded CPU;
  falls back to CPU when CUDA unavailable. Reduces VAD time from
  ~1.5 s to ~0.05 s per request on GPU hardware.
- SileroVADModel.__call__: single np.empty allocation replaces the
  separate np.pad copy the caller previously had to perform. Zeroes
  only segment-0 context strip and last-segment tail — avoids ~80 MB
  memset for 21-minute audio. Output verified bit-identical to the
  original np.pad approach across 11 audio lengths (0 to 20M samples).

faster_whisper/transcribe.py:
- Feature extraction pipelining: ThreadPoolExecutor(max_workers=1)
  submits all batch extraction futures upfront so a background thread
  extracts batch N+1 features while CTranslate2 processes batch N on
  GPU. CTranslate2 releases the GIL during CUDA ops so the thread
  runs freely with no extra synchronization.
- Feature buffer pre-allocation: np.zeros((B, n_mels, 3000)) replaces
  np.stack([pad_or_trim(f) for f in feats]), eliminating two
  intermediate allocations per batch.
- Single-batch executor skip: when len(batch_starts)==1 the executor
  is bypassed entirely — no pipeline benefit exists and thread
  creation overhead is pure cost for short audio.
- Eliminated duplicate tokenizer decode: walrus operator ensures
  tokenizer.decode(tokens) is called once per subsegment instead of
  twice (text field + get_compression_ratio).

Results (cold-pass, cache disabled, same config on both branches):
  RTX 3090  int8_float16/beam=5/batch=32  -15.7 to -21.6%  +27.6% throughput
  A100-SXM4 bfloat16/beam=5/batch=32     -18.2 to -22.9%  across 9 scenarios

SHA1-identical outputs. 6/6 noise conditions pass 3% WER threshold.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant