openai/whisper
Decoding
Active contributors: Jong Wook Kim
Purpose
Run autoregressive decoding on a single 30 s audio chunk. Given a Whisper model and a Mel spectrogram, produce a list of DecodingResults — one per audio in the (optional) batch — each containing the predicted text, tokens, detected language, average log-probability, no-speech probability, and the compression ratio. This is the layer the long-form transcribe() calls many times per file.
Directory layout
whisper/
└── decoding.pyKey abstractions
| Symbol | Description |
|---|---|
DecodingOptions |
Frozen dataclass of decoder configuration: task, language, sampling, prompt, suppress, fp16. |
DecodingResult |
Frozen dataclass of decoder output: text, tokens, language, probs, log-probs, ratios. |
Inference |
Interface for "given tokens and audio features, return next-token logits." |
PyTorchInference |
Concrete Inference that owns the KV cache and runs model.decoder per step. |
SequenceRanker / MaximumLikelihoodRanker |
Pick the best candidate per audio at the end of beam search / best-of-N sampling. |
TokenDecoder |
Strategy for picking the next token: GreedyDecoder or BeamSearchDecoder. |
LogitFilter family |
SuppressBlank, SuppressTokens, ApplyTimestampRules. In-place logit modifications. |
DecodingTask |
The orchestrator. Builds initial tokens, instantiates inference/decoder/filters, runs main loop. |
detect_language(model, mel, tokenizer=None) |
Special single-step decode that masks all non-language tokens. Returns top language token + per-language probabilities. |
decode(model, mel, options=DecodingOptions(), **kwargs) |
The public entry point. Bound onto Whisper.decode. |
How it works
graph TD
options[DecodingOptions] --> task[DecodingTask.__init__]
model[Whisper] --> task
task --> tok[Tokenizer for language/task]
task --> inf[PyTorchInference]
task --> dec{decoder}
task --> filters[LogitFilter list:<br/>SuppressBlank<br/>SuppressTokens<br/>ApplyTimestampRules]
dec -- temperature == 0 and beam_size --> beam[BeamSearchDecoder]
dec -- otherwise --> greedy[GreedyDecoder]
mel[mel: B x n_mels x 3000] --> run[DecodingTask.run]
run --> feats[encoder forward]
feats --> langd{language is None?}
langd -- yes --> dl[_detect_language] --> rewrite[overwrite lang token in tokens]
langd -- no --> loop
rewrite --> loop
loop[_main_loop] -->|step k| inf
inf -->|logits| filters
filters --> dec
dec -->|completed?| loop
loop --> finalize[decoder.finalize] --> rank[MaximumLikelihoodRanker.rank]
rank --> result[List of DecodingResult]Initial tokens
_get_initial_tokens builds the decoder prefix:
[<|startofprev|>, ...prompt..., <|startoftranscript|>, <|lang|>, <|task|>, ...prefix..., <|notimestamps|>?]prompt(text or token IDs) — previous-context priming. Truncated to fitn_ctx // 2 - 1tokens.prefix(text or token IDs) — primes the current segment; preserved at decode time. Truncated to fitn_ctx // 2 - sample_len.- The
sot_sequencealready includes<|startoftranscript|>plus the language and task tokens (built inTokenizer.__post_init__). - If
options.without_timestamps,<|notimestamps|>is appended.
sample_begin is set to the length of this prefix and is used everywhere downstream to know where "sampled" tokens start.
Inference and KV caching
PyTorchInference.logits(tokens, audio_features) is the only place that calls model.decoder. It lazy-installs the KV cache hooks the first time it is called. After the first call it slices tokens[:, -1:] so only the newest token runs through self-attention; the cache returns the previously computed K/V for older tokens.
rearrange_kv_cache(source_indices) reorders cached K/V tensors when beam search reshuffles its beams. cleanup_caching() removes the forward hooks and clears the cache; it runs in a finally in _main_loop.
Logit filters
Three filters are composed in this order:
SuppressBlank: at the very first sampled position, masks out the space token and EOT so the model cannot emit an empty output.SuppressTokens: masks any tokens listed inoptions.suppress_tokens. The default"-1"expands totokenizer.non_speech_tokens(parens, brackets, music notes, etc.) plus the special tokens<|transcribe|>,<|translate|>,<|startoftranscript|>,<|startofprev|>,<|startoflm|>, and<|nospeech|>.ApplyTimestampRules(only when timestamps are enabled): enforces three rules on the output:<|notimestamps|>is always masked (it would only confuse the rest of the loop).- Timestamps must come in pairs (start, end). After a single timestamp, the next must be non-timestamp; after a pair, the next must be a timestamp again.
- Timestamps must be non-decreasing within a segment, and at least one frame apart (no zero-length segments).
- At
sample_begin, only timestamp tokens are allowed (segments must start with a timestamp), bounded above bymax_initial_timestamp_index. - At every step, if the total probability mass on timestamp tokens exceeds the largest non-timestamp probability, all non-timestamp logits are masked. This biases generation toward timestamps when the model is "uncertain" between text and a segment break.
Token decoders
GreedyDecoder(temperature == 0): picksargmax. Withtemperature > 0, samples fromCategorical(logits / temperature).BeamSearchDecoder(beam_size is not None,temperature == 0): standard beam search with patience. For each beam it considersbeam_size + 1candidate next tokens (so the EOT can pop out without losing any actual text continuations), keeps the topbeam_sizenon-EOT and routes EOT-completing sequences into per-audio finished sets. Stops when each audio hasround(beam_size * patience)finished candidates.rearrange_kv_cachekeeps the inference cache in sync with the surviving beams.
_detect_language
When options.language is None (or options.task == "lang_id"), a single forward pass at sot_index + 1 is taken to score every language token. The detected language token is written into tokens[:, sot_index + 1] for the rest of the loop. This is also reachable as whisper.detect_language(model, mel).
Main loop
for i in range(self.sample_len):
logits = self.inference.logits(tokens, audio_features)
if i == 0 and self.tokenizer.no_speech is not None:
# save no_speech_prob from the SOT-position logits
probs_at_sot = logits[:, self.sot_index].float().softmax(dim=-1)
no_speech_probs = probs_at_sot[:, self.tokenizer.no_speech].tolist()
logits = logits[:, -1]
for f in self.logit_filters: f.apply(logits, tokens)
tokens, completed = self.decoder.update(tokens, logits, sum_logprobs)
if completed or tokens.shape[-1] > self.n_ctx: breakno_speech_prob is computed exactly once, from the SOT-position logits at step 0, before any logit filter runs. This is what transcribe() later compares against no_speech_threshold.
After the loop, decoder.finalize(tokens, sum_logprobs) returns full token sequences (padded with EOT for greedy, or unfinished beams promoted to finished for beam search). MaximumLikelihoodRanker selects the best candidate per audio using either plain length normalization (length_penalty=None) or the Google NMT length penalty.
Verification
_verify_options rejects nonsense combinations: beam_size and best_of are mutually exclusive; best_of is incompatible with greedy sampling; patience requires beam_size; length_penalty must be in [0, 1].
Integration points
- Imports from:
whisper/audio.py:CHUNK_LENGTH;whisper/tokenizer.py(Tokenizer,get_tokenizer);whisper/utils.py:compression_ratio. - Imported by:
whisper/__init__.py(re-exportsDecodingOptions,DecodingResult,decode,detect_language);whisper/model.py(bindsdecodeanddetect_languageontoWhisper);whisper/transcribe.py(callsmodel.decode).
Entry points for modification
- New sampling strategies: subclass
TokenDecoder. Add a branch inDecodingTask.__init__. - New logit constraints: subclass
LogitFilterand add to thelogit_filterslist. - New decoding options: extend
DecodingOptions, then thread the field through_get_initial_tokens/_get_suppress_tokens/_main_loopas appropriate. - Custom inference backend (e.g. ONNX, CT2): subclass
Inferenceand instantiate it inDecodingTask.__init__instead ofPyTorchInference.
See also: Transcribe for the surrounding fallback / segment loop, and Tokenizer for what sot_sequence and non_speech_tokens actually contain.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.