Skip to content

[Model] Add transcription to the Transformers modeling backend - #65735

Merged
hmellor merged 27 commits into
vllm-project:mainfrom
hmellor:transformers-backend-audio
Nov 9, 2026
Merged

hmellor merged 27 commits into
vllm-project:mainfrom
hmellor:transformers-backend-audio

Conversation

@hmellor

@hmellor hmellor commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Overview

Adds /v1/audio/transcriptions to the Transformers modeling backend, so VibeVoice-ASR is served without the native implementation in #62398. Needs Transformers 5.23 for tokenize on apply_transcription_request (huggingface/transformers#49944), and huggingface/transformers#50417 for the sync check, so it stays a draft until those are released.

Claims

  • New TransformersMultiModalForTranscription, selected when the HF processor class has apply_transcription_request, serves json, text and diarized_json. Also checked through the API: GLM-ASR, Audio Flamingo 3 and Qwen/Qwen3-ASR-1.7B-hf transcribe, and Granite Speech has no endpoint.
  • post_process_output and parse_diarized_transcript now take model_config, and the latter parses each chunk's raw output, with timestamps shifted by the chunk offset.
  • Transcription models measure their audio token limit from the processor, as config values can be per encoder window. Those that don't truncate are sized by the context length up to the audio decode limits (--limit-mm-per-prompt changes it), and other audio models keep their budget.
  • Different-length audios in one encoder batch no longer crash, and VibeVoice-ASR's padding_mask stays on the CPU so that path passes VLLM_GPU_SYNC_CHECK=error with [Bugfix][Model Runner V2] Restore multimodal draft capability detection #50417.

Validation

B200, bf16, Transformers 5.23.0.dev0. Native numbers are from #62398 on different hardware. The long clips differ: here it is plain WER on 40 min of concatenated LibriSpeech.

WER #62398 native This PR, via the API
LibriSpeech test-clean, 500 utts 2.14 % 2.07 %
40 min, one request 1.56 % 1.64 %

Noise: the backend keeps HF's default VAE sampling, which gave 2.03 % and 2.08 % across two seeds through LLM before this rebase. With that default, 5 min at the default limits gave 2.35 %, and 40 min with the three audio limits raised gave 1.58 % at --max-model-len 32768, on this PR's code applied to 9b34fec. --pipeline-parallel-size 2 gave 2.48 % on 20 LibriSpeech utterances and 2.47 % on 5 min. A LoRA adapter on the text model loads and changes the output, and a zero adapter leaves it unchanged. VoxPopuli-de, cpWER, 60 min, vllm bench serve and TP from #62398 were not rerun.

vllm serve microsoft/VibeVoice-ASR-HF --dtype bfloat16 --max-model-len 32768 --gpu-memory-utilization 0.5 --seed 0
pytest tests/models/multimodal/processing/test_transformers_audio.py \
  tests/models/multimodal/processing/test_moss_transcribe_diarize.py \
  tests/models/multimodal/processing/test_ming_v3_asr.py \
  tests/entrypoints/speech_to_text/transcription/test_transcription_inter_chunk_spacing.py
pytest tests/models/multimodal/generation/test_transformers_audio.py -k "VibeVoice or mixed_lengths"
pytest tests/entrypoints/speech_to_text/transcription/test_transcription_validation.py -k transformers_backend_audio
pytest tests/models/test_initialization.py -k "VibeVoice or TransformersMultiModalForTranscription"

All pass (86 passed, 1 xfailed). test_transformers_audio_mixed_lengths fails without the fix with AttributeError: 'list' object has no attribute 'sum'. Native Qwen/Qwen3-ASR-1.7B still transcribes. MossAudioModel initialization fails with and without this PR. The tests needing 5.23 skip in CI until the pin is bumped.

Details


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

🤖 Generated with Claude Code

Serve /v1/audio/transcriptions, including diarized_json, for Transformers
speech models through a new TransformersMultiModalForTranscription class.
Prompts come from HF's apply_transcription_request and outputs from the
processor's decode return formats. post_process_output and
parse_diarized_transcript now take model_config so the backend can reach the
processor, and parse_diarized_transcript gets the raw output.

Audio processors that do not truncate (e.g. VibeVoice-ASR) were limited to
their config's max_position_embeddings, which for VibeVoice is the tokens in
one 60 s encoder chunk. Read truncation/max_length from the audio processor
instead (falling back to n_samples/nb_max_samples until
huggingface/transformers#44394 lands). With no cap, bound audio by the context
length and profile with one dummy audio of that size.

Select ragged per-item fields as a batch of one tensor, which crashed
different-length audios in one batch, and allow the sync in that path.

Replaces vllm-project#62398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
@mergify

mergify Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--65735.org.readthedocs.build/en/65735/

@mergify mergify Bot added documentation Improvements or additions to documentation frontend multi-modality Related to multi-modality (#4194) new-model Requests to new models qwen Related to Qwen models kimi labels Nov 4, 2026
hmellor and others added 7 commits November 5, 2026 10:20
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Parse each chunk's raw output on its own and shift its timestamps by the
chunk offset, so serving needs no second list of raw text and diarization
works for models that split audio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
hmellor and others added 4 commits November 5, 2026 18:19
…yncs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Sizing every uncapped audio model by the context length stopped Granite
Speech starting on small GPUs. Keep the previous budget unless the processor
has apply_transcription_request and does not truncate, and cap those models
at one hour, which the audio limit can change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
The speech model mapping gave Granite Speech a class it could not use and left
out GLM-ASR and Audio Flamingo 3. Check the processor class instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
vLLM's Qwen3ASRConfig replaced the Transformers config for every qwen3_asr
checkpoint, which dropped the audio_config of Transformers-format ones. Keep
it for native checkpoints only. Measure the audio token limit of transcription
models from the processor, as Qwen3-ASR's config limit is per encoder window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
@sebfelix

sebfelix commented Nov 6, 2026

Copy link
Copy Markdown

Thanks @hmellor, this covers everything #62398 was for. I ran the evaluation from #62398 against this PR (head f071d7d881, Transformers main @ b55e076, 5.23.0.dev0) on the same hardware as the native numbers: Quadro RTX 6000 (Turing), fp16, TP=2, --enforce-eager.

Native (#62398) This PR, seed 0 This PR, seed 1 This PR + sample=False
LibriSpeech test-clean, 500 utts, WER 2.14 % 2.13 % 2.18 % 2.14 %
VoxPopuli-de, 300 utts, WER (cleaned, 290) 17.23 % (12.97 %) 17.24 % (13.00 %) – 17.24 % (12.98 %)
20 synthetic 2–3 speaker conversations, WER / cpWER / speaker count 1.98 / 4.25 % / 18 of 20 2.06 / 4.38 % / 18 of 20 2.00 / 4.27 % / 18 of 20 1.98 / 4.25 % / 18 of 20
40 min, 8 speakers, one request, WER / cpWER 1.56 / 7.86 % 1.50 / 7.79 % – 1.53 / 7.83 %

Accuracy and diarization are on par in every set, including single-pass long audio. Three notes:

  1. Determinism. With VAE sampling, the transcript depends on the seed: only 471/500 LibriSpeech and 8/20 conversation transcripts are identical between seeds 0 and 1. Passing sample=False to get_audio_features ([VibeVoice] Add sample argument to disable VAE noise in audio features huggingface/transformers#50179, also in 5.23) makes the outputs identical to the native implementation (500/500, 20/20, 299/300 VoxPopuli) at no accuracy cost. It also keeps embeddings that the encoder or prefix cache reuses independent of which request computed them. I tested this by setting kwargs["sample"] = False in _process_audio_input. A signature check won't find the parameter, because the top-level model's get_audio_features is wrapped as (*args, **kwargs).
  2. Streamed usage already includes the audio tokens. A 4.03 s clip reports prompt_tokens: 90, which is 60 text tokens plus 30 audio placeholder tokens: prompt_token_ids contain the expanded placeholders. So the "Details" line about usage could go. Adding get_num_audio_tokens would count the audio twice, which [Model][Frontend] Add native VibeVoice-ASR with transcription API support #62398 actually did.
  3. Long audio. In [Model][Frontend] Add native VibeVoice-ASR with transcription API support #62398 runs, the output degraded beyond about 40 min of audio (60-min clips loop after ~36–50 min). It's a decoder long-context limit above ~40k tokens: fp32 behaves the same, and those minutes transcribe cleanly on their own. A docs note recommending client-side splitting beyond ~40 min might help users who raise the decode limits.

Pass `strip_prefix` to the processor's `decode` when it is supported. Part of
vllm-project#65735, which replaces vllm-project#62398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
hmellor added a commit to hmellor/vllm that referenced this pull request Nov 6, 2026
Part of the Qwen3-ASR config port for vllm-project#65735, which replaces vllm-project#62398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
hmellor added a commit to hmellor/vllm that referenced this pull request Nov 6, 2026
Part of the Qwen3-ASR config port for vllm-project#65735, which replaces vllm-project#62398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
hmellor added a commit to hmellor/vllm that referenced this pull request Nov 6, 2026
Part of the Qwen3-ASR config port for vllm-project#65735, which replaces vllm-project#62398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
…6097

vllm-project#66097 deletes the vendored config, so both checkpoint formats use the
upstream class and this branch no longer needs to choose between them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>

@Isotr0py Isotr0py left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall look reasonable

Comment thread vllm/config/model.py Resolved
hmellor and others added 4 commits November 6, 2026 16:45
The class check only sees processors in Transformers, so a custom processor
with `apply_transcription_request` never reached the transcription class.
Load it when remote code is trusted, as nothing else exposes its class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
…ackend

`n_samples` was read as a limit, but MOSS and Qwen3-ASR split longer audio into
windows of it, so the server cut MOSS recordings at 30 s and speaker labels
restarted. Check whether long audios stop adding tokens instead.

That needs the windows of one audio to stay together, which also fixes audio
over 30 s crashing the multimodal processor for GLM-ASR and Audio Flamingo 3.
Checkpoint output limits are now skipped for unchunked audio, as 512 tokens
cut Qwen3-ASR short, and HF-format MOSS keeps the upstream config so it loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
It was added because the Transformers backend chunked MOSS by mistake. No
diarizing model chunks, so parse the raw output as a whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
@mergify

mergify Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @hmellor.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Nov 8, 2026
Comment thread vllm/config/model.py Resolved
Also pass `processor_cls` so the remote code load shares its cache entry
with the bare load in `cached_get_processor_without_dynamic_kwargs`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
@hmellor

hmellor commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

/ci run

@hmellor
hmellor enabled auto-merge (squash) November 9, 2026 09:32
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

❌ This PR is 136 commits behind upstream main. The bot could not merge upstream main into this branch (API returned 422: merge conflict between base and head). No new CI build was started. Either merge or rebase onto the latest main yourself and rerun /ci run, or use /ci run --allow-stale to test this branch as is.

…d-audio

Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>

# Conflicts:
#	vllm/model_executor/models/interfaces.py
#	vllm/model_executor/models/moss_transcribe_diarize.py
@hmellor

hmellor commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

/ci run

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #101068 for commit 4ac5a05965d3.

@mergify mergify Bot removed the needs-rebase label Nov 9, 2026
@github-actions github-actions Bot added the ready ONLY add when PR is ready to merge/full CI is needed label Nov 9, 2026
@hmellor

hmellor commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

Checked that pre-commit passes locally on the PR head (4ac5a05), running pre-commit run --from-ref <merge-base with main> --to-ref HEAD. All hooks pass (ruff, mypy, typos, markdownlint, docstring and config checks, etc.).

@hmellor hmellor added the verified Run pre-commit for new contributors without triggering other tests label Nov 9, 2026
@hmellor
hmellor merged commit 68a62a2 into vllm-project:main Nov 9, 2026
316 checks passed
@hmellor
hmellor deleted the transformers-backend-audio branch November 9, 2026 12:19
@github-project-automation github-project-automation Bot moved this from In review to Shipped in Transformers modeling backend Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation frontend kimi multi-modality Related to multi-modality (#4194) new-model Requests to new models qwen Related to Qwen models ready ONLY add when PR is ready to merge/full CI is needed verified Run pre-commit for new contributors without triggering other tests

Projects

Development

Successfully merging this pull request may close these issues.

3 participants