Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
From 2186b89a9a02fac11fa0e274f3201b9812dbf65c Mon Sep 17 00:00:00 2001
From: Damien Laine <damien.laine@gmail.com>
Date: Fri, 9 Oct 2026 11:40:29 +0200
Subject: [PATCH] [Bugfix][Model] Voxtral realtime: fix boot OOM and engine
crash at max_model_len (#45022)

Signed-off-by: Damien Laine <damien.laine@gmail.com>
Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk>
---
Backport of the model-side part of vllm-project/vllm#45022 (commit
2186b89a9a02fac11fa0e274f3201b9812dbf65c) onto vLLM 0.31.0. Only the
vllm/model_executor/models/voxtral_realtime.py diff is carried, unchanged.
Remove this patch when the CUDA vLLM pin contains 2186b89a9a.
---
diff --git a/vllm/model_executor/models/voxtral_realtime.py b/vllm/model_executor/models/voxtral_realtime.py
index 8531d7d18f39..72318976f1d4 100644
--- a/vllm/model_executor/models/voxtral_realtime.py
+++ b/vllm/model_executor/models/voxtral_realtime.py
@@ -120,9 +120,35 @@ def _expand_tensor(input_tensor: torch.Tensor, scaling: int) -> torch.Tensor:
return (base.unsqueeze(1) + offsets).view(-1)


+class VoxtralRealtimeProcessingInfo(VoxtralProcessingInfo):
+ def get_supported_mm_limits(self) -> Mapping[str, int | None]:
+ # Each streaming update carries a single audio frame.
+ return {"audio": 1}
+
+ def get_max_audio_array_len(self) -> int:
+ # Profile with the largest streaming chunk, the first one: it covers
+ # the prompt prefix (BOS + left padding + delay), later chunks are
+ # ~1 token. get_max_audio_tokens keeps max_model_len so the encoder
+ # cache stays as large as before: each paused session keeps its last
+ # chunk cached until it resumes, and a cache sized to one chunk
+ # deadlocks a few concurrent sessions.
+ tokenizer = self.get_tokenizer()
+ audio_encoder = tokenizer.instruct.audio_encoder
+ prompt_tokens = (
+ tokenizer.instruct.start() + audio_encoder.encode_streaming_tokens()
+ )
+ return len(prompt_tokens) * audio_encoder.audio_config.raw_audio_length_per_tok
+
+
class VoxtralRealtimeBuffer:
- def __init__(self, config: AudioConfig, prompt_tokens: list[int]) -> None:
+ def __init__(
+ self,
+ config: AudioConfig,
+ prompt_tokens: list[int],
+ max_model_len: int | None = None,
+ ) -> None:
self._config = config
+ self._max_model_len = max_model_len

_look_ahead_in_ms = self._config.streaming_look_ahead_ms
_look_back_in_ms = self._config.streaming_look_back_ms
@@ -165,7 +191,16 @@ async def append_tokens(self, tokens: Iterable[int]) -> None:
await self._token_queue.put(token)

async def get_input_stream(self) -> AsyncGenerator[TokensPrompt]:
+ n_tokens = 0
for frame_size, num_tokens in self._generate_frame_size_and_num_tokens():
+ n_tokens += num_tokens
+ if self._max_model_len is not None and n_tokens >= self._max_model_len:
+ logger.warning(
+ "Stopping realtime audio stream: reached max_model_len (%d).",
+ self._max_model_len,
+ )
+ return
+
next_tokens = [await self._token_queue.get() for _ in range(num_tokens)]

audio_arrays: list[np.ndarray] = (
@@ -198,7 +233,7 @@ async def get_input_stream(self) -> AsyncGenerator[TokensPrompt]:

@MULTIMODAL_REGISTRY.register_processor(
VoxtralRealtimeMultiModalProcessor,
- info=VoxtralProcessingInfo,
+ info=VoxtralRealtimeProcessingInfo,
dummy_inputs=VoxtralDummyInputsBuilder,
)
@support_torch_compile
@@ -248,7 +283,9 @@ async def buffer_realtime_audio(
# Get left/right padding audio
left_pad, right_pad = audio_encoder.get_padding_audio()

- buffer = VoxtralRealtimeBuffer(config, prompt_tokens)
+ buffer = VoxtralRealtimeBuffer(
+ config, prompt_tokens, max_model_len=model_config.max_model_len
+ )

# Feed audio with padding into buffer in background
async def feed_audio():
23 changes: 23 additions & 0 deletions container/templates/vllm_runtime.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,29 @@ RUN --mount=type=cache,id=uv-root-{{ context.dynamo.uv_version }},target=/root/.

{% endif %}

{% if device == "cuda" %}
# Backport upstream vLLM fixes; runs after the Omni layer so nothing reinstalls vLLM.
# A vLLM bump that already has a fix fails here (reversed patch); delete that patch.
RUN --mount=type=bind,source=./container/deps/vllm/patches,target=/tmp/vllm_patches \
set -eux; \
VLLM_SITE="$({{ python_executable }} -c 'import os, vllm; print(os.path.dirname(os.path.dirname(vllm.__file__)))')"; \
PATCH_INSTALLED=0; \
if ! command -v patch >/dev/null 2>&1; then \
apt-get update; \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends patch; \
PATCH_INSTALLED=1; \
fi; \
set -- /tmp/vllm_patches/*.patch; \
[ -e "$1" ] || { echo "ERROR: no vLLM patches found in container/deps/vllm/patches" >&2; exit 1; }; \
for p in "$@"; do \
patch --forward --fuzz=0 --batch -p1 -d "${VLLM_SITE}" < "${p}"; \
done; \
if [ "${PATCH_INSTALLED}" = "1" ]; then \
DEBIAN_FRONTEND=noninteractive apt-get purge -y patch; \
rm -rf /var/lib/apt/lists/*; \
fi
{% endif %}

{% if device == "xpu" %}
RUN apt-get update && \
apt-get install -y --no-install-recommends --fix-missing \
Expand Down
Loading