diff --git a/container/deps/vllm/patches/0001-voxtral-realtime-processing-info-vllm-45022.patch b/container/deps/vllm/patches/0001-voxtral-realtime-processing-info-vllm-45022.patch new file mode 100644 index 000000000000..16bca6ca1439 --- /dev/null +++ b/container/deps/vllm/patches/0001-voxtral-realtime-processing-info-vllm-45022.patch @@ -0,0 +1,92 @@ +From 2186b89a9a02fac11fa0e274f3201b9812dbf65c Mon Sep 17 00:00:00 2001 +From: Damien Laine +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 +Co-authored-by: Cyrus Leung +--- +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(): diff --git a/container/templates/vllm_runtime.Dockerfile b/container/templates/vllm_runtime.Dockerfile index 1264205eb85a..f11360a2d51b 100644 --- a/container/templates/vllm_runtime.Dockerfile +++ b/container/templates/vllm_runtime.Dockerfile @@ -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 \