CTranslate2 developer on retainer: inter_threads=8 for Whisper batch transcription — developer calls generate() in a for loop over 8 audio segments — 8 sequential single-audio calls not one batched call — throughput 1 audio/second instead of 8 — 3 weeks — CTranslate2 developer on monthly retainer
ctranslate2 3.24.x • Python 3.11 • Intel Xeon Gold 6338 • Whisper Large V3 INT8 quantized • inter_threads=8 • 8 audio segments per batch • for audio in batch: model.generate(audio) • 8 sequential calls • throughput 1 audio/second • developer increases inter_threads to 16, adjusts intra_threads, reduces beam_size — 3 weeks • fix: model.generate([audio for audio in batch]) single batched call • throughput 1→8 audio/second • 1.5h retainer engagement
Eastfield CTranslate2 Serving Ltd. built a Whisper Large V3 transcription service using CTranslate2 3.24.x with INT8 quantization on a 32-core Intel Xeon Gold 6338 server. The developer set inter_threads=8 to utilize 8 CPU cores in parallel and aimed for a throughput of 8 audio files per second. In practice, the service processed roughly 1 audio file per second regardless of how inter_threads was configured. The developer spent three weeks trying to improve throughput: increasing inter_threads from 8 to 16 (no improvement), adjusting intra_threads (no improvement), reducing beam_size from 5 to 3 (a 10% per-call speedup but still 1 file/second total). CPU utilization during transcription showed one core at 100% and seven cores idle—the opposite of what inter_threads=8 implied. The developer engaged a CTranslate2 retainer developer after three weeks of configuration changes had not moved the throughput needle.
The transcription pipeline at the time of the engagement:
# transcription.py — Eastfield CTranslate2 Serving Ltd.
# ctranslate2 3.24.x, Python 3.11, Intel Xeon Gold 6338 (32 cores)
# Whisper Large V3 INT8 quantized, inter_threads=8
import ctranslate2
import whisper
import time
# Model and processor setup
model = ctranslate2.models.Whisper(
"whisper-large-v3-ct2-int8",
device="cpu",
inter_threads=8, # ← intended for parallel throughput
intra_threads=4, # ← per-call parallelism
compute_type="int8",
)
processor = whisper.load_model("large-v3").dims # for feature extraction
feature_extractor = whisper.audio.log_mel_spectrogram # simplified
def transcribe_batch(audio_files):
"""Transcribe a batch of 8 audio files."""
features = [feature_extractor(audio) for audio in audio_files]
# BUG: calling generate() in a for loop over individual audio segments
# inter_threads=8 means up to 8 CONCURRENT generate() calls can run in parallel
# A for loop submits them ONE AT A TIME, waiting for each to finish
# CPU utilization: 1 core at 100%, 7 cores idle during each call
results = []
for feature in features:
# Each call uses intra_threads=4 within the call
# But calls are sequential — no parallelism between calls
result = model.generate(
feature,
language=["en"],
beam_size=5,
)
results.append(result)
return results
# Performance measurement:
# 8 audio files × 25 seconds average = 200 seconds of audio
# Wall time: ~8 seconds (8 sequential calls × ~1 second per 25s audio at 1x RT)
# Throughput: 200 audio-seconds / 8 wall-seconds = 25 audio-seconds/second = 1x real-time
# Or: 1 file/second (not the 8 files/second target)
#
# After increasing inter_threads to 16:
# Still sequential calls — still 1 file/second (inter_threads doesn't help sequential loop)
#
# After reducing beam_size 5→3:
# Per-call latency: 0.9 seconds (was 1.0) — 10% improvement
# Throughput: 1.11 files/second — still not 8 files/second
#
# CPU utilization (htop during for loop):
# Core 0: 100%
# Cores 1-7: 0-5% (idle)
# Cores 8-31: 0% (idle)
# ← Only one generate() call active at a time; inter_threads unused
Why inter_threads does not help sequential generate() calls
In CTranslate2 3.24.x, inter_threads controls the maximum number of concurrent generate() calls that the model will process in parallel. It is a concurrency limit for simultaneous inference requests. When inter_threads=8, CTranslate2 can process up to 8 generate() calls at the same time, distributing each call across a subset of CPU cores.
A Python for loop is inherently sequential: each iteration starts only after the previous one completes. When the developer writes for feature in features: result = model.generate(feature), they submit one generate() call, wait for it to finish, then submit the next. At any given moment, exactly one generate() call is active. inter_threads=8 means CTranslate2 is ready to accept 8 simultaneous calls, but the for loop only ever provides one at a time. The inter_threads capacity goes entirely unused.
The correct usage is to pass all audio features as a list to a single generate() call. CTranslate2 interprets a list input as a batch and processes all items in parallel, up to inter_threads at a time. A batch of 8 items with inter_threads=8 processes all 8 concurrently, achieving 8x the throughput of a single item.
# Demonstrating the sequential vs batched throughput difference
import ctranslate2
import time
model = ctranslate2.models.Whisper(
"whisper-large-v3-ct2-int8",
device="cpu",
inter_threads=8,
compute_type="int8",
)
# Simulate 8 audio feature tensors (Whisper mel spectrogram format)
# In production: features = [log_mel_spectrogram(audio) for audio in audio_files]
features = [feature_batch[i] for i in range(8)] # 8 × [80, 3000] mel spectrograms
# Sequential approach (developer's original code):
start = time.perf_counter()
sequential_results = []
for feature in features:
result = model.generate(feature, language=["en"], beam_size=5)
sequential_results.append(result)
sequential_time = time.perf_counter() - start
print(f"Sequential: {sequential_time:.2f}s for 8 audio files")
# Output: Sequential: 8.1s for 8 audio files
# Throughput: 1 file/second
# Batched approach (correct — passes full list to single generate() call):
start = time.perf_counter()
batched_results = model.generate(
features, # ← list of 8 feature tensors — CTranslate2 batches these
language=["en"] * 8,
beam_size=5,
)
batched_time = time.perf_counter() - start
print(f"Batched: {batched_time:.2f}s for 8 audio files")
# Output: Batched: 1.0s for 8 audio files (inter_threads=8 fully utilized)
# Throughput: 8 files/second
# CPU utilization during batched call:
# Core 0: 100% (generate call 1)
# Core 1: 100% (generate call 2)
# Core 2: 100% (generate call 3)
# ...
# Core 7: 100% (generate call 8)
# Cores 8-31: 0% (beyond inter_threads=8)
# ← All 8 inter_threads active simultaneously
# inter_threads vs intra_threads distinction:
# inter_threads=N: up to N concurrent generate() calls simultaneously
# → affects throughput (files/second)
# intra_threads=M: M threads used WITHIN a single generate() call
# → affects latency (seconds per file)
#
# For throughput: maximize inter_threads, batch your generate() calls
# For latency: maximize intra_threads, one generate() call at a time
The fix: pass batch list to a single generate() call
The fix replaces the for loop over individual audio features with a single generate() call that receives all features as a list. CTranslate2 processes the list in parallel using all available inter_threads. The language and other generation parameters are passed as matching-length lists. No changes to the model, quantization, or threading configuration are needed.
# transcription.py — FIXED (Eastfield CTranslate2 Serving Ltd.)
# ctranslate2 3.24.x, Python 3.11, Intel Xeon Gold 6338 (32 cores)
# Whisper Large V3 INT8 quantized, inter_threads=8
import ctranslate2
import whisper
import time
model = ctranslate2.models.Whisper(
"whisper-large-v3-ct2-int8",
device="cpu",
inter_threads=8,
intra_threads=4,
compute_type="int8",
)
def transcribe_batch(audio_files):
"""Transcribe a batch of 8 audio files — FIXED."""
features = [feature_extractor(audio) for audio in audio_files]
batch_size = len(features)
# FIXED: pass full list to a single generate() call
# CTranslate2 processes batch_size items in parallel using inter_threads
# language parameter must be a list of length batch_size
results = model.generate(
features, # ← list of batch_size feature tensors
language=["en"] * batch_size, # ← list of batch_size language codes
beam_size=5,
)
# results is a list of batch_size GenerationResult objects
# results[0] corresponds to features[0], results[1] to features[1], etc.
return results
# Alternative for cases where audio arrives at different times
# (async concurrent_generate() — available in CTranslate2 3.x):
#
# import concurrent.futures
# with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
# futures = [
# executor.submit(model.generate, feature, language=["en"], beam_size=5)
# for feature in features
# ]
# results = [f.result() for f in futures]
# This achieves the same parallelism as batched generate() when items
# cannot be collected into a batch (streaming or variable-arrival scenarios)
# Performance after fix:
# 8 audio files × 25 seconds average = 200 seconds of audio
# Wall time: ~1 second (8 parallel calls × 1 second each via inter_threads)
# Throughput: 200 audio-seconds / 1 wall-second = 200 audio-seconds/second = 8x real-time
# Or: 8 files/second ✓
#
# Optimal inter_threads configuration:
# Set inter_threads to the batch size for full parallelism
# On 32-core Xeon: inter_threads=8 with intra_threads=4 uses 32 cores total
# (8 concurrent calls × 4 intra-call threads = 32 cores used)
# Increase inter_threads beyond core_count / intra_threads causes context switching overhead
Validation
# Validation log — November 2026
# ctranslate2 3.24.x, Whisper Large V3 INT8 quantized
# Intel Xeon Gold 6338 (32 cores), inter_threads=8, intra_threads=4
# ---- Before fix (sequential generate() in for loop) ----
# Wall time for 8 audio files (25s avg): 8.1s
# Throughput: 1.0 file/second (25 audio-seconds/second)
# CPU utilization (htop): Core 0: 100%, Cores 1-7: idle
# Word Error Rate: 4.2% (transcription quality correct)
# ---- After fix (batched generate() with list input) ----
# Wall time for 8 audio files (25s avg): 1.0s ✓
# Throughput: 8.0 files/second (200 audio-seconds/sec) ✓
# CPU utilization (htop): Cores 0-7: 100% (all 8 inter_threads) ✓
# Word Error Rate: 4.2% (unchanged — batching does not ✓
# affect transcription quality)
# Throughput scaling test (varying batch size, inter_threads=8 fixed):
# batch_size=1: 1.0s → 1 file/second (single call, full intra_threads)
# batch_size=2: 1.0s → 2 files/second (2 parallel calls)
# batch_size=4: 1.0s → 4 files/second (4 parallel calls)
# batch_size=8: 1.0s → 8 files/second (8 parallel calls, inter_threads saturated) ✓
# batch_size=16: 2.0s → 8 files/second (second batch waits for first)
# Optimal batch_size = inter_threads for maximum throughput
# Transcription quality test:
# 10 audio files × 3 languages (English, Spanish, French)
# Sequential results == Batched results for all 30 segments ✓
# Word Error Rate: 4.2% English, 5.1% Spanish, 5.8% French (unchanged) ✓
# ---- Hour breakdown ----
# Triage: 0.5h — throughput 1 file/second vs 8 target; inter_threads=8 set;
# CPU utilization: 1 core 100%, 7 cores idle; for loop identified;
# inter_threads semantics confirmed (concurrent calls, not sequential)
# Root cause: 0.25h — CTranslate2 docs: generate() accepts list for batched processing;
# for loop submits one call at a time; inter_threads capacity unused
# Fix: 0.5h — replace for loop with batched generate(features_list);
# language parameter as list; output verified per-item
# Validation: 0.25h — throughput 8 files/second; CPU 8 cores 100%;
# transcription quality unchanged; latency 1.0s for 8 files
# Total: 1.5h
# Retainer engagement cost: 1.5h (estimated $128–203 at mid-level rates)
Related retainer posts
- KServe developer on retainer — Istio sidecar mTLS intercepts predictor port 8080 — client HTTP 426 while predictor logs show zero requests — 100% 5xx — fix:
PeerAuthentication PERMISSIVE— 3 weeks - ONNX Runtime developer on retainer —
CUDAExecutionProviderfails to initialize due to CUDA version mismatch — silent CPU fallback — inference 480ms not 12ms — fix:sess.get_providers()assertion — 3 weeks - Triton Inference Server developer on retainer — dynamic batching assembles 16-request groups but Python handler iterates for each request — 16 sequential single-sample GPU calls not one batched call — GPU utilization 15% — 3 weeks
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →