abort_callback now stops whisper_full during language auto-detect

2 days ago

If you call whisper_full with the language set to auto, it runs a detection pass before it transcribes anything, one encode and one decode. Until this change, that pass ignored the abort callback you passed in. This pull request passes the callback down, so an abort during detection now stops the call before transcription starts.

Ask about this presentation

Chapters

  1. 0:00abort_callback reaches language detection
  2. 0:21Where the abort was lost
  3. 0:44The wrappers pass nullptr
  4. 1:06A static helper carries the callback
  5. 1:27Encode and decode inside the helper
  6. 1:49whisper_full hands over its params
  7. 2:11The test
  8. 2:33What it does not cover
  9. 2:54Covered in CI
Show transcript
  1. If you call whisper_full with the language set to auto, it runs a detection pass before it transcribes anything, one encode and one decode. Until this change, that pass ignored the abort callback you passed in. This pull request passes the callback down, so an abort during detection now stops the call before transcription starts.

    Slide text

    [00:00:00.000 --> 00:00:21.754]

    abort_callback now stops whisper_full during language auto-detect

    whisper : run abort_callback on the language auto-detect path

    Callers that set language = "auto" and pass an abort callback.

    whisper.cpp
  2. Here is the order inside whisper_full_with_state. It computes the mel spectrogram, and when the language is auto, it runs language detection first. The abort callback isn't passed there. It's only handed to the encoder and decoder in the main loop, so an abort asked for during detection wasn't noticed until the first encode of the main loop had finished. That's issue 3888.

    Slide text

    [00:00:21.754 --> 00:00:46.200]

    Language detection ran before the abort callback was wired in

    Mel spectrogram

    whisper_full_with_state

    language = "auto"

    →

    Language detection

    whisper_lang_auto_detect_with_state

    abort_callback: not passed

    →

    Main loop

    encode, then decode

    abort_callback: passed

    Fixes #3888. encoder_begin_callback got its own fix in #3936.

  3. The cause is in the two public wrappers detection was calling. whisper_encode_with_state takes no callback, and it hands nullptr to the internal encoder. whisper_decode_with_state does the same, after it prepares the batch and clears the KV cache. Adding a parameter to either one would change the public API, and the author set out to avoid that.

    Slide text

    [00:00:46.200 --> 00:01:08.338]

    Detection called public wrappers that hard-code nullptr for the callback

    // src/whisper.cpp, before this change int whisper_encode_with_state(ctx, state, offset, n_threads) { if (!whisper_encode_internal(*ctx, *state, offset, n_threads, nullptr, nullptr)) {
    int whisper_decode_with_state(ctx, state, tokens, n_tokens, n_past, n_threads) { whisper_batch_prep_legacy(state->batch, tokens, n_tokens, n_past, 0); whisper_kv_cache_seq_rm(state->kv_self, 0, n_past, -1); if (!whisper_decode_internal(*ctx, *state, state->batch, n_threads, false, nullptr, nullptr)) {
  4. So the detection body moves into a static function, whisper_lang_auto_detect_internal, with two more arguments, the callback and its user data. The public whisper_lang_auto_detect_with_state becomes a one line wrapper that calls it with nullptr for both. Existing callers keep the same signature and the same behavior.

    Slide text

    [00:01:08.338 --> 00:01:26.246]

    A static helper takes the callback, and the public function wraps it

    - int whisper_lang_auto_detect_with_state( + static int whisper_lang_auto_detect_internal( struct whisper_context * ctx, struct whisper_state * state, int offset_ms, int n_threads, float * lang_probs, + ggml_abort_callback abort_callback, void * abort_callback_data) {
    + int whisper_lang_auto_detect_with_state(ctx, state, offset_ms, n_threads, lang_probs) { + return whisper_lang_auto_detect_internal(ctx, state, offset_ms, n_threads, lang_probs, + nullptr, nullptr); + }
  5. The encode now calls whisper_encode_internal with the callback. The test flips, because the wrapper returned zero on success and the internal function returns true. Decode takes more. The wrapper also prepared the batch and cleared the KV cache, so those two lines move into the helper, with n past fixed at zero, which is the value the old call passed.

    Slide text

    [00:01:26.246 --> 00:01:49.923]

    Inside the helper, encode and decode call the internal functions directly

    - if (whisper_encode_with_state(ctx, state, seek, n_threads) != 0) { + if (!whisper_encode_internal(*ctx, *state, seek, n_threads, + abort_callback, abort_callback_data)) {
    - if (whisper_decode_with_state(ctx, state, prompt.data(), prompt.size(), - 0, n_threads) != 0) { + whisper_batch_prep_legacy(state->batch, prompt.data(), prompt.size(), 0, 0); + whisper_kv_cache_seq_rm(state->kv_self, 0, 0, -1); + if (!whisper_decode_internal(*ctx, *state, state->batch, n_threads, false, + abort_callback, abort_callback_data)) {
  6. And this is the line that connects it. whisper_full_with_state now calls the helper with the abort callback and the user data from its params. The callback is checked as soon as the detection encode finishes. When it says stop, the encode returns false, the helper returns minus 6, and whisper_full returns minus 3 with failed to auto-detect language in the log.

    Slide text

    [00:01:49.923 --> 00:02:13.985]

    whisper_full_with_state now hands its abort callback to detection

    - const auto lang_id = whisper_lang_auto_detect_with_state( - ctx, state, 0, params.n_threads, probs.data()); + const auto lang_id = whisper_lang_auto_detect_internal( + ctx, state, 0, params.n_threads, probs.data(), + params.abort_callback, params.abort_callback_user_data); if (lang_id < 0) { WHISPER_LOG_ERROR("%s: failed to auto-detect language\n", __func__); return -3; }

    An abort in detection: encode fails, the helper returns -6, whisper_full returns -3.

  7. The evidence is a new test. It feeds two seconds of silence with the language on auto, an abort callback that returns true right away, and an encoder begin callback that only counts. whisper_full has to fail, and the counter has to read one. That one call is the check that runs just before detection, so the encoder in the main loop never began.

    Slide text

    [00:02:13.985 --> 00:02:39.200]

    A new test aborts during detection and checks the main loop never began

    tests/test-whisper-lang-detect-abort.cpp

    std::vector<float> pcmf32(2*WHISPER_SAMPLE_RATE, 0.0f); // 2 s of silence params.language = "auto"; params.encoder_begin_callback = encoder_begin_cb; // don't block anything, just count params.abort_callback = abort_cb; // abort immediately const int rc = whisper_full(ctx, params, pcmf32.data(), pcmf32.size()); assert(rc != 0); // the call must fail because we aborted // aborted during auto-detect: main-loop encoder never started assert(n_encoder_begin == 1);
  8. The fix lives inside whisper_full. If your code calls whisper_lang_auto_detect or its with state version directly, those still pass nullptr, so detection there still runs to the end. And the batch prep and KV cache lines now exist in two places, so a later change to whisper_decode_with_state needs the same change in the helper.

    Slide text

    [00:02:39.200 --> 00:03:00.569]

    Calling the detection functions directly still can't be aborted

    Public detect functions

    The wrapper still passes nullptr, nullptr.

    whisper_lang_auto_detect(…) whisper_lang_auto_detect_with_state(…)

    Two lines, two places

    Batch prep and KV cache clear now also live in the helper.

    whisper_decode_with_state(…) whisper_lang_auto_detect_internal(…)
  9. The test is registered with the tiny and gh labels and runs against the tiny test model, so the ctest run with the gh label covers it. If you already pass an abort callback to whisper_full with the language on auto, there's nothing to change. The abort callback now runs on the language auto-detect path.

    Slide text

    [00:03:00.569 --> 00:03:22.323]

    The abort callback now runs on the language auto-detect path

    # tests/CMakeLists.txt registers it with LABELS "tiny;gh" ctest -L gh --output-on-failure

    Using whisper_full with language = "auto" and an abort callback? Nothing to change.

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built in New York with ❤️