forked from Karylab-cklius/vllm
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4417044fc | ||
|
|
c44e191b01 | ||
|
|
5b14019576 | ||
|
|
dad7a6383b | ||
|
|
5b29c958c7 | ||
|
|
df2735ea2e | ||
|
|
32e657e689 | ||
|
|
ad5d29db70 |
@@ -369,7 +369,7 @@ export HF_TOKEN ZE_AFFINITY_MASK
|
||||
-e CMDS \
|
||||
--name "${container_name}" \
|
||||
"${IMAGE}" \
|
||||
bash -c 'set -e; source /opt/intel/oneapi/setvars.sh --force; source /opt/intel/oneapi/ccl/2021.15/env/vars.sh --force; echo "ZE_AFFINITY_MASK is ${ZE_AFFINITY_MASK:-}"; eval "$CMDS"' \
|
||||
bash -c 'set -e; echo "ZE_AFFINITY_MASK is ${ZE_AFFINITY_MASK:-}"; eval "$CMDS"' \
|
||||
>/dev/null
|
||||
} 9>/tmp/docker-pull.lock
|
||||
|
||||
|
||||
+2
-35
@@ -63,21 +63,6 @@ RUN apt-get update -y && \
|
||||
python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add oneAPI repo, pin oneAPI to 2025.3, then install pinned packages in one layer.
|
||||
RUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" | tee /etc/apt/sources.list.d/oneAPI.list && \
|
||||
printf '%s\n' \
|
||||
'Package: intel-oneapi-* intel-deep-learning-essentials* intel-pti*' \
|
||||
'Pin: version 2025.3*' \
|
||||
'Pin-Priority: 1001' \
|
||||
> /etc/apt/preferences.d/oneapi-2025.3.pref && \
|
||||
apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
intel-oneapi-compiler-dpcpp-cpp-2025.3 \
|
||||
intel-oneapi-mkl-devel-2025.3 \
|
||||
intel-oneapi-dnnl-devel-2025.3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install UMD
|
||||
RUN mkdir neo && \
|
||||
cd neo && \
|
||||
@@ -100,22 +85,6 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||
&& uv venv --python ${PYTHON_VERSION} --seed ${VIRTUAL_ENV}
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
# This oneccl contains the BMG support which is not the case for default version of oneapi 2025.3.
|
||||
ARG ONECCL_INSTALLER="intel-oneccl-2021.15.9.14_offline.sh"
|
||||
RUN wget "https://github.com/uxlfoundation/oneCCL/releases/download/2021.15.9/${ONECCL_INSTALLER}" && \
|
||||
bash "${ONECCL_INSTALLER}" -a --silent --eula accept && \
|
||||
rm "${ONECCL_INSTALLER}" && \
|
||||
echo "source /opt/intel/oneapi/setvars.sh --force" >> /root/.bashrc && \
|
||||
echo "source /opt/intel/oneapi/ccl/2021.15/env/vars.sh --force" >> /root/.bashrc && \
|
||||
rm -f /opt/intel/oneapi/ccl/latest && \
|
||||
ln -s /opt/intel/oneapi/ccl/2021.15 /opt/intel/oneapi/ccl/latest && \
|
||||
printf '%s\n' \
|
||||
'/opt/intel/oneapi/ccl/2021.15/lib' \
|
||||
'/opt/intel/oneapi/mpi/2021.15/lib' \
|
||||
'/opt/intel/oneapi/compiler/2025.3/lib' \
|
||||
> /etc/ld.so.conf.d/oneapi-ccl.conf && \
|
||||
ldconfig
|
||||
|
||||
SHELL ["bash", "-c"]
|
||||
CMD ["bash", "-c", "source /root/.bashrc && exec bash"]
|
||||
|
||||
@@ -135,8 +104,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip install --upgrade pip
|
||||
|
||||
|
||||
|
||||
ENV LD_LIBRARY_PATH=/opt/intel/oneapi/ccl/2021.15/lib:/opt/intel/oneapi/mpi/2021.15/lib:/opt/intel/oneapi/compiler/2025.3/lib:/usr/local/lib
|
||||
ENV LD_LIBRARY_PATH=/opt/venv/lib:/usr/local/lib
|
||||
CMD ["/bin/bash"]
|
||||
|
||||
######################### UCX + NIXL BUILD STAGE #########################
|
||||
@@ -216,8 +184,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip install -r /workspace/vllm/requirements/xpu.txt && \
|
||||
uv pip install --no-build-isolation -r /workspace/vllm/requirements/test/xpu.txt && \
|
||||
uv pip uninstall triton triton-xpu && \
|
||||
uv pip install triton-xpu==3.7.1 && \
|
||||
uv pip uninstall oneccl oneccl-devel
|
||||
uv pip install triton-xpu==3.7.2
|
||||
|
||||
# Keep source-dependent layers near the end so frequent code-only changes
|
||||
# don't invalidate heavy dependency and UCX/NIXL layers.
|
||||
|
||||
@@ -42,12 +42,12 @@ pip install -v -r requirements/xpu.txt
|
||||
|
||||
```bash
|
||||
pip uninstall -y triton triton-xpu
|
||||
pip install triton-xpu==3.7.1 --extra-index-url https://download.pytorch.org/whl/xpu
|
||||
pip install triton-xpu==3.7.2 --extra-index-url https://download.pytorch.org/whl/xpu
|
||||
```
|
||||
|
||||
!!! note
|
||||
- `triton` (without suffix) is for NVIDIA GPUs only. On XPU, using it instead of `triton-xpu` can cause correctness or runtime issues.
|
||||
- For torch 2.12 (the version used in `requirements/xpu.txt`), the matching package is `triton-xpu==3.7.1`. If you use a different version of torch, check the corresponding `triton-xpu` version in [docker/Dockerfile.xpu](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.xpu).
|
||||
- For torch 2.13 (the version used in `requirements/xpu.txt`), the matching package is `triton-xpu==3.7.2`. If you use a different version of torch, check the corresponding `triton-xpu` version in [docker/Dockerfile.xpu](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.xpu).
|
||||
|
||||
- Finally, build and install vLLM XPU backend:
|
||||
|
||||
|
||||
+26
-24
@@ -140,7 +140,7 @@ docopt==0.6.2
|
||||
# via num2words
|
||||
docstring-parser==0.18.0
|
||||
# via anthropic
|
||||
dpcpp-cpp-rt==2025.3.2
|
||||
dpcpp-cpp-rt==2026.0.0
|
||||
# via
|
||||
# onemkl-sycl-blas
|
||||
# onemkl-sycl-dft
|
||||
@@ -253,27 +253,27 @@ ijson==3.5.0
|
||||
# via -r requirements/test/../common.txt
|
||||
imageio==2.37.3
|
||||
# via scikit-image
|
||||
impi-rt==2021.17.2
|
||||
impi-rt==2021.18.0
|
||||
# via
|
||||
# oneccl
|
||||
# torch
|
||||
iniconfig==2.3.0
|
||||
# via pytest
|
||||
intel-cmplr-lib-rt==2025.3.2
|
||||
intel-cmplr-lib-rt==2026.0.0
|
||||
# via
|
||||
# intel-sycl-rt
|
||||
# torch
|
||||
intel-cmplr-lib-ur==2025.3.2
|
||||
intel-cmplr-lib-ur==2026.0.0
|
||||
# via
|
||||
# intel-openmp
|
||||
# intel-sycl-rt
|
||||
# torch
|
||||
intel-cmplr-lic-rt==2025.3.2
|
||||
intel-cmplr-lic-rt==2026.0.0
|
||||
# via
|
||||
# intel-opencl-rt
|
||||
# intel-sycl-rt
|
||||
# torch
|
||||
intel-opencl-rt==2025.3.2
|
||||
intel-opencl-rt==2026.0.0
|
||||
# via
|
||||
# dpcpp-cpp-rt
|
||||
# onemkl-sycl-blas
|
||||
@@ -282,14 +282,14 @@ intel-opencl-rt==2025.3.2
|
||||
# onemkl-sycl-rng
|
||||
# onemkl-sycl-sparse
|
||||
# torch
|
||||
intel-openmp==2025.3.2
|
||||
intel-openmp==2026.0.0
|
||||
# via
|
||||
# dpcpp-cpp-rt
|
||||
# mkl
|
||||
# torch
|
||||
intel-pti==0.16.0
|
||||
intel-pti==0.17.0
|
||||
# via torch
|
||||
intel-sycl-rt==2025.3.2
|
||||
intel-sycl-rt==2026.0.0
|
||||
# via
|
||||
# dpcpp-cpp-rt
|
||||
# oneccl
|
||||
@@ -378,7 +378,7 @@ mistral-common==1.11.5
|
||||
# -c requirements/common.txt
|
||||
# -r requirements/test/../common.txt
|
||||
# -r requirements/test/xpu.in
|
||||
mkl==2025.3.1
|
||||
mkl==2026.0.0
|
||||
# via
|
||||
# onemkl-sycl-blas
|
||||
# onemkl-sycl-dft
|
||||
@@ -453,28 +453,28 @@ numpy==2.2.6
|
||||
# torchvision
|
||||
# transformers
|
||||
# xgrammar
|
||||
oneccl==2021.17.2
|
||||
oneccl==2022.0.0
|
||||
# via
|
||||
# oneccl-devel
|
||||
# torch
|
||||
oneccl-devel==2021.17.2
|
||||
oneccl-devel==2022.0.0
|
||||
# via torch
|
||||
onemkl-license==2025.3.1
|
||||
onemkl-license==2026.0.0
|
||||
# via
|
||||
# mkl
|
||||
# torch
|
||||
onemkl-sycl-blas==2025.3.1
|
||||
onemkl-sycl-blas==2026.0.0
|
||||
# via
|
||||
# onemkl-sycl-lapack
|
||||
# onemkl-sycl-sparse
|
||||
# torch
|
||||
onemkl-sycl-dft==2025.3.1
|
||||
onemkl-sycl-dft==2026.0.0
|
||||
# via torch
|
||||
onemkl-sycl-lapack==2025.3.1
|
||||
onemkl-sycl-lapack==2026.0.0
|
||||
# via torch
|
||||
onemkl-sycl-rng==2025.3.1
|
||||
onemkl-sycl-rng==2026.0.0
|
||||
# via torch
|
||||
onemkl-sycl-sparse==2025.3.1
|
||||
onemkl-sycl-sparse==2026.0.0
|
||||
# via torch
|
||||
openai==2.44.0
|
||||
# via
|
||||
@@ -719,6 +719,8 @@ pyyaml==6.0.3
|
||||
# timm
|
||||
# transformers
|
||||
# uvicorn
|
||||
pyzes==0.1.1
|
||||
# via torch
|
||||
pyzmq==27.1.0
|
||||
# via
|
||||
# -c requirements/common.txt
|
||||
@@ -871,14 +873,14 @@ tabledata==1.3.4
|
||||
# via pytablewriter
|
||||
tabulate==0.10.0
|
||||
# via sacrebleu
|
||||
tbb==2022.3.1
|
||||
tbb==2023.0.0
|
||||
# via
|
||||
# intel-opencl-rt
|
||||
# mkl
|
||||
# torch
|
||||
tblib==3.1.0
|
||||
# via -r requirements/test/xpu.in
|
||||
tcmlib==1.4.1
|
||||
tcmlib==1.5.0
|
||||
# via
|
||||
# tbb
|
||||
# torch
|
||||
@@ -910,7 +912,7 @@ tokenizers==0.22.2
|
||||
# -c requirements/common.txt
|
||||
# -r requirements/test/../common.txt
|
||||
# transformers
|
||||
torch==2.12.0+xpu
|
||||
torch==2.13.0+xpu
|
||||
# via
|
||||
# -c requirements/xpu.txt
|
||||
# accelerate
|
||||
@@ -920,7 +922,7 @@ torch==2.12.0+xpu
|
||||
# timm
|
||||
# torchvision
|
||||
# xgrammar
|
||||
torchvision==0.27.0+xpu
|
||||
torchvision==0.28.0+xpu
|
||||
# via timm
|
||||
tqdm==4.67.3
|
||||
# via
|
||||
@@ -946,7 +948,7 @@ transformers==5.14.1
|
||||
# xgrammar
|
||||
triton==3.7.1
|
||||
# via xgrammar
|
||||
triton-xpu==3.7.1
|
||||
triton-xpu==3.7.2
|
||||
# via torch
|
||||
typepy==1.3.4
|
||||
# via
|
||||
@@ -1001,7 +1003,7 @@ typing-inspection==0.4.2
|
||||
# mcp
|
||||
# pydantic
|
||||
# pydantic-settings
|
||||
umf==1.0.3
|
||||
umf==1.1.0
|
||||
# via
|
||||
# intel-cmplr-lib-ur
|
||||
# torch
|
||||
|
||||
@@ -12,10 +12,10 @@ jinja2>=3.1.6
|
||||
datasets # for benchmark scripts
|
||||
numba == 0.65.0 # Required for N-gram speculative decoding
|
||||
--extra-index-url=https://download.pytorch.org/whl/xpu
|
||||
torch==2.12.0
|
||||
torch==2.13.0
|
||||
torchaudio
|
||||
torchvision
|
||||
torchcodec >= 0.14 # Required for the torchcodec video decoding backend
|
||||
|
||||
auto_round_lib==0.14.1
|
||||
vllm_xpu_kernels @ https://github.com/vllm-project/vllm-xpu-kernels/releases/download/v0.1.11.1/vllm_xpu_kernels-0.1.11.1-cp38-abi3-manylinux_2_28_x86_64.whl
|
||||
auto_round_lib==0.14.2
|
||||
vllm_xpu_kernels @ https://github.com/vllm-project/vllm-xpu-kernels/releases/download/v0.1.12/vllm_xpu_kernels-0.1.12-cp38-abi3-manylinux_2_28_x86_64.whl
|
||||
|
||||
@@ -57,6 +57,7 @@ impl HfChatBackend {
|
||||
processor_config: files.processor_config_path.as_deref(),
|
||||
},
|
||||
tokenizer.clone(),
|
||||
options.limit_mm_per_prompt.clone(),
|
||||
)?
|
||||
};
|
||||
let multimodal_render_info = resolve_multimodal_render_info(multimodal_model_info.as_ref());
|
||||
@@ -231,6 +232,7 @@ mod tests {
|
||||
chat_template_content_format: Default::default(),
|
||||
chat_template: None,
|
||||
default_chat_template_kwargs: HashMap::new(),
|
||||
limit_mm_per_prompt: HashMap::new(),
|
||||
},
|
||||
test_tokenizer(),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde_json::Value;
|
||||
use vllm_text::{DynTextBackend, TextBackend};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::multimodal::MultimodalModelInfo;
|
||||
use crate::multimodal::{MmLimitPerPrompt, MultimodalModelInfo};
|
||||
use crate::output::DynChatOutputProcessor;
|
||||
use crate::renderer::DynChatRenderer;
|
||||
use crate::request::ChatRequest;
|
||||
@@ -74,6 +74,9 @@ pub struct LoadModelBackendsOptions {
|
||||
/// Optional server-default keyword arguments merged into every
|
||||
/// chat-template render before request-level `chat_template_kwargs`.
|
||||
pub default_chat_template_kwargs: HashMap<String, Value>,
|
||||
/// Maximum number of input items allowed per prompt for each modality.
|
||||
/// Unspecified modalities are unlimited.
|
||||
pub limit_mm_per_prompt: MmLimitPerPrompt,
|
||||
}
|
||||
|
||||
/// Shared backends loaded from a model id.
|
||||
|
||||
@@ -23,6 +23,8 @@ pub enum Error {
|
||||
UnsupportedMultimodalContent(&'static str),
|
||||
#[error("`{modality}` input is not supported by this model")]
|
||||
UnsupportedModality { modality: String },
|
||||
#[error("At most {limit} {modality}(s) may be provided in one prompt.")]
|
||||
MmLimitExceeded { modality: String, limit: usize },
|
||||
#[error("multimodal preprocessing error: {0}")]
|
||||
Multimodal(#[message] String),
|
||||
#[error("{kind} parsing is not available for model `{model_id}`")]
|
||||
@@ -87,7 +89,8 @@ impl Error {
|
||||
Self::Text(error) => error.is_request_validation_error(),
|
||||
Self::UnsupportedMultimodalRenderer
|
||||
| Self::UnsupportedMultimodalContent(_)
|
||||
| Self::UnsupportedModality { .. } => true,
|
||||
| Self::UnsupportedModality { .. }
|
||||
| Self::MmLimitExceeded { .. } => true,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! Raw media stays above `vllm-text`; this module lowers it into token IDs and
|
||||
//! opaque tensor payloads before the request is handed to text generation.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
@@ -24,6 +24,7 @@ use llm_multimodal::{
|
||||
PromptReplacement, Tokenizer as TokenResolver, TrackedMedia, VideoClip, VisionPreProcessor,
|
||||
VisionProcessorRegistry,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror_ext::AsReport as _;
|
||||
use tracing::warn;
|
||||
use vllm_engine_core_client::protocol::dtype::ModelDtype;
|
||||
@@ -52,6 +53,71 @@ pub struct MultimodalModelInfo {
|
||||
video: Option<ModalitySupport>,
|
||||
audio: Option<AudioModalitySupport>,
|
||||
media_connector: Arc<MediaConnector>,
|
||||
/// Maximum number of input items allowed per prompt for each modality.
|
||||
limit_mm_per_prompt: MmLimitPerPrompt,
|
||||
}
|
||||
|
||||
/// Per-modality item-count limits configured by `--limit-mm-per-prompt`.
|
||||
///
|
||||
/// Modalities absent from the map are unlimited.
|
||||
pub type MmLimitPerPrompt = HashMap<MmLimitModality, MmLimitSpec>;
|
||||
|
||||
/// Modalities that `--limit-mm-per-prompt` can be keyed by.
|
||||
///
|
||||
/// Closed on purpose: these are exactly the keys Python accepts, per
|
||||
/// `MultiModalDummyOptionsBuiltins` in `vllm/config/multimodal.py`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MmLimitModality {
|
||||
Image,
|
||||
Audio,
|
||||
Video,
|
||||
}
|
||||
|
||||
impl MmLimitModality {
|
||||
/// The wire name, matching Python's modality strings.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Image => "image",
|
||||
Self::Audio => "audio",
|
||||
Self::Video => "video",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One modality's limit, in either of the two shapes Python accepts.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
untagged,
|
||||
expecting = "an item count, or an object with an optional `count` field"
|
||||
)]
|
||||
pub enum MmLimitSpec {
|
||||
/// Legacy form: `"image": 16`
|
||||
Count(usize),
|
||||
|
||||
/// Configurable form:
|
||||
/// `"video": {"count": 1, "num_frames": 32}`
|
||||
Options {
|
||||
/// Absent means unlimited, matching an absent modality.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
count: Option<usize>,
|
||||
|
||||
/// Preserve Python-owned options for forwarding. Never interpreted
|
||||
/// here: they size the engine's dummy-profiling encoder cache, which
|
||||
/// has no Rust counterpart.
|
||||
#[serde(flatten)]
|
||||
extra: BTreeMap<String, serde_json::Value>,
|
||||
},
|
||||
}
|
||||
|
||||
impl MmLimitSpec {
|
||||
/// The configured item count, or `None` when this modality is unlimited.
|
||||
pub fn count(&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::Count(count) => Some(*count),
|
||||
Self::Options { count, .. } => *count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Model metadata and tokenizer access shared by all multimodal specs.
|
||||
@@ -287,6 +353,7 @@ impl MultimodalModelInfo {
|
||||
model_type: Option<String>,
|
||||
files: MultimodalConfigFiles<'_>,
|
||||
tokenizer: DynTokenizer,
|
||||
limit_mm_per_prompt: MmLimitPerPrompt,
|
||||
) -> Result<Option<Self>> {
|
||||
let config = match files.config {
|
||||
Some(path) => {
|
||||
@@ -319,7 +386,12 @@ impl MultimodalModelInfo {
|
||||
tokenizer: TokenizerResolver(tokenizer),
|
||||
};
|
||||
|
||||
Self::from_loaded(context, preprocessor_config, video_preprocessor_config)
|
||||
Self::from_loaded(
|
||||
context,
|
||||
preprocessor_config,
|
||||
video_preprocessor_config,
|
||||
limit_mm_per_prompt,
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve multimodal support from an assembled context and parsed
|
||||
@@ -328,6 +400,7 @@ impl MultimodalModelInfo {
|
||||
context: MultimodalModelContext,
|
||||
preprocessor_config: PreProcessorConfig,
|
||||
video_preprocessor_config: PreProcessorConfig,
|
||||
limit_mm_per_prompt: MmLimitPerPrompt,
|
||||
) -> Result<Option<Self>> {
|
||||
let (image, video) = Self::resolve_vision_lanes(
|
||||
&context,
|
||||
@@ -356,6 +429,7 @@ impl MultimodalModelInfo {
|
||||
video,
|
||||
audio,
|
||||
media_connector,
|
||||
limit_mm_per_prompt,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -567,7 +641,55 @@ fn input_audio_data_url(data: &str, format: Option<&str>) -> Result<String> {
|
||||
Ok(format!("data:{mime_type};base64,{data}"))
|
||||
}
|
||||
|
||||
/// The modality a content part counts against, or `None` for plain text.
|
||||
///
|
||||
/// Embedding inputs share their base modality's budget rather than getting one
|
||||
/// of their own, matching Python's `modality.replace("_embeds", "")` in
|
||||
/// `vllm/entrypoints/chat_utils.py`.
|
||||
fn media_part_limit_modality(part: &MediaContentPart) -> Option<MmLimitModality> {
|
||||
match part {
|
||||
MediaContentPart::Text { .. } => None,
|
||||
MediaContentPart::ImageUrl { .. }
|
||||
| MediaContentPart::ImageData { .. }
|
||||
| MediaContentPart::ImageEmbeds { .. } => Some(MmLimitModality::Image),
|
||||
MediaContentPart::AudioUrl { .. } | MediaContentPart::AudioData { .. } => {
|
||||
Some(MmLimitModality::Audio)
|
||||
}
|
||||
MediaContentPart::VideoUrl { .. } | MediaContentPart::VideoData { .. } => {
|
||||
Some(MmLimitModality::Video)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MultimodalModelInfo {
|
||||
/// Reject requests exceeding `--limit-mm-per-prompt`'s configured
|
||||
/// per-modality item count, before any fetch/decode work is spent on them.
|
||||
///
|
||||
/// Modalities without a configured count are unlimited.
|
||||
fn validate_mm_limits(&self, media_parts: &[MediaContentPart]) -> Result<()> {
|
||||
let mut counts: HashMap<MmLimitModality, usize> = HashMap::new();
|
||||
for part in media_parts {
|
||||
if let Some(modality) = media_part_limit_modality(part) {
|
||||
*counts.entry(modality).or_default() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (modality, count) in counts {
|
||||
let Some(limit) = self.limit_mm_per_prompt.get(&modality).and_then(MmLimitSpec::count)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if count > limit {
|
||||
return Err(Error::MmLimitExceeded {
|
||||
modality: modality.as_str().to_string(),
|
||||
limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run media fetch, per-modality preprocessing, prompt expansion, and
|
||||
/// feature build.
|
||||
///
|
||||
@@ -584,8 +706,7 @@ impl MultimodalModelInfo {
|
||||
if media_parts_len == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
// TODO: enforce per-modality item-count limits, aligned with the
|
||||
// engine's `--limit-mm-per-prompt` semantics.
|
||||
self.validate_mm_limits(&media_parts)?;
|
||||
let fetched = self.fetch_media(media_parts).await?;
|
||||
|
||||
let mut prepared = Vec::new();
|
||||
@@ -753,10 +874,11 @@ mod tests {
|
||||
.with_regular_token("<|video_pad|>", QWEN3_VIDEO_PAD_ID)
|
||||
}
|
||||
|
||||
fn test_info(
|
||||
fn test_info_with_limits(
|
||||
model_type: &str,
|
||||
config: serde_json::Value,
|
||||
tokenizer: TestTokenizer,
|
||||
limit_mm_per_prompt: MmLimitPerPrompt,
|
||||
) -> MultimodalModelInfo {
|
||||
let context = MultimodalModelContext {
|
||||
model_id: format!("{model_type}-test"),
|
||||
@@ -769,11 +891,20 @@ mod tests {
|
||||
context,
|
||||
PreProcessorConfig::default(),
|
||||
PreProcessorConfig::default(),
|
||||
limit_mm_per_prompt,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| panic!("{model_type} multimodal support should resolve"))
|
||||
}
|
||||
|
||||
fn test_info(
|
||||
model_type: &str,
|
||||
config: serde_json::Value,
|
||||
tokenizer: TestTokenizer,
|
||||
) -> MultimodalModelInfo {
|
||||
test_info_with_limits(model_type, config, tokenizer, HashMap::new())
|
||||
}
|
||||
|
||||
fn llama4_info() -> MultimodalModelInfo {
|
||||
let config = serde_json::json!({
|
||||
"model_type": "llama4",
|
||||
@@ -783,16 +914,19 @@ mod tests {
|
||||
test_info("llama4", config, llama4_tokenizer())
|
||||
}
|
||||
|
||||
pub(super) fn qwen3_vl_info() -> MultimodalModelInfo {
|
||||
let config = serde_json::json!({
|
||||
fn qwen3_vl_config() -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"model_type": "qwen3_vl",
|
||||
"image_token_id": QWEN3_IMAGE_PAD_ID,
|
||||
"video_token_id": QWEN3_VIDEO_PAD_ID,
|
||||
"vision_start_token_id": 151652,
|
||||
"vision_end_token_id": 151653,
|
||||
"vision_config": {"patch_size": 16}
|
||||
});
|
||||
test_info("qwen3_vl", config, qwen3_vl_tokenizer())
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn qwen3_vl_info() -> MultimodalModelInfo {
|
||||
test_info("qwen3_vl", qwen3_vl_config(), qwen3_vl_tokenizer())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -853,4 +987,123 @@ mod tests {
|
||||
);
|
||||
assert!(input_audio_data_url("AAAA", Some("flac")).is_err());
|
||||
}
|
||||
|
||||
fn image_url_part() -> MediaContentPart {
|
||||
MediaContentPart::ImageUrl {
|
||||
url: "https://example.com/image.png".to_string(),
|
||||
detail: None,
|
||||
uuid: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn qwen3_vl_info_with_limits(limit_mm_per_prompt: MmLimitPerPrompt) -> MultimodalModelInfo {
|
||||
test_info_with_limits(
|
||||
"qwen3_vl",
|
||||
qwen3_vl_config(),
|
||||
qwen3_vl_tokenizer(),
|
||||
limit_mm_per_prompt,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_mm_limits_ignores_text_parts() {
|
||||
let info = qwen3_vl_info();
|
||||
let parts = vec![
|
||||
MediaContentPart::Text {
|
||||
text: "hello".to_string(),
|
||||
},
|
||||
MediaContentPart::Text {
|
||||
text: "world".to_string(),
|
||||
},
|
||||
];
|
||||
assert!(info.validate_mm_limits(&parts).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_mm_limits_leaves_unconfigured_modalities_unlimited() {
|
||||
let info = qwen3_vl_info();
|
||||
let parts: Vec<_> = std::iter::repeat_with(image_url_part).take(1_000).collect();
|
||||
|
||||
assert!(info.validate_mm_limits(&parts).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_mm_limits_enforces_configured_limit_at_the_boundary() {
|
||||
let info = qwen3_vl_info_with_limits(HashMap::from([(
|
||||
MmLimitModality::Image,
|
||||
MmLimitSpec::Count(1),
|
||||
)]));
|
||||
assert!(info.validate_mm_limits(&[image_url_part()]).is_ok());
|
||||
|
||||
let error = info.validate_mm_limits(&[image_url_part(), image_url_part()]).unwrap_err();
|
||||
assert_eq!(
|
||||
error.to_report_string(),
|
||||
"At most 1 image(s) may be provided in one prompt."
|
||||
);
|
||||
// Confirms the HTTP-mapping bug found during implementation stays fixed:
|
||||
// this must map to 400, not 500.
|
||||
assert!(error.is_request_validation_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_mm_limits_counts_image_embeds_against_the_image_limit() {
|
||||
let info = qwen3_vl_info_with_limits(HashMap::from([(
|
||||
MmLimitModality::Image,
|
||||
MmLimitSpec::Count(1),
|
||||
)]));
|
||||
let image_embeds_part = MediaContentPart::ImageEmbeds {
|
||||
payload: serde_json::Value::String("AAAA".to_string()),
|
||||
uuid: None,
|
||||
};
|
||||
|
||||
let error = info.validate_mm_limits(&[image_url_part(), image_embeds_part]).unwrap_err();
|
||||
assert_eq!(
|
||||
error.to_report_string(),
|
||||
"At most 1 image(s) may be provided in one prompt."
|
||||
);
|
||||
}
|
||||
|
||||
/// An options object without a `count` carries only profiling keys, which
|
||||
/// say nothing about how many items are allowed.
|
||||
#[test]
|
||||
fn validate_mm_limits_treats_a_count_less_options_object_as_unlimited() {
|
||||
let info = qwen3_vl_info_with_limits(HashMap::from([(
|
||||
MmLimitModality::Image,
|
||||
MmLimitSpec::Options {
|
||||
count: None,
|
||||
extra: BTreeMap::from([("width".to_string(), serde_json::json!(512))]),
|
||||
},
|
||||
)]));
|
||||
|
||||
assert!(info.validate_mm_limits(&[image_url_part(), image_url_part()]).is_ok());
|
||||
}
|
||||
|
||||
fn parse_limits(json: &str) -> MmLimitPerPrompt {
|
||||
serde_json::from_str(json).expect("limit map should parse")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limit_map_parses_both_shapes_python_accepts() {
|
||||
let limits = parse_limits(r#"{"image": 16, "video": {"count": 1, "num_frames": 32}}"#);
|
||||
|
||||
assert_eq!(limits[&MmLimitModality::Image].count(), Some(16));
|
||||
assert_eq!(limits[&MmLimitModality::Video].count(), Some(1));
|
||||
assert_eq!(limits.get(&MmLimitModality::Audio), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limit_map_rejects_keys_python_does_not_accept() {
|
||||
assert!(serde_json::from_str::<MmLimitPerPrompt>(r#"{"image_embeds": 1}"#).is_err());
|
||||
}
|
||||
|
||||
/// Managed mode forwards this map back to Python as JSON, where
|
||||
/// `BaseDummyOptions.count` is a non-optional `int` under `extra="forbid"`.
|
||||
/// Emitting `"count": null` would make the engine subprocess fail to start.
|
||||
#[test]
|
||||
fn limit_map_round_trips_without_emitting_a_null_count() {
|
||||
let source = r#"{"video":{"num_frames":32}}"#;
|
||||
let encoded = serde_json::to_string(&parse_limits(source)).expect("map should serialize");
|
||||
|
||||
assert_eq!(encoded, source);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ mod tests {
|
||||
context,
|
||||
PreProcessorConfig::default(),
|
||||
PreProcessorConfig::default(),
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap()
|
||||
.expect("Inkling multimodal support")
|
||||
@@ -126,6 +127,7 @@ mod tests {
|
||||
context,
|
||||
PreProcessorConfig::default(),
|
||||
PreProcessorConfig::default(),
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap()
|
||||
.expect("Qwen3-ASR multimodal support")
|
||||
|
||||
@@ -207,6 +207,7 @@ mod tests {
|
||||
Some("qwen3_vl".to_string()),
|
||||
files,
|
||||
Arc::new(qwen3_vl_tokenizer()),
|
||||
std::collections::HashMap::new(),
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ use serde_with::{DefaultOnNull, OneOrMany, serde_as};
|
||||
use thiserror_ext::AsReport as _;
|
||||
use uuid::Uuid;
|
||||
use vllm_chat::ReasoningParserFactory;
|
||||
use vllm_chat::multimodal::MmLimitPerPrompt;
|
||||
use vllm_engine_core_client::TransportMode;
|
||||
use vllm_managed_engine::ManagedEngineConfig;
|
||||
use vllm_managed_engine::cli::{ManagedEngineArgs, repartition_managed_engine_args};
|
||||
@@ -186,6 +187,17 @@ pub struct SharedRuntimeArgs {
|
||||
#[serde(default)]
|
||||
pub default_chat_template_kwargs: Option<HashMap<String, Value>>,
|
||||
|
||||
/// The maximum number of input items allowed per prompt for each
|
||||
/// modality, as a JSON object (e.g. `{"image": 16, "video": 2}`).
|
||||
///
|
||||
/// Also accepts the engine's configurable form
|
||||
/// (e.g. `{"video": {"count": 1, "num_frames": 32}}`); the extra
|
||||
/// profiling options are forwarded to the engine untouched.
|
||||
/// Unspecified modalities are unlimited.
|
||||
#[arg(long, value_parser = parse_json::<MmLimitPerPrompt>, value_name = "JSON", default_value = "{}")]
|
||||
#[serde(default)]
|
||||
pub limit_mm_per_prompt: MmLimitPerPrompt,
|
||||
|
||||
/// The format to render message content within a chat template.
|
||||
///
|
||||
/// * "auto" detects the format from the template
|
||||
@@ -348,6 +360,18 @@ impl SharedRuntimeArgs {
|
||||
.expect("profiler config serialization should not fail")
|
||||
}
|
||||
|
||||
/// Return the per-modality limits as JSON for managed Python engine
|
||||
/// forwarding, or `None` when nothing is configured.
|
||||
///
|
||||
/// Round-tripping the parsed map rather than the raw argument keeps the
|
||||
/// engine's own profiling options (`num_frames`, `width`, ...) intact.
|
||||
pub fn limit_mm_per_prompt_json(&self) -> Option<String> {
|
||||
(!self.limit_mm_per_prompt.is_empty()).then(|| {
|
||||
serde_json::to_string(&self.limit_mm_per_prompt)
|
||||
.expect("limit-mm-per-prompt serialization should not fail")
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply fallback logic for API key configuration from env variables.
|
||||
fn apply_env_api_key_fallback(&mut self) {
|
||||
if self.api_key.is_empty()
|
||||
@@ -399,6 +423,7 @@ impl SharedRuntimeArgs {
|
||||
language_model_only: self.language_model_only,
|
||||
chat_template: self.chat_template,
|
||||
default_chat_template_kwargs: self.default_chat_template_kwargs,
|
||||
limit_mm_per_prompt: self.limit_mm_per_prompt,
|
||||
chat_template_content_format: self.chat_template_content_format,
|
||||
max_logprobs: self.max_logprobs,
|
||||
api_server_options,
|
||||
@@ -451,6 +476,7 @@ impl SharedRuntimeArgs {
|
||||
language_model_only: self.language_model_only,
|
||||
chat_template: self.chat_template,
|
||||
default_chat_template_kwargs: self.default_chat_template_kwargs,
|
||||
limit_mm_per_prompt: self.limit_mm_per_prompt,
|
||||
chat_template_content_format: self.chat_template_content_format,
|
||||
max_logprobs: self.max_logprobs,
|
||||
api_server_options,
|
||||
@@ -666,6 +692,7 @@ impl ServeArgs {
|
||||
self.runtime.disable_log_stats,
|
||||
self.runtime.shutdown_timeout,
|
||||
handshake_port,
|
||||
self.runtime.limit_mm_per_prompt_json(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ fn serve_args_forward_python_flags_with_separator() {
|
||||
http_timeout_keep_alive: None,
|
||||
chat_template: None,
|
||||
default_chat_template_kwargs: None,
|
||||
limit_mm_per_prompt: {},
|
||||
chat_template_content_format: Auto,
|
||||
enable_log_requests: false,
|
||||
enable_prompt_tokens_details: false,
|
||||
@@ -762,6 +763,7 @@ fn frontend_args_accept_json() {
|
||||
http_timeout_keep_alive: None,
|
||||
chat_template: None,
|
||||
default_chat_template_kwargs: None,
|
||||
limit_mm_per_prompt: {},
|
||||
chat_template_content_format: Auto,
|
||||
enable_log_requests: false,
|
||||
enable_prompt_tokens_details: false,
|
||||
@@ -1117,6 +1119,24 @@ fn frontend_args_json_rejects_malformed_json() {
|
||||
"#]].assert_eq(&error.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_args_reject_unsupported_modality_in_limit_mm_per_prompt() {
|
||||
let error = Cli::try_parse_from([
|
||||
"vllm-rs",
|
||||
"serve",
|
||||
"Qwen/Qwen3-0.6B",
|
||||
"--limit-mm-per-prompt",
|
||||
r#"{"unsupported_modality": 1}"#,
|
||||
])
|
||||
.unwrap_err();
|
||||
|
||||
expect![[r#"
|
||||
error: invalid value '{"unsupported_modality": 1}' for '--limit-mm-per-prompt <JSON>': invalid JSON object: unknown variant `unsupported_modality`, expected one of `image`, `audio`, `video` at line 1 column 23
|
||||
|
||||
For more information, try '--help'.
|
||||
"#]].assert_eq(&error.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_args_reject_flags_before_model() {
|
||||
let error = Cli::try_parse_from(["vllm-rs", "serve", "--python", "python3", "Qwen/Qwen3-0.6B"])
|
||||
@@ -1331,6 +1351,7 @@ fn serve_args_accept_handshake_aliases() {
|
||||
http_timeout_keep_alive: None,
|
||||
chat_template: None,
|
||||
default_chat_template_kwargs: None,
|
||||
limit_mm_per_prompt: {},
|
||||
chat_template_content_format: Auto,
|
||||
enable_log_requests: false,
|
||||
enable_prompt_tokens_details: false,
|
||||
@@ -1474,6 +1495,7 @@ fn serve_frontend_config_uses_dp_address_as_advertised_host() {
|
||||
language_model_only: false,
|
||||
chat_template: None,
|
||||
default_chat_template_kwargs: None,
|
||||
limit_mm_per_prompt: {},
|
||||
chat_template_content_format: Auto,
|
||||
max_logprobs: None,
|
||||
api_server_options: ApiServerOptions {
|
||||
@@ -1558,6 +1580,7 @@ fn serve_frontend_config_keeps_tcp_transport_for_non_local_only_topology() {
|
||||
language_model_only: false,
|
||||
chat_template: None,
|
||||
default_chat_template_kwargs: None,
|
||||
limit_mm_per_prompt: {},
|
||||
chat_template_content_format: Auto,
|
||||
max_logprobs: None,
|
||||
api_server_options: ApiServerOptions {
|
||||
@@ -1660,6 +1683,7 @@ fn frontend_config_uses_external_coordinator_when_coordinator_address_is_present
|
||||
language_model_only: false,
|
||||
chat_template: None,
|
||||
default_chat_template_kwargs: None,
|
||||
limit_mm_per_prompt: {},
|
||||
chat_template_content_format: Auto,
|
||||
max_logprobs: None,
|
||||
api_server_options: ApiServerOptions {
|
||||
|
||||
@@ -299,11 +299,6 @@ pub struct EngineUnsupportedArgs {
|
||||
)]
|
||||
pub kv_sharing_fast_prefill: Option<Unsupported>,
|
||||
|
||||
/// The maximum number of input items and options allowed per
|
||||
/// prompt for each modality.
|
||||
#[arg(long)]
|
||||
pub limit_mm_per_prompt: Option<Unsupported>,
|
||||
|
||||
/// Additional args passed to process media inputs, keyed by modalities.
|
||||
#[arg(long)]
|
||||
pub media_io_kwargs: Option<Unsupported>,
|
||||
|
||||
@@ -91,6 +91,7 @@ impl ManagedEngineArgs {
|
||||
disable_log_stats: bool,
|
||||
shutdown_timeout: u64,
|
||||
handshake_port: u16,
|
||||
limit_mm_per_prompt: Option<String>,
|
||||
) -> ManagedEngineConfig {
|
||||
let mut python_args = self.python_args;
|
||||
// Manually forward some args to the Python engine.
|
||||
@@ -126,6 +127,10 @@ impl ManagedEngineArgs {
|
||||
python_args.push("--data-parallel-size-local".to_string());
|
||||
python_args.push(data_parallel_size_local.to_string());
|
||||
}
|
||||
if let Some(limit_mm_per_prompt) = limit_mm_per_prompt {
|
||||
python_args.push("--limit-mm-per-prompt".to_string());
|
||||
python_args.push(limit_mm_per_prompt);
|
||||
}
|
||||
|
||||
ManagedEngineConfig {
|
||||
python: self.python,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
@@ -70,6 +71,7 @@ async fn main() -> Result<()> {
|
||||
language_model_only: false,
|
||||
chat_template: None,
|
||||
default_chat_template_kwargs: None,
|
||||
limit_mm_per_prompt: HashMap::new(),
|
||||
chat_template_content_format: ChatTemplateContentFormatOption::Auto,
|
||||
max_logprobs: None,
|
||||
api_server_options: ApiServerOptions::default(),
|
||||
|
||||
@@ -10,6 +10,7 @@ use axum::http::{HeaderName, HeaderValue, Method};
|
||||
use educe::Educe;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use vllm_chat::multimodal::MmLimitPerPrompt;
|
||||
use vllm_chat::{ChatTemplateContentFormatOption, ParserSelection, RendererSelection};
|
||||
use vllm_engine_core_client::{CoordinatorMode as EngineCoreCoordinatorMode, TransportMode};
|
||||
|
||||
@@ -184,6 +185,9 @@ pub struct Config {
|
||||
pub chat_template: Option<String>,
|
||||
/// Server-default keyword arguments merged into every chat-template render.
|
||||
pub default_chat_template_kwargs: Option<HashMap<String, Value>>,
|
||||
/// Maximum number of input items allowed per prompt for each modality.
|
||||
/// Unspecified modalities are unlimited.
|
||||
pub limit_mm_per_prompt: MmLimitPerPrompt,
|
||||
/// How to serialize `message.content` for chat-template rendering.
|
||||
pub chat_template_content_format: ChatTemplateContentFormatOption,
|
||||
/// Optional maximum number of top log probabilities accepted by the
|
||||
|
||||
@@ -101,6 +101,7 @@ async fn build_state(config: &Config) -> Result<Arc<AppState>> {
|
||||
.default_chat_template_kwargs
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
limit_mm_per_prompt: config.limit_mm_per_prompt.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -577,6 +577,12 @@ fn render_fake_content(content: &ChatContent, placeholder: &str) -> vllm_chat::R
|
||||
}
|
||||
|
||||
fn qwen_multimodal_model_info() -> vllm_chat::multimodal::MultimodalModelInfo {
|
||||
qwen_multimodal_model_info_with_limits(std::collections::HashMap::new())
|
||||
}
|
||||
|
||||
fn qwen_multimodal_model_info_with_limits(
|
||||
limit_mm_per_prompt: vllm_chat::multimodal::MmLimitPerPrompt,
|
||||
) -> vllm_chat::multimodal::MultimodalModelInfo {
|
||||
let config_path = std::env::temp_dir().join(format!(
|
||||
"vllm-server-qwen-config-{}.json",
|
||||
uuid::Uuid::new_v4()
|
||||
@@ -594,6 +600,7 @@ fn qwen_multimodal_model_info() -> vllm_chat::multimodal::MultimodalModelInfo {
|
||||
..Default::default()
|
||||
},
|
||||
Arc::new(fake_chat_tokenizer()),
|
||||
limit_mm_per_prompt,
|
||||
)
|
||||
.expect("load multimodal info")
|
||||
.expect("qwen multimodal info is registered");
|
||||
@@ -2307,6 +2314,74 @@ async fn non_stream_chat_image_url_reaches_engine_mm_features() {
|
||||
assert_eq!(json["choices"][0]["message"]["content"], "hi");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial]
|
||||
async fn non_stream_chat_rejects_when_image_count_exceeds_limit_mm_per_prompt() {
|
||||
// The request is rejected by `--limit-mm-per-prompt` validation before
|
||||
// ever reaching the engine, so the mock engine task is never awaited.
|
||||
let (chat, _engine_task) = test_models_with_engine_outputs_and_backend(
|
||||
b"engine-openai-mm-limit",
|
||||
default_stream_output_specs(),
|
||||
Arc::new(FakeChatBackend::with_multimodal_model_info(
|
||||
qwen_multimodal_model_info_with_limits(std::collections::HashMap::from([(
|
||||
vllm_chat::multimodal::MmLimitModality::Image,
|
||||
vllm_chat::multimodal::MmLimitSpec::Count(1),
|
||||
)])),
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
let app = build_router(Arc::new(AppState::new(
|
||||
vec!["Qwen/Qwen1.5-0.5B-Chat".to_string()],
|
||||
chat,
|
||||
)));
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.call(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/chat/completions")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"model": "Qwen/Qwen1.5-0.5B-Chat",
|
||||
"stream": false,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "describe "},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("build request"),
|
||||
)
|
||||
.await
|
||||
.expect("call app");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let body = to_bytes(response.into_body(), usize::MAX).await.expect("read body");
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).expect("decode json");
|
||||
assert_eq!(json["error"]["type"], "invalid_request_error");
|
||||
assert_eq!(
|
||||
json["error"]["message"],
|
||||
"At most 1 image(s) may be provided in one prompt."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial]
|
||||
async fn non_stream_chat_includes_logprobs_and_prompt_logprobs() {
|
||||
|
||||
@@ -1122,7 +1122,7 @@ if _is_cuda() or _is_hip():
|
||||
# copying the relevant .py files from the source repository.
|
||||
ext_modules.append(CMakeExtension(name="vllm.triton_kernels", optional=True))
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
if not _is_xpu() and sys.version_info >= (3, 11):
|
||||
ext_modules.append(CMakeExtension(name="vllm.spinloop"))
|
||||
ext_modules.append(CMakeExtension(name="vllm.fs_io_C"))
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Tests for the EAGLE draft ``max_position_embeddings`` override (#48894).
|
||||
|
||||
EAGLE drafts share the target's positional space, but some draft
|
||||
checkpoints (e.g. ``yuhuili/EAGLE3-LLaMA3.1-Instruct-8B``) ship a
|
||||
``max_position_embeddings`` (2048) far smaller than the target's context.
|
||||
That value sizes the draft's rotary ``cos_sin_cache`` while the proposer
|
||||
feeds positions up to the target's ``max_model_len``, so the cache gather
|
||||
goes out of bounds — a device-side assert under torch.compile and silent
|
||||
garbage reads in eager mode. ``SpeculativeConfig`` must raise the draft's
|
||||
value to the target's ``max_model_len``, with a log, for the eagle/eagle3
|
||||
methods only.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
from vllm.config.model import ModelConfig
|
||||
from vllm.config.parallel import ParallelConfig
|
||||
from vllm.config.speculative import SpeculativeConfig
|
||||
|
||||
# All repos are public; only config/tokenizer-config files are fetched.
|
||||
EAGLE3_DRAFT = "yuhuili/EAGLE3-LLaMA3.1-Instruct-8B" # max_position_embeddings=2048
|
||||
LLAMA3_TARGET = "unsloth/Meta-Llama-3.1-8B-Instruct" # max_position_embeddings=131072
|
||||
AR_MODEL = "JackFram/llama-68m" # max_position_embeddings=2048
|
||||
|
||||
_LOGGER = "vllm.config.speculative"
|
||||
_OVERRIDE_MSG = "Overriding draft model max_position_embeddings"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vllm_caplog(caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Make caplog see vLLM logger records (vLLM sets propagate=False)."""
|
||||
monkeypatch.setattr(logging.getLogger("vllm"), "propagate", True)
|
||||
with caplog.at_level(logging.INFO, logger=_LOGGER):
|
||||
yield caplog
|
||||
|
||||
|
||||
def _override_logged(caplog: pytest.LogCaptureFixture) -> bool:
|
||||
return any(_OVERRIDE_MSG in record.getMessage() for record in caplog.records)
|
||||
|
||||
|
||||
@pytest.mark.cpu_test
|
||||
def test_override_raises_smaller_value(vllm_caplog: pytest.LogCaptureFixture):
|
||||
hf_config = PretrainedConfig(max_position_embeddings=2048)
|
||||
SpeculativeConfig._maybe_override_draft_max_position_embeddings(
|
||||
hf_config, target_max_model_len=8192
|
||||
)
|
||||
assert hf_config.max_position_embeddings == 8192
|
||||
assert _override_logged(vllm_caplog)
|
||||
|
||||
|
||||
@pytest.mark.cpu_test
|
||||
def test_override_keeps_sufficient_value(vllm_caplog: pytest.LogCaptureFixture):
|
||||
hf_config = PretrainedConfig(max_position_embeddings=8192)
|
||||
SpeculativeConfig._maybe_override_draft_max_position_embeddings(
|
||||
hf_config, target_max_model_len=8192
|
||||
)
|
||||
assert hf_config.max_position_embeddings == 8192
|
||||
assert not _override_logged(vllm_caplog)
|
||||
|
||||
|
||||
@pytest.mark.cpu_test
|
||||
def test_override_ignores_missing_attribute(vllm_caplog: pytest.LogCaptureFixture):
|
||||
hf_config = PretrainedConfig()
|
||||
hf_config.__dict__.pop("max_position_embeddings", None)
|
||||
SpeculativeConfig._maybe_override_draft_max_position_embeddings(
|
||||
hf_config, target_max_model_len=8192
|
||||
)
|
||||
assert not hasattr(hf_config, "max_position_embeddings")
|
||||
assert not _override_logged(vllm_caplog)
|
||||
|
||||
|
||||
@pytest.mark.cpu_test
|
||||
@pytest.mark.parametrize("method", ["eagle", "eagle3"])
|
||||
def test_eagle_draft_inherits_target_max_model_len(
|
||||
method: str, vllm_caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
target_model_config = ModelConfig(LLAMA3_TARGET)
|
||||
assert target_model_config.max_model_len > 2048
|
||||
speculative_config = SpeculativeConfig(
|
||||
target_model_config=target_model_config,
|
||||
target_parallel_config=ParallelConfig(),
|
||||
model=EAGLE3_DRAFT,
|
||||
method=method,
|
||||
num_speculative_tokens=3,
|
||||
)
|
||||
draft_hf_config = speculative_config.draft_model_config.hf_config
|
||||
assert draft_hf_config.max_position_embeddings == target_model_config.max_model_len
|
||||
assert _override_logged(vllm_caplog)
|
||||
|
||||
|
||||
@pytest.mark.cpu_test
|
||||
def test_independent_draft_model_keeps_its_own_limit(
|
||||
vllm_caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""An independent AR draft may genuinely have a smaller context than the
|
||||
target; its max_position_embeddings must not be resized."""
|
||||
target_model_config = ModelConfig(
|
||||
AR_MODEL, hf_overrides={"max_position_embeddings": 8192}
|
||||
)
|
||||
assert target_model_config.max_model_len == 8192
|
||||
speculative_config = SpeculativeConfig(
|
||||
target_model_config=target_model_config,
|
||||
target_parallel_config=ParallelConfig(),
|
||||
model=AR_MODEL,
|
||||
method="draft_model",
|
||||
num_speculative_tokens=3,
|
||||
)
|
||||
draft_hf_config = speculative_config.draft_model_config.hf_config
|
||||
assert draft_hf_config.max_position_embeddings == 2048
|
||||
assert not _override_logged(vllm_caplog)
|
||||
@@ -27,8 +27,8 @@ from vllm.model_executor.layers.fused_moe.experts.aiter_mxfp8_moe import ( # no
|
||||
_AITER_SWIGLU_BETA,
|
||||
AiterMxfp8Experts,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.experts.mxfp8_native_moe import ( # noqa: E402
|
||||
Mxfp8NativeTritonExperts,
|
||||
from vllm.model_executor.layers.fused_moe.experts.mxfp8_emulation_moe import ( # noqa: E402
|
||||
Mxfp8EmulationTritonExperts,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.modular_kernel import ( # noqa: E402
|
||||
FusedMoEActivationFormat,
|
||||
@@ -150,16 +150,20 @@ def test_explicit_moe_backend_aiter():
|
||||
|
||||
def test_gfx950_picks_aiter():
|
||||
"""Auto-select on real ROCm hardware with flydsl usable -> FlyDSL wins."""
|
||||
with _flydsl_installed(True):
|
||||
# NOTE: Fp8MoeBackend.AITER_MXFP8 does not require VLLM_ROCM_USE_AITER=1
|
||||
with (
|
||||
patch(f"{_AITER_MOD}.current_platform.supports_mx", return_value=True),
|
||||
_flydsl_installed(True),
|
||||
):
|
||||
backend, experts_cls = select_mxfp8_moe_backend(_config())
|
||||
assert backend is Fp8MoeBackend.AITER_MXFP8
|
||||
assert experts_cls is AiterMxfp8Experts
|
||||
|
||||
|
||||
def test_gfx942_picks_triton():
|
||||
def test_gfx942_picks_emulation():
|
||||
"""flydsl unusable (e.g. gfx942, no FlyDSL support) -> native Triton
|
||||
dot_scaled backend wins instead."""
|
||||
with _flydsl_installed(False):
|
||||
with patch(f"{_AITER_MOD}.current_platform.supports_mx", return_value=False):
|
||||
backend, experts_cls = select_mxfp8_moe_backend(_config())
|
||||
assert backend is Fp8MoeBackend.TRITON_MXFP8
|
||||
assert experts_cls is Mxfp8NativeTritonExperts
|
||||
assert backend is Fp8MoeBackend.EMULATION
|
||||
assert experts_cls is Mxfp8EmulationTritonExperts
|
||||
|
||||
@@ -497,6 +497,8 @@ _TEXT_GENERATION_EXAMPLE_MODELS = {
|
||||
"Qwen2MoeForCausalLM": _HfExamplesInfo("Qwen/Qwen1.5-MoE-A2.7B-Chat"),
|
||||
"Qwen3ForCausalLM": _HfExamplesInfo("Qwen/Qwen3-8B"),
|
||||
"Qwen3MoeForCausalLM": _HfExamplesInfo("Qwen/Qwen3-30B-A3B"),
|
||||
"Qwen3_5ForCausalLM": _HfExamplesInfo("codecho/Qwen3.5-0.8B-text-only"),
|
||||
"Qwen3_5MoeForCausalLM": _HfExamplesInfo("codecho/Qwen3.5-35B-A3B-text-only"),
|
||||
"MellumForCausalLM": _HfExamplesInfo("JetBrains/Mellum2-12B-A2.5B-Base"),
|
||||
"Qwen3NextForCausalLM": _HfExamplesInfo(
|
||||
"Qwen/Qwen3-Next-80B-A3B-Instruct",
|
||||
|
||||
@@ -187,3 +187,7 @@ def test_merge_multimodal_embeddings_no_sync():
|
||||
_merge_multimodal_embeddings(
|
||||
inputs_embeds, multimodal_embeddings, is_multimodal
|
||||
)
|
||||
|
||||
|
||||
def test_ci_retry_workflow():
|
||||
pytest.fail("Intentional failure for /ci retry workflow validation")
|
||||
|
||||
@@ -14,6 +14,7 @@ from transformers import AutoVideoProcessor
|
||||
from transformers.video_utils import VideoMetadata
|
||||
|
||||
from vllm.assets.base import get_vllm_public_assets
|
||||
from vllm.models.minimax_m3.common.mm_preprocess import MiniMaxM3VideoBackend
|
||||
from vllm.multimodal.video import (
|
||||
PYNVVIDEOCODEC_DECODER_CACHE_SIZE,
|
||||
PYNVVIDEOCODEC_VIDEO_BACKEND,
|
||||
@@ -388,6 +389,12 @@ def test_cosmos3_edge_uses_qwen3_vl_video_backend():
|
||||
{"fps": 2},
|
||||
id="qwen2_5_vl",
|
||||
),
|
||||
pytest.param(
|
||||
"MiniMaxAI/MiniMax-M3",
|
||||
MiniMaxM3VideoBackend,
|
||||
None,
|
||||
id="minimax_m3_vl",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_video_processor_from_model_repo(
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Tests for SpecDecodeBaseProposer.initialize_attn_backend.
|
||||
|
||||
Block tables are stored at kernel-block granularity, so the proposer's
|
||||
``block_size`` (used for slot-mapping math) must be the kernel block size,
|
||||
not the KV cache manager's block size — the two differ when manager blocks
|
||||
are split for the attention kernel. The value must also be deterministic:
|
||||
``_draft_attn_layer_names`` is a set, whose iteration order varies across
|
||||
processes, so anything derived from iteration order must not leak into
|
||||
``block_size``.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
import vllm.v1.spec_decode.llm_base_proposer as llm_base_proposer
|
||||
from vllm.v1.spec_decode.eagle import EagleProposer
|
||||
|
||||
SCHEDULER_BLOCK_SIZE = 256
|
||||
KERNEL_BLOCK_SIZE = 64
|
||||
|
||||
|
||||
class _FakeAttentionGroup:
|
||||
def __init__(self, backend, layer_names, kv_cache_spec, kv_cache_group_id):
|
||||
self.backend = backend
|
||||
self.layer_names = list(layer_names)
|
||||
self.kv_cache_spec = kv_cache_spec
|
||||
self.kv_cache_group_id = kv_cache_group_id
|
||||
self.kernel_block_size = None
|
||||
|
||||
def create_metadata_builders(self, vllm_config, device, kernel_block_size=None):
|
||||
self.kernel_block_size = kernel_block_size
|
||||
|
||||
def get_metadata_builder(self):
|
||||
return SimpleNamespace(kv_cache_spec=self.kv_cache_spec)
|
||||
|
||||
|
||||
def _make_proposer(
|
||||
monkeypatch: pytest.MonkeyPatch, layer_names: set[str]
|
||||
) -> EagleProposer:
|
||||
fake_layers = {}
|
||||
for name in layer_names:
|
||||
backend = SimpleNamespace(full_cls_name=lambda: "FakeBackend")
|
||||
fake_layers[name] = SimpleNamespace(
|
||||
get_attn_backend=lambda backend=backend: backend
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
llm_base_proposer, "get_layers_from_vllm_config", lambda *a, **k: fake_layers
|
||||
)
|
||||
monkeypatch.setattr(llm_base_proposer, "AttentionGroup", _FakeAttentionGroup)
|
||||
|
||||
proposer = EagleProposer.__new__(EagleProposer)
|
||||
proposer.vllm_config = None
|
||||
proposer.device = None
|
||||
proposer._draft_attn_layer_names = set(layer_names)
|
||||
proposer.kv_cache_gid = -1
|
||||
proposer.draft_attn_groups = []
|
||||
proposer.block_size = -1
|
||||
return proposer
|
||||
|
||||
|
||||
def _make_kv_cache_config(layer_names: set[str]) -> SimpleNamespace:
|
||||
spec = SimpleNamespace(block_size=SCHEDULER_BLOCK_SIZE)
|
||||
group = SimpleNamespace(layer_names=list(layer_names), kv_cache_spec=spec)
|
||||
return SimpleNamespace(kv_cache_groups=[group])
|
||||
|
||||
|
||||
def test_block_size_uses_kernel_block_size(monkeypatch: pytest.MonkeyPatch):
|
||||
"""The proposer's slot-mapping math runs against the kernel-granularity
|
||||
block table, so block_size must come from kernel_block_sizes."""
|
||||
layer_names = {"draft.0.self_attn.attn"}
|
||||
proposer = _make_proposer(monkeypatch, layer_names)
|
||||
|
||||
proposer.initialize_attn_backend(
|
||||
_make_kv_cache_config(layer_names),
|
||||
kernel_block_sizes=[KERNEL_BLOCK_SIZE],
|
||||
)
|
||||
|
||||
assert proposer.block_size == KERNEL_BLOCK_SIZE
|
||||
assert proposer.block_size != SCHEDULER_BLOCK_SIZE
|
||||
# The metadata builder keeps receiving the kernel block size as well.
|
||||
assert proposer.draft_attn_groups[0].kernel_block_size == KERNEL_BLOCK_SIZE
|
||||
|
||||
|
||||
def test_block_size_falls_back_to_kv_cache_spec(monkeypatch: pytest.MonkeyPatch):
|
||||
layer_names = {"draft.0.self_attn.attn"}
|
||||
proposer = _make_proposer(monkeypatch, layer_names)
|
||||
|
||||
proposer.initialize_attn_backend(
|
||||
_make_kv_cache_config(layer_names), kernel_block_sizes=None
|
||||
)
|
||||
|
||||
assert proposer.block_size == SCHEDULER_BLOCK_SIZE
|
||||
|
||||
|
||||
def test_draft_layer_iteration_is_deterministic(monkeypatch: pytest.MonkeyPatch):
|
||||
"""_draft_attn_layer_names is a set; the attention groups built from it
|
||||
must not depend on its (process-random) iteration order."""
|
||||
layer_names = {"draft.c.attn", "draft.a.attn", "draft.b.attn"}
|
||||
expected_order = sorted(layer_names)
|
||||
|
||||
for insertion_order in (expected_order, expected_order[::-1]):
|
||||
proposer = _make_proposer(monkeypatch, set(insertion_order))
|
||||
proposer.initialize_attn_backend(
|
||||
_make_kv_cache_config(set(insertion_order)),
|
||||
kernel_block_sizes=[KERNEL_BLOCK_SIZE],
|
||||
)
|
||||
assert len(proposer.draft_attn_groups) == 1
|
||||
assert proposer.draft_attn_groups[0].layer_names == expected_order
|
||||
assert proposer.block_size == KERNEL_BLOCK_SIZE
|
||||
@@ -2391,17 +2391,6 @@ def topk_softmax(
|
||||
e_score_correction_bias: torch.Tensor | None = None,
|
||||
is_padding: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
if current_platform.is_xpu():
|
||||
# TODO: Remove after vllm-xpu-kernels supports is_padding.
|
||||
torch.ops._moe_C.topk_softmax(
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
token_expert_indices,
|
||||
gating_output,
|
||||
renormalize,
|
||||
e_score_correction_bias,
|
||||
)
|
||||
return
|
||||
torch.ops._moe_C.topk_softmax(
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
@@ -2447,21 +2436,6 @@ def topk_hash_softplus_sqrt(
|
||||
hash_indices_table: torch.Tensor | None = None,
|
||||
is_padding: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
if current_platform.is_xpu():
|
||||
# TODO: Remove after vllm-xpu-kernels supports is_padding.
|
||||
torch.ops._moe_C.topk_softplus_sqrt(
|
||||
topk_weights,
|
||||
topk_indices,
|
||||
token_expert_indices,
|
||||
gating_output,
|
||||
renormalize,
|
||||
routed_scaling_factor,
|
||||
e_score_correction_bias,
|
||||
input_tokens,
|
||||
hash_indices_table,
|
||||
)
|
||||
return
|
||||
|
||||
torch.ops._moe_C.topk_softplus_sqrt(
|
||||
topk_weights,
|
||||
topk_indices,
|
||||
|
||||
@@ -910,6 +910,15 @@ class SpeculativeConfig:
|
||||
f"Unsupported speculative method: '{self.method}'"
|
||||
)
|
||||
|
||||
if self.method in ("eagle", "eagle3"):
|
||||
# EAGLE drafts share the target's positional space; a
|
||||
# draft checkpoint with a smaller max_position_embeddings
|
||||
# than the target under-sizes its rotary cache (#48894).
|
||||
SpeculativeConfig._maybe_override_draft_max_position_embeddings(
|
||||
self.draft_model_config.hf_config,
|
||||
self.target_model_config.max_model_len,
|
||||
)
|
||||
|
||||
# Replace hf_config for EAGLE draft_model
|
||||
if self.method in ("eagle", "eagle3", "dflash"):
|
||||
from vllm.transformers_utils.configs.eagle import EAGLEConfig
|
||||
@@ -1130,6 +1139,40 @@ class SpeculativeConfig:
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _maybe_override_draft_max_position_embeddings(
|
||||
draft_hf_config: PretrainedConfig,
|
||||
target_max_model_len: int,
|
||||
) -> None:
|
||||
"""Raise an EAGLE draft's max_position_embeddings up to the target's.
|
||||
|
||||
The proposer feeds the draft positions up to the target's
|
||||
max_model_len, while max_position_embeddings sizes the draft's
|
||||
rotary cos_sin_cache. A smaller checkpoint value (e.g. 2048 for
|
||||
yuhuili/EAGLE3-LLaMA3.1-Instruct-8B) makes that cache gather go
|
||||
out of bounds (#48894).
|
||||
|
||||
Args:
|
||||
draft_hf_config: The draft model's HF config, mutated in place.
|
||||
target_max_model_len: The target model's max_model_len.
|
||||
"""
|
||||
draft_max_position_embeddings = getattr(
|
||||
draft_hf_config, "max_position_embeddings", None
|
||||
)
|
||||
if (
|
||||
draft_max_position_embeddings is None
|
||||
or draft_max_position_embeddings >= target_max_model_len
|
||||
):
|
||||
return
|
||||
logger.info(
|
||||
"Overriding draft model max_position_embeddings from %d to the "
|
||||
"target model's max_model_len (%d); EAGLE drafts share the "
|
||||
"target's positional space.",
|
||||
draft_max_position_embeddings,
|
||||
target_max_model_len,
|
||||
)
|
||||
draft_hf_config.max_position_embeddings = target_max_model_len
|
||||
|
||||
@staticmethod
|
||||
def _verify_and_get_draft_tp(
|
||||
target_parallel_config: ParallelConfig,
|
||||
|
||||
@@ -275,3 +275,15 @@ class ECConnectorBase(ABC):
|
||||
get_finished().
|
||||
"""
|
||||
return False, None
|
||||
|
||||
def has_pending_push_work(self) -> bool:
|
||||
"""Return True if the connector has push-mode work that requires
|
||||
the engine main loop to keep stepping (e.g. for EPD,
|
||||
Producer has push work when Xfer is in progress - Consumer
|
||||
is reading it).
|
||||
This mirrors exactly the KV Connector's has_pending_push_work().
|
||||
|
||||
Connectors that don't implement push-based EC transfer should
|
||||
leave this as False.
|
||||
"""
|
||||
return False
|
||||
|
||||
@@ -768,6 +768,20 @@ class Qwen3_5ForConditionalGenerationConfig(VerifyAndUpdateConfig):
|
||||
)
|
||||
|
||||
|
||||
class Qwen3_5ForCausalLMConfig(Qwen3_5ForConditionalGenerationConfig):
|
||||
@staticmethod
|
||||
def verify_and_update_config(vllm_config: "VllmConfig") -> None:
|
||||
Qwen3_5ForConditionalGenerationConfig.verify_and_update_config(vllm_config)
|
||||
|
||||
# Text-only Qwen3.5 models use one-dimensional positions. Remove the
|
||||
# M-RoPE fields inherited from the multimodal configuration.
|
||||
hf_text_config = vllm_config.model_config.hf_text_config
|
||||
rope_parameters = getattr(hf_text_config, "rope_parameters", None)
|
||||
if rope_parameters is not None:
|
||||
rope_parameters.pop("mrope_section", None)
|
||||
rope_parameters.pop("mrope_interleaved", None)
|
||||
|
||||
|
||||
class ColQwen3_5Config(Qwen3_5ForConditionalGenerationConfig):
|
||||
"""Apply the attention contract declared by a ColQwen3.5 checkpoint."""
|
||||
|
||||
@@ -884,7 +898,9 @@ MODELS_CONFIG_MAP: dict[str, type[VerifyAndUpdateConfig]] = {
|
||||
"Qwen2ForRewardModel": Qwen2ForRewardModelConfig,
|
||||
"Qwen3ForSequenceClassification": Qwen3ForSequenceClassificationConfig,
|
||||
"Qwen3VLForSequenceClassification": Qwen3VLForSequenceClassificationConfig,
|
||||
"Qwen3_5ForCausalLM": Qwen3_5ForCausalLMConfig,
|
||||
"Qwen3_5ForConditionalGeneration": Qwen3_5ForConditionalGenerationConfig,
|
||||
"Qwen3_5MoeForCausalLM": Qwen3_5ForCausalLMConfig,
|
||||
"Qwen3_5MoeForConditionalGeneration": Qwen3_5ForConditionalGenerationConfig,
|
||||
"UnlimitedOCRForCausalLM": UnlimitedOCRForCausalLMConfig,
|
||||
"VoyageQwen3BidirectionalEmbedModel": VoyageQwen3BidirectionalEmbedModelConfig,
|
||||
|
||||
@@ -282,6 +282,7 @@ class Qwen3_5Model(Qwen3NextModel):
|
||||
class Qwen3_5ForCausalLMBase(
|
||||
nn.Module,
|
||||
HasInnerState,
|
||||
IsHybrid,
|
||||
SupportsEagle3,
|
||||
SupportsLoRA,
|
||||
SupportsPP,
|
||||
@@ -361,6 +362,45 @@ class Qwen3_5ForCausalLMBase(
|
||||
|
||||
return hidden_states
|
||||
|
||||
@classmethod
|
||||
def get_mamba_state_dtype_from_config(
|
||||
cls,
|
||||
vllm_config: "VllmConfig",
|
||||
) -> tuple[torch.dtype, torch.dtype]:
|
||||
return MambaStateDtypeCalculator.gated_delta_net_state_dtype(
|
||||
vllm_config.model_config.dtype,
|
||||
vllm_config.cache_config.mamba_cache_dtype,
|
||||
vllm_config.cache_config.mamba_ssm_cache_dtype,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_mamba_state_shape_from_config(
|
||||
cls, vllm_config: "VllmConfig"
|
||||
) -> tuple[tuple[int, int], tuple[int, int]]:
|
||||
parallel_config = vllm_config.parallel_config
|
||||
hf_config = vllm_config.model_config.hf_text_config
|
||||
tp_size = parallel_config.tensor_parallel_size
|
||||
num_spec = (
|
||||
vllm_config.speculative_config.num_speculative_tokens
|
||||
if vllm_config.speculative_config
|
||||
else 0
|
||||
)
|
||||
return MambaStateShapeCalculator.gated_delta_net_state_shape(
|
||||
tp_size,
|
||||
hf_config.linear_num_key_heads,
|
||||
hf_config.linear_num_value_heads,
|
||||
hf_config.linear_key_head_dim,
|
||||
hf_config.linear_value_head_dim,
|
||||
hf_config.linear_conv_kernel_dim,
|
||||
num_spec,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_mamba_state_copy_func(
|
||||
cls,
|
||||
) -> tuple[MambaStateCopyFunc, MambaStateCopyFunc]:
|
||||
return MambaStateCopyFuncCalculator.gated_delta_net_state_copy_func()
|
||||
|
||||
def compute_logits(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
|
||||
@@ -195,6 +195,8 @@ _TEXT_GENERATION_MODELS = {
|
||||
"Qwen2MoeForCausalLM": ("qwen2_moe", "Qwen2MoeForCausalLM"),
|
||||
"Qwen3ForCausalLM": ("qwen3", "Qwen3ForCausalLM"),
|
||||
"Qwen3MoeForCausalLM": ("qwen3_moe", "Qwen3MoeForCausalLM"),
|
||||
"Qwen3_5ForCausalLM": ("qwen3_5", "Qwen3_5ForCausalLM"),
|
||||
"Qwen3_5MoeForCausalLM": ("qwen3_5", "Qwen3_5MoeForCausalLM"),
|
||||
"RWForCausalLM": ("falcon", "FalconForCausalLM"),
|
||||
"SarvamMoEForCausalLM": ("sarvam", "SarvamMoEForCausalLM"),
|
||||
"SarvamMLAForCausalLM": ("sarvam", "SarvamMLAForCausalLM"),
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
import math
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import cast
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import numpy.typing as npt
|
||||
import torch
|
||||
from transformers import BatchFeature
|
||||
from transformers.video_utils import VideoMetadata
|
||||
@@ -469,10 +470,39 @@ class MiniMaxM3VLMultiModalProcessor(
|
||||
]
|
||||
|
||||
|
||||
# TODO(Isotr0py): Tie with MinimaxVideoProcessor
|
||||
# after https://github.com/vllm-project/vllm/pull/44126
|
||||
@VIDEO_LOADER_REGISTRY.register("minimax_m3_vl")
|
||||
@VIDEO_LOADER_REGISTRY.register(
|
||||
name="minimax_m3_vl",
|
||||
video_processor="MiniMaxM3VLVideoProcessor",
|
||||
)
|
||||
class MiniMaxM3VideoBackend(VideoBackend):
|
||||
@classmethod
|
||||
def load_bytes(
|
||||
cls,
|
||||
data: bytes,
|
||||
num_frames: int = -1,
|
||||
fps: int = 1,
|
||||
max_duration: int = 300,
|
||||
frame_recovery: bool = False,
|
||||
*,
|
||||
backend: Literal[
|
||||
"opencv",
|
||||
"pyav",
|
||||
"torchcodec",
|
||||
"pynvvideocodec",
|
||||
"deepstream",
|
||||
] = "opencv",
|
||||
**kwargs,
|
||||
) -> tuple[npt.NDArray, dict[str, Any]]:
|
||||
return super().load_bytes(
|
||||
data,
|
||||
num_frames=num_frames,
|
||||
fps=fps,
|
||||
max_duration=max_duration,
|
||||
frame_recovery=frame_recovery,
|
||||
backend=backend,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def compute_frames_index_to_sample(
|
||||
cls,
|
||||
@@ -483,7 +513,6 @@ class MiniMaxM3VideoBackend(VideoBackend):
|
||||
total_frames = source.total_frames_num
|
||||
video_fps = source.original_fps
|
||||
fps = target.fps
|
||||
|
||||
if total_frames <= 0 or video_fps <= 0 or fps <= 0:
|
||||
return [0] if total_frames > 0 else []
|
||||
|
||||
@@ -503,8 +532,9 @@ class MiniMaxM3VideoBackend(VideoBackend):
|
||||
break
|
||||
indices.append(target_frame)
|
||||
prev_kept_ts = target_frame / video_fps
|
||||
|
||||
last_frame_idx = total_frames - 1
|
||||
# Because HF sample_frames includes the last frame,
|
||||
# we will use HF as the standard.
|
||||
last_frame_idx = total_frames
|
||||
last_ts = last_frame_idx / video_fps
|
||||
if indices and indices[-1] != last_frame_idx and last_ts - prev_kept_ts > eps:
|
||||
indices.append(last_frame_idx)
|
||||
|
||||
@@ -124,7 +124,9 @@ _CONFIG_REGISTRY: dict[str, type[PretrainedConfig]] = LazyConfigDict(
|
||||
qwen3_asr="Qwen3ASRConfig",
|
||||
qwen3_next="Qwen3NextConfig",
|
||||
qwen3_5="Qwen3_5Config",
|
||||
qwen3_5_text="Qwen3_5TextConfig",
|
||||
qwen3_5_moe="Qwen3_5MoeConfig",
|
||||
qwen3_5_moe_text="Qwen3_5MoeTextConfig",
|
||||
laguna="LagunaConfig",
|
||||
lfm2_moe="Lfm2MoeConfig",
|
||||
**{"unlimited-ocr": "UnlimitedOCRConfig"},
|
||||
|
||||
@@ -2362,6 +2362,10 @@ class Scheduler(SchedulerInterface):
|
||||
self.has_unfinished_requests()
|
||||
or self.has_finished_requests()
|
||||
or (self.connector is not None and self.connector.has_pending_push_work())
|
||||
or (
|
||||
self.ec_connector is not None
|
||||
and self.ec_connector.has_pending_push_work()
|
||||
)
|
||||
)
|
||||
|
||||
def reset_prefix_cache(
|
||||
|
||||
@@ -1740,7 +1740,10 @@ class SpecDecodeBaseProposer:
|
||||
|
||||
attention_groups: dict[tuple[str, str], AttentionGroup] = {}
|
||||
if kv_cache_spec is not None:
|
||||
for layer_name in self._draft_attn_layer_names:
|
||||
# _draft_attn_layer_names is a set; iterate in sorted order so
|
||||
# that attention_groups (and anything derived from its first
|
||||
# element) is deterministic across processes.
|
||||
for layer_name in sorted(self._draft_attn_layer_names):
|
||||
attn_backend = all_attn_layers[layer_name].get_attn_backend()
|
||||
backend_key = attn_backend.full_cls_name()
|
||||
if backend_key not in attention_groups:
|
||||
@@ -1772,9 +1775,20 @@ class SpecDecodeBaseProposer:
|
||||
attention_groups[backend_key].layer_names.append(layer_name)
|
||||
|
||||
self.draft_attn_groups = list(attention_groups.values())
|
||||
self.block_size = (
|
||||
self.draft_attn_groups[0].get_metadata_builder().kv_cache_spec.block_size
|
||||
)
|
||||
if kernel_block_sizes is not None and 0 <= self.kv_cache_gid < len(
|
||||
kernel_block_sizes
|
||||
):
|
||||
# Slot mappings are computed against the block table, which is
|
||||
# stored at kernel-block granularity. Use the kernel block size
|
||||
# rather than the KV cache manager's block size; the two differ
|
||||
# when manager blocks are split for the attention kernel.
|
||||
self.block_size = kernel_block_sizes[self.kv_cache_gid]
|
||||
else:
|
||||
self.block_size = (
|
||||
self.draft_attn_groups[0]
|
||||
.get_metadata_builder()
|
||||
.kv_cache_spec.block_size
|
||||
)
|
||||
logger.debug("Using block size %d for drafting layers", self.block_size)
|
||||
|
||||
def _determine_batch_execution_and_padding(
|
||||
|
||||
Reference in New Issue
Block a user