Compare commits

...
Author SHA1 Message Date
yewentao256 b870c8edb4 optimize allpool forward
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-05-04 22:47:43 +00:00
Matthew BonanniandGitHub be5983b874 [Docs] Add non-causal support to attention backend docs (#41643)
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com>
2026-05-04 20:35:15 +00:00
fxmarty-amdandGitHub 9c07342fdc [NVFP4][fix] Fix layer.weight -> w13 typo in NVFP4 MOE emulation kernel preparation (#41630)
Signed-off-by: Felix Marty <Felix.Marty@amd.com>
2026-05-04 20:13:37 +00:00
844df54269 feat: update xgrammar==0.2.0 to use structural tags for strict tool calling + reasoning for more models (#40894)
Signed-off-by: Yuchuan <yuchuan.7streams@gmail.com>
Signed-off-by: Michael Goin <mgoin64@gmail.com>
Signed-off-by: mgoin <mgoin64@gmail.com>
Signed-off-by: Ubospica <ubospica@gmail.com>
Signed-off-by: sfeng33 <4florafeng@gmail.com>
Co-authored-by: Michael Goin <mgoin64@gmail.com>
Co-authored-by: Ubospica <ubospica@gmail.com>
Co-authored-by: sfeng33 <4florafeng@gmail.com>
2026-05-04 12:45:24 -07:00
422dd02598 [bugfix] Fix prompt logprobs on request eviction during chunked prefill (#41411)
Signed-off-by: Joachim Studnia <joachim@mistral.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 11:46:00 -07:00
8c780943b4 Fix Nano Nemotron text-only weight loading (#41205)
Signed-off-by: sunghoon.baek <sunghoon.baek@connectfy.cloud>
Signed-off-by: Baekpica <35071468+Baekpica@users.noreply.github.com>
Signed-off-by: sunghoon.baek <seanbb93@gmail.com>
Co-authored-by: sunghoon.baek <sunghoon.baek@connectfy.cloud>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Netanel Haber <58652339+netanel-haber@users.noreply.github.com>
2026-05-04 21:43:07 +03:00
e724b0ea8d [ROCm] ROCm7.2.2 + profiler fix + AITER 0.1.12.post2 (#41386)
Signed-off-by: Rohan138 <rohanpotdar138@gmail.com>
Signed-off-by: Gregory Shtrasberg <Gregory.Shtrasberg@amd.com>
Co-authored-by: Rohan138 <rohanpotdar138@gmail.com>
2026-05-04 13:07:19 -05:00
712ad0286c [Bugfix] KimiK2ReasoningParser: guard against buffered end-token in streaming (#41068)
Signed-off-by: Keyi Li <likey6688@gmail.com>
Co-authored-by: Keyi Li <likey6688@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Flora Feng <4florafeng@gmail.com>
2026-05-04 17:42:05 +00:00
Ekagra RanjanandGitHub 321fa2d6d1 Limit gpu utils and lower max BS on test_transcription_api_correctness.py (#41649)
Signed-off-by: Ekagra Ranjan <3116519+ekagra-ranjan@users.noreply.github.com>
2026-05-04 10:30:02 -07:00
Wentao YeandGitHub 3e1ad4435f [Bug] Fix tests/compile/test_config.py AttributeError: 'NoneType' object has no attribute 'dtype' (#41288)
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-05-05 00:22:07 +08:00
Netanel HaberandGitHub 8decbfa02c Test nemotron nano-v2 and nemotron nano-v3 separately, disable super-omni redundant tests (#41616)
Signed-off-by: Netanel Haber <58652339+netanel-haber@users.noreply.github.com>
2026-05-04 16:31:37 +03:00
Stefano CastagnettaandGitHub 62ba7516e8 Revert "[Doc] Fix RTD build: pytorch.org/docs/stable/objects.inv returns 404" (#41618)
Signed-off-by: Stefano Castagnetta <scastagnetta@nvidia.com>
2026-05-04 04:47:42 -07:00
Stefano CastagnettaandGitHub 6f53753fc9 [Bugfix] Apply ruff-format to hyperclovax.py (#41620)
Signed-off-by: Stefano Castagnetta <scastagnetta@nvidia.com>
2026-05-04 03:37:16 -07:00
Andreas KaratzasandGitHub 6ec9bbec38 [CI] Stabilize cpu offload compressed tensors test (#41102)
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
2026-05-04 05:22:42 +00:00
Andreas KaratzasandGitHub 01d4d1ad37 [ROCm][CI] Align spec decode logprob test prefill settings (#41335)
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
2026-05-04 04:33:29 +00:00
c103c02a1a [Transformers v5] Vendor HCXVisionConfig for compatibility (#38447)
Signed-off-by: Fang Han <fhan0520@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 04:19:52 +00:00
Andreas KaratzasandGitHub 67058ca326 [CI] Clean up remote servers on pytest parent exit (#41570)
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
2026-05-04 03:11:22 +00:00
Akim TsvigunandGitHub 894a02500b [Bench] Forward --seed to CustomDataset and CustomMMDataset shuffle (#40788)
Signed-off-by: akimtsvigun <akimtsvigun@gmail.com>
2026-05-04 00:39:10 +00:00
66dfee7121 [Bugfix] Fix degenerate KV cache stride causing TMA cudaErrorIllegalInstruction (#40737)
Signed-off-by: David Oy <david@baseten.co>
Signed-off-by: David Oy <58150256+the-david-oy@users.noreply.github.com>
Signed-off-by: David Oy <david.oy@baseten.co>
Co-authored-by: David Oy <david@baseten.co>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Vadim Gimpelson <156319763+vadiklyutiy@users.noreply.github.com>
2026-05-03 23:52:18 +00:00
Alex BrooksandGitHub db9a84e0cd [Bugfix] Fix FP8 Bias Loading (#41424)
Signed-off-by: Alex Brooks <albrooks@redhat.com>
2026-05-03 20:30:04 +00:00
tomeras91andGitHub cb03fee32b [Bugfix][Ray] Fix RayExecutorV2 actor name collision with DP > 1 (#40398)
Signed-off-by: Tomer Asida <57313761+tomeras91@users.noreply.github.com>
2026-05-03 13:00:41 -07:00
Wei ZhaoandGitHub c51df43005 Disable flashinfer autotune temporarily due to correctness issues (#41524)
Signed-off-by: wzhao18 <wzhao18.sz@gmail.com>
2026-05-03 16:19:59 +00:00
Taneem IbrahimandGitHub 54dc64d5d3 [Doc] Add Qwen3-30B-A3B-Thinking-2507-FP8 to batch invariance verified models (#41513)
Signed-off-by: Taneem Ibrahim <taneem.ibrahim@gmail.com>
2026-05-03 08:47:55 -04:00
48 changed files with 1537 additions and 196 deletions
+1 -1
View File
@@ -723,7 +723,7 @@ steps:
- "bash tools/vllm-rocm/generate-rocm-wheels-root-index.sh"
env:
S3_BUCKET: "vllm-wheels"
VARIANT: "rocm721"
VARIANT: "rocm722"
# ROCm Job 6: Build ROCm Release Docker Image
- label: ":docker: Build release image - x86_64 - ROCm"
+24 -4
View File
@@ -1,4 +1,4 @@
ARG BASE_IMAGE=rocm/dev-ubuntu-22.04:7.2.1-complete
ARG BASE_IMAGE=rocm/dev-ubuntu-22.04:7.2.2-complete
ARG TRITON_BRANCH="ba5c1517"
ARG TRITON_REPO="https://github.com/ROCm/triton.git"
ARG PYTORCH_BRANCH="8514f051" # release/2.10 as of 3/17
@@ -9,7 +9,7 @@ ARG PYTORCH_AUDIO_BRANCH="v2.9.0"
ARG PYTORCH_AUDIO_REPO="https://github.com/pytorch/audio.git"
ARG FA_BRANCH="0e60e394"
ARG FA_REPO="https://github.com/Dao-AILab/flash-attention.git"
ARG AITER_BRANCH="v0.1.10.post3"
ARG AITER_BRANCH="v0.1.12.post2"
ARG AITER_REPO="https://github.com/ROCm/aiter.git"
ARG MORI_BRANCH="v1.1.0"
ARG MORI_REPO="https://github.com/ROCm/mori.git"
@@ -104,6 +104,28 @@ ENV SCCACHE_REGION=${USE_SCCACHE:+${SCCACHE_REGION_NAME}}
ENV SCCACHE_S3_NO_CREDENTIALS=${USE_SCCACHE:+${SCCACHE_S3_NO_CREDENTIALS}}
ENV SCCACHE_IDLE_TIMEOUT=${USE_SCCACHE:+0}
# torch profiler hotfix for 7.2.2: rebuild CLR with https://github.com/ROCm/rocm-systems/pull/5062
# will be removed once we move to ROCm 7.2.3
RUN apt-get update && apt-get install -y rocm-llvm-dev
RUN pip install CppHeaderParser
RUN git clone --no-checkout --filter=blob:none https://github.com/ROCm/rocm-systems /tmp/rocm-systems \
&& cd /tmp/rocm-systems \
&& git sparse-checkout init --cone \
&& git sparse-checkout set projects/hip projects/clr \
&& git checkout 35e8c7bf8911862e5389509800e65fdf125412b3 \
&& export CLR_DIR=/tmp/rocm-systems/projects/clr \
&& export HIP_DIR=/tmp/rocm-systems/projects/hip \
&& mkdir -p $CLR_DIR/build && cd $CLR_DIR/build \
&& cmake \
-DHIP_COMMON_DIR=$HIP_DIR \
-DCMAKE_PREFIX_PATH="/opt/rocm/" \
-DCLR_BUILD_HIP=ON \
-DCLR_BUILD_OCL=OFF \
-DHIP_PLATFORM=amd \
.. \
&& make -j$(nproc) \
&& make install \
&& rm -rf /tmp/rocm-systems
###
### Triton Build
@@ -153,8 +175,6 @@ RUN git clone ${PYTORCH_REPO} pytorch
RUN cd pytorch && git checkout ${PYTORCH_BRANCH}
RUN cd pytorch \
&& pip install -r requirements.txt && git submodule update --init --recursive
RUN cd pytorch/third_party/kineto \
&& git remote add rocm https://github.com/ROCm/kineto && git fetch rocm && git checkout 2d73be3
RUN cd pytorch && python3 tools/amd_build/build_amd.py \
&& if [ "$USE_SCCACHE" = "1" ]; then \
export HIP_CLANG_PATH=/opt/sccache-wrappers \
+30 -29
View File
@@ -155,6 +155,7 @@ Priority is **1 = highest** (tried first).
| **Block Sizes** | Supported KV cache block sizes (%N means multiples of N) |
| **Head Sizes** | Supported attention head sizes |
| **Sink** | Attention sink support (for StreamingLLM) |
| **Non-Causal** | Non-causal (bidirectional) attention support for decoder models |
| **Sparse** | Sparse attention support (MLA only) |
| **MM Prefix** | Multimodal prefix full attention support |
| **DCP** | Decode Context Parallelism support (`--decode-context-parallel-size`) |
@@ -165,22 +166,22 @@ Priority is **1 = highest** (tried first).
## Standard Attention (MHA, MQA, GQA) Backends
| Backend | Version | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | MM Prefix | DCP | Attention Types | Compute Cap. |
| ------- | ------- | ------ | --------- | ----------- | ---------- | ---- | --------- | --- | --------------- | ------------ |
| `CPU_ATTN` | | fp16, bf16, fp32 | `auto`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | Any | 32, 64, 80, 96, 112, 128, 160, 192, 224, 256, 512 | ❌ | ❌ | ❌ | All | N/A |
| `FLASHINFER` | Native† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64 | 64, 128, 256 | ❌ | ❌ | ✅ | Decoder | 7.x-9.x |
| `FLASHINFER` | TRTLLM† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `nvfp4` | 16, 32, 64 | 64, 128, 256 | ✅ | ❌ | ✅ | Decoder | 10.x |
| `FLASH_ATTN` | FA2* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ❌ | ✅ | All | ≥8.0 |
| `FLASH_ATTN` | FA3* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | Any | ✅ | ❌ | ✅ | All | 9.x |
| `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ✅ | ❌ | ✅ | All | ≥10.0 |
| `FLASH_ATTN_DIFFKV` | | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ✅ | Decoder | Any |
| `FLEX_ATTENTION` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ❌ | Decoder, Encoder Only | Any |
| `ROCM_AITER_FA` | | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32 | 64, 128, 256 | ❌ | ❌ | ❌ | Decoder | N/A |
| `ROCM_AITER_UNIFIED_ATTN` | | fp16, bf16 | `auto` | %16 | Any | ✅ | ✅ | ❌ | All | N/A |
| `ROCM_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 128, 160, 192, 224, 256 | ❌ | ✅ | ❌ | Decoder, Encoder, Encoder Only | N/A |
| `TREE_ATTN` | | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | 32, 64, 96, 128, 160, 192, 224, 256 | ❌ | ❌ | ❌ | Decoder | Any |
| `TRITON_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `int8_per_token_head`, `fp8_per_token_head` | %16 | Any | ✅ | ✅ | ❌ | All | Any |
| `TURBOQUANT` | | fp16, bf16 | `turboquant_k8v4`, `turboquant_4bit_nc`, `turboquant_k3v4_nc`, `turboquant_3bit_nc` | 16, 32, 64, 128 | Any | ❌ | ❌ | ❌ | Decoder | Any |
| Backend | Version | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | MM Prefix | DCP | Attention Types | Compute Cap. |
| ------- | ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | --------- | --- | --------------- | ------------ |
| `CPU_ATTN` | | fp16, bf16, fp32 | `auto`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | Any | 32, 64, 80, 96, 112, 128, 160, 192, 224, 256, 512 | ❌ | ❌ | ❌ | ❌ | All | N/A |
| `FLASHINFER` | Native† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64 | 64, 128, 256 | ❌ | ❌ | ❌ | ✅ | Decoder | 7.x-9.x |
| `FLASHINFER` | TRTLLM† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `nvfp4` | 16, 32, 64 | 64, 128, 256 | ✅ | ❌ | ❌ | ✅ | Decoder | 10.x |
| `FLASH_ATTN` | FA2* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ❌ | ✅ | All | ≥8.0 |
| `FLASH_ATTN` | FA3* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | 9.x |
| `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | ≥10.0 |
| `FLASH_ATTN_DIFFKV` | | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
| `FLEX_ATTENTION` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder Only | Any |
| `ROCM_AITER_FA` | | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32 | 64, 128, 256 | ❌ | ✅ | ❌ | ❌ | Decoder | N/A |
| `ROCM_AITER_UNIFIED_ATTN` | | fp16, bf16 | `auto` | %16 | Any | ✅ | ❌ | ✅ | ❌ | All | N/A |
| `ROCM_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 128, 160, 192, 224, 256 | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder, Encoder Only | N/A |
| `TREE_ATTN` | | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | 32, 64, 96, 128, 160, 192, 224, 256 | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
| `TRITON_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `int8_per_token_head`, `fp8_per_token_head` | %16 | Any | ✅ | ❌ | ✅ | ❌ | All | Any |
| `TURBOQUANT` | | fp16, bf16 | `turboquant_k8v4`, `turboquant_4bit_nc`, `turboquant_k3v4_nc`, `turboquant_3bit_nc` | 16, 32, 64, 128 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
> **†** FlashInfer uses TRTLLM attention on Blackwell (SM100), which supports sinks. Disable via `--attention-config.use_trtllm_attention=0`.
>
@@ -211,16 +212,16 @@ hardware and configuration.
MLA decode backends are selected using the standard
`-ac.backend=<BACKEND>` argument (e.g., `FLASHMLA`, `TRITON_MLA`).
| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
| ------- | ------ | --------- | ----------- | ---------- | ---- | ------ | --------- | --- | --------------- | ------------ |
| `CUTLASS_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | 576 | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x |
| `FLASHMLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
| `FLASHMLA_SPARSE` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla` | 64 | 512, 576 | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
| `FLASH_ATTN_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x |
| `ROCM_AITER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %1 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
| `ROCM_AITER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 1, 64 | Any | ❌ | ✅ | ❌ | ❌ | Decoder | N/A |
| `ROCM_AITER_TRITON_MLA` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
| `TRITON_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
| `XPU_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16` | Any | 576 | ❌ | ✅ | ❌ | ❌ | Decoder | Any |
| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
| `CUTLASS_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x |
| `FLASHMLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
| `FLASHMLA_SPARSE` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla` | 64 | 512, 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
| `FLASH_ATTN_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x |
| `ROCM_AITER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %1 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
| `ROCM_AITER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 1, 64 | Any | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | N/A |
| `ROCM_AITER_TRITON_MLA` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
| `TRITON_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
| `XPU_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16` | Any | 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | Any |
+1 -1
View File
@@ -105,7 +105,7 @@ Batch invariance has been tested and verified on the following models:
- **DeepSeek series**: `deepseek-ai/DeepSeek-V3`, `deepseek-ai/DeepSeek-V3-0324`, `deepseek-ai/DeepSeek-R1`, `deepseek-ai/DeepSeek-V3.1`
- **Qwen3 (Dense)**: `Qwen/Qwen3-1.7B`, `Qwen/Qwen3-8B`, `Qwen/Qwen3-4B-AWQ`, `Qwen/Qwen3-8B-AWQ`
- **Qwen3 (MoE)**: `Qwen/Qwen3-30B-A3B`, `Qwen/Qwen3-Next-80B-A3B-Instruct`
- **Qwen3 (MoE)**: `Qwen/Qwen3-30B-A3B`, `Qwen/Qwen3-Next-80B-A3B-Instruct`, `Qwen/Qwen3-30B-A3B-Thinking-2507-FP8`
- **Qwen2.5**: `Qwen/Qwen2.5-0.5B-Instruct`, `Qwen/Qwen2.5-1.5B-Instruct`, `Qwen/Qwen2.5-3B-Instruct`, `Qwen/Qwen2.5-7B-Instruct`, `Qwen/Qwen2.5-14B-Instruct`, `Qwen/Qwen2.5-32B-Instruct`
- **Llama 3**: `meta-llama/Llama-3.1-8B-Instruct`, `meta-llama/Llama-3.2-1B-Instruct`
- **GPT-OSS**: `openai/gpt-oss-20b`, `openai/gpt-oss-120b`
+1 -2
View File
@@ -105,8 +105,7 @@ plugins:
- https://docs.aiohttp.org/en/stable/objects.inv
- https://pillow.readthedocs.io/en/stable/objects.inv
- https://numpy.org/doc/stable/objects.inv
# TODO revert to stable once https://github.com/pytorch/pytorch/issues/182007 is fixed
- https://pytorch.org/docs/2.11/objects.inv
- https://pytorch.org/docs/stable/objects.inv
- redirects:
redirect_maps:
features/spec_decode/README.md: features/speculative_decoding/README.md
+1 -1
View File
@@ -24,7 +24,7 @@ outlines_core == 0.2.14
# required for outlines backend disk cache
diskcache == 5.6.3
lark == 1.2.2
xgrammar >= 0.1.32, < 1.0.0; platform_machine == "x86_64" or platform_machine == "aarch64" or platform_machine == "arm64" or platform_machine == "s390x" or platform_machine == "ppc64le"
xgrammar >= 0.2.0, < 1.0.0; platform_machine == "x86_64" or platform_machine == "aarch64" or platform_machine == "arm64" or platform_machine == "s390x" or platform_machine == "ppc64le"
typing_extensions >= 4.10
filelock >= 3.16.1 # need to contain https://github.com/tox-dev/filelock/pull/317
partial-json-parser # used for parsing partial JSON outputs
+4 -1
View File
@@ -42,6 +42,8 @@ anyio==4.13.0
# sse-starlette
# starlette
# watchfiles
apache-tvm-ffi==0.1.10
# via xgrammar
arctic-inference==0.1.1
# via -r requirements/test/rocm.in
argcomplete==3.6.3
@@ -1264,6 +1266,7 @@ typing-extensions==4.15.0
# alembic
# anthropic
# anyio
# apache-tvm-ffi
# azure-core
# azure-identity
# azure-storage-blob
@@ -1345,7 +1348,7 @@ word2number==1.1
# via lm-eval
wrapt==2.1.2
# via smart-open
xgrammar==0.1.33
xgrammar==0.2.0
# via
# -c requirements/common.txt
# -r requirements/test/../common.txt
@@ -0,0 +1,77 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import argparse
import json
from pathlib import Path
import pytest
from transformers import AutoTokenizer, PreTrainedTokenizerBase
from vllm.benchmarks.datasets import get_samples
@pytest.fixture(scope="session")
def hf_tokenizer() -> PreTrainedTokenizerBase:
return AutoTokenizer.from_pretrained("gpt2")
def _write_jsonl(path: Path, n_rows: int) -> None:
with path.open("w") as f:
for i in range(n_rows):
f.write(json.dumps({"prompt": f"row {i}: unique prompt content."}) + "\n")
def _args_for_custom(dataset_path: str, seed: int) -> argparse.Namespace:
return argparse.Namespace(
dataset_name="custom",
dataset_path=dataset_path,
disable_shuffle=False,
num_prompts=30,
custom_output_len=32,
skip_chat_template=True,
no_oversample=False,
seed=seed,
request_id_prefix="",
)
@pytest.mark.benchmark
def test_custom_dataset_seed_propagates(
hf_tokenizer: PreTrainedTokenizerBase, tmp_path: Path
) -> None:
"""--seed must control the CustomDataset shuffle used by get_samples.
Without the fix, CustomDataset was instantiated without random_seed,
so its load-time shuffle always used DEFAULT_SEED=0 regardless of
args.seed, causing every run with --dataset-name custom to pick the
same subset of rows from a larger file.
"""
jsonl = tmp_path / "data.jsonl"
_write_jsonl(jsonl, n_rows=60)
samples_a = get_samples(_args_for_custom(str(jsonl), seed=0), hf_tokenizer)
samples_b = get_samples(_args_for_custom(str(jsonl), seed=42), hf_tokenizer)
prompts_a = {s.prompt for s in samples_a}
prompts_b = {s.prompt for s in samples_b}
assert len(prompts_a) == 30
assert len(prompts_b) == 30
assert prompts_a != prompts_b
@pytest.mark.benchmark
def test_custom_dataset_same_seed_is_deterministic(
hf_tokenizer: PreTrainedTokenizerBase, tmp_path: Path
) -> None:
"""Same --seed must yield the same CustomDataset subset."""
jsonl = tmp_path / "data.jsonl"
_write_jsonl(jsonl, n_rows=60)
samples_a = get_samples(_args_for_custom(str(jsonl), seed=7), hf_tokenizer)
samples_b = get_samples(_args_for_custom(str(jsonl), seed=7), hf_tokenizer)
prompts_a = [s.prompt for s in samples_a]
prompts_b = [s.prompt for s in samples_b]
assert prompts_a == prompts_b
+2
View File
@@ -996,6 +996,8 @@ class VllmRunner:
req_sample_output_ids: list[list[int]] = []
req_sample_output_strs: list[str] = []
req_logprobs = []
if req_output.prompt_logprobs:
req_logprobs.extend(req_output.prompt_logprobs)
for sample in req_output.outputs:
output_str = sample.text
output_ids = list(sample.token_ids)
@@ -27,7 +27,8 @@ from ....models.registry import HF_EXAMPLE_MODELS
from ....utils import RemoteOpenAIServer
# Tuned to prevent OOM on 18GB GPUs in transcription correctness tests.
MAX_SEQS_FOR_TRANSCRIPTION_TEST = 32
MAX_SEQS_FOR_TRANSCRIPTION_TEST = 8
GPU_UTIL_FOR_TRANSCRIPTION_TEST = 0.5
def to_bytes(y, sr):
@@ -188,6 +189,7 @@ def test_wer_correctness(
"--enforce-eager",
f"--tokenizer_mode={model_info.tokenizer_mode}",
f"--max_num_seqs={MAX_SEQS_FOR_TRANSCRIPTION_TEST}",
f"--gpu_memory_utilization={GPU_UTIL_FOR_TRANSCRIPTION_TEST}",
]
if model_info.trust_remote_code:
server_args.append("--trust-remote-code")
@@ -59,6 +59,34 @@ def test_reload_lifecycle():
assert tensor.__dict__ == materialized_tensor.__dict__
def test_materialize_layer_preserves_non_meta_tensors():
"""Ensure that materialize_layer does not overwrite non meta tensors."""
layer = torch.nn.Linear(2, 3, bias=True)
# Create a non meta bias tensor and meta weight, which can happen with FP8
bias_values = torch.ones(3)
layer.bias.data.copy_(bias_values)
layer.weight = torch.nn.Parameter(layer.weight.data.to("meta"))
assert layer.weight.is_meta
assert not layer.bias.is_meta
# materialize the layer weights after the bias is initialized
info = LayerReloadingInfo(
restore_metadata=({}, {}),
restore_device=torch.device("cpu"),
)
materialize_layer(layer, info)
# Ensure the weight materialized off meta
assert not layer.weight.is_meta
assert layer.weight.device.type == "cpu"
# Ensure that the bias is (still) not meta and values are unchanged
assert not layer.bias.is_meta
assert torch.equal(layer.bias.data, bias_values)
def test_model_cleanup(dist_init, default_vllm_config):
layer = QKVParallelLinear(2, 3, 4)
assert layer.weight.weight_loader.__self__ is layer
@@ -0,0 +1,114 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import pytest
from vllm.model_executor.models.nano_nemotron_vl import NemotronH_Nano_VL_V2
class _TextOnlyMultiModalConfig:
def get_limit_per_prompt(self, modality: str) -> int:
return 0
class _ImageOnlyMultiModalConfig:
def get_limit_per_prompt(self, modality: str) -> int:
return 1 if modality == "image" else 0
class _ModelConfig:
multimodal_config = _TextOnlyMultiModalConfig()
class _ImageOnlyModelConfig:
multimodal_config = _ImageOnlyMultiModalConfig()
class _LanguageModel:
def __init__(self) -> None:
self.loaded_weights: list[tuple[str, object]] = []
def load_weights(self, weights):
self.loaded_weights = list(weights)
class _MissingMultiModalModule:
def named_parameters(self):
raise AssertionError("multimodal weights should not be inspected")
def load_weights(self, weights):
raise AssertionError("multimodal weights should not be loaded")
class _AdapterModule:
def named_parameters(self):
return []
class _VisionModel:
def __init__(self) -> None:
self.loaded_weights: list[tuple[str, object]] = []
def load_weights(self, weights):
self.loaded_weights = list(weights)
def test_nano_nemotron_vl_skips_multimodal_weights_in_text_only_mode():
model = object.__new__(NemotronH_Nano_VL_V2)
language_model = _LanguageModel()
object.__setattr__(model, "model_config", _ModelConfig())
object.__setattr__(model, "language_model", language_model)
object.__setattr__(model, "mlp1", _AdapterModule())
object.__setattr__(model, "vision_model", _MissingMultiModalModule())
object.__setattr__(model, "sound_encoder", None)
language_weight = object()
model.load_weights(
[
("language_model.layers.0.weight", language_weight),
("mlp1.0.weight", object()),
("vision_model.radio_model.encoder.weight", object()),
("sound_encoder.encoder.weight", object()),
]
)
assert language_model.loaded_weights == [("layers.0.weight", language_weight)]
def test_nano_nemotron_vl_loads_vision_weights_without_sound_encoder():
model = object.__new__(NemotronH_Nano_VL_V2)
language_model = _LanguageModel()
vision_model = _VisionModel()
object.__setattr__(model, "model_config", _ImageOnlyModelConfig())
object.__setattr__(model, "language_model", language_model)
object.__setattr__(model, "mlp1", _AdapterModule())
object.__setattr__(model, "vision_model", vision_model)
object.__setattr__(model, "sound_encoder", None)
language_weight = object()
vision_weight = object()
model.load_weights(
[
("language_model.layers.0.weight", language_weight),
("vision_model.radio_model.encoder.weight", vision_weight),
]
)
assert language_model.loaded_weights == [("layers.0.weight", language_weight)]
assert vision_model.loaded_weights == [
("radio_model.encoder.weight", vision_weight)
]
def test_nano_nemotron_vl_requires_sound_encoder_for_sound_weights():
model = object.__new__(NemotronH_Nano_VL_V2)
language_model = _LanguageModel()
vision_model = _VisionModel()
object.__setattr__(model, "model_config", _ImageOnlyModelConfig())
object.__setattr__(model, "language_model", language_model)
object.__setattr__(model, "mlp1", _AdapterModule())
object.__setattr__(model, "vision_model", vision_model)
object.__setattr__(model, "sound_encoder", None)
with pytest.raises(AssertionError):
model.load_weights([("sound_encoder.encoder.weight", object())])
+10 -48
View File
@@ -946,13 +946,6 @@ _MULTIMODAL_EXAMPLE_MODELS = {
"HCXVisionForCausalLM": _HfExamplesInfo(
"naver-hyperclovax/HyperCLOVAX-SEED-Vision-Instruct-3B",
trust_remote_code=True,
max_transformers_version="4.57",
transformers_version_reason={
"vllm": (
"Custom config cannot be loaded with Transformers "
"v5 because `text_config` is not always set"
)
},
),
"HCXVisionV2ForCausalLM": _HfExamplesInfo(
"naver-hyperclovax/HyperCLOVAX-SEED-Think-32B",
@@ -1148,30 +1141,17 @@ _MULTIMODAL_EXAMPLE_MODELS = {
"NemotronH_Nano_VL_V2": _HfExamplesInfo(
"nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16",
max_model_len=4096,
# NemotronH layers are constructed via `hybrid_override_pattern`:
# NemotronH layers are constructed via `hybrid_override_pattern`
use_original_num_layers=True,
hf_overrides={
"vision_config": PretrainedConfig(
args={
"min_num_patches": 1, # Trigger image dynamic res
"max_num_patches": 12,
"model": "vit_huge_patch16_224",
},
# Trigger conv3d:
video_temporal_patch_size=2,
),
"text_config": {
"num_hidden_layers": 2,
"hybrid_override_pattern": "M*",
},
"text_config": {"num_hidden_layers": 2, "hybrid_override_pattern": "M*"},
},
trust_remote_code=True,
),
# NemotronH_Nano_Omni_Reasoning_V3 is an alias for NemotronH_Nano_VL_V2
# Use the same registry test as NemotronH_Nano_VL_V2 above
"NemotronH_Nano_Omni_Reasoning_V3": _HfExamplesInfo(
"nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16",
"nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16",
max_model_len=4096,
# NemotronH layers are constructed via `hybrid_override_pattern`
use_original_num_layers=True,
hf_overrides={
"vision_config": PretrainedConfig(
@@ -1181,35 +1161,17 @@ _MULTIMODAL_EXAMPLE_MODELS = {
"model": "vit_huge_patch16_224",
},
video_temporal_patch_size=2,
# TODO(nhaber): This is `true` in the official `config.json`,
# but this causes a processor exception in the tests due to a known bug
# with mixed-resolution video when `true`. To be resolved.
video_maintain_aspect_ratio=False,
),
"text_config": {
"num_hidden_layers": 2,
"hybrid_override_pattern": "M*",
},
"text_config": {"num_hidden_layers": 2, "hybrid_override_pattern": "M*"},
},
trust_remote_code=True,
),
# NemotronH_Super_Omni_Reasoning_V3 is an alias for NemotronH_Nano_VL_V2 as well
# Use the same registry test as NemotronH_Nano_VL_V2 above
"NemotronH_Super_Omni_Reasoning_V3": _HfExamplesInfo(
"nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16",
max_model_len=4096,
use_original_num_layers=True,
hf_overrides={
"vision_config": PretrainedConfig(
args={
"min_num_patches": 1,
"max_num_patches": 12,
"model": "vit_huge_patch16_224",
},
video_temporal_patch_size=2,
),
"text_config": {
"num_hidden_layers": 2,
"hybrid_override_pattern": "M*",
},
},
trust_remote_code=True,
"nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16", is_available_online=False
),
"OpenCUAForConditionalGeneration": _HfExamplesInfo(
"xlangai/OpenCUA-7B",
+1
View File
@@ -70,4 +70,5 @@ def test_cpu_offload_compressed_tensors(monkeypatch):
["--enforce_eager"],
["--enforce_eager", "--cpu-offload-gb", "1"],
max_wait_seconds=480,
include_seeded_sampling=False,
)
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from unittest.mock import MagicMock
import pytest
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
@@ -12,6 +14,20 @@ from vllm.tokenizers import get_tokenizer
REASONING_MODEL_NAME = "moonshotai/Kimi-K2.5"
@pytest.fixture
def mock_kimi_k2_tokenizer():
tokenizer = MagicMock()
tokenizer.get_vocab.return_value = {
"<think>": 100,
"</think>": 101,
"<|tool_calls_section_begin|>": 200,
"<|tool_calls_section_end|>": 201,
"<|tool_call_begin|>": 202,
"<|tool_call_end|>": 203,
}
return tokenizer
@pytest.fixture(scope="module")
def kimi_k2_tokenizer():
return get_tokenizer(tokenizer_name=REASONING_MODEL_NAME, trust_remote_code=True)
@@ -153,3 +169,50 @@ def test_streaming_tool_section_ends_reasoning(kimi_k2_tokenizer):
)
assert isinstance(result, DeltaMessage)
assert result.content == "<|tool_calls_section_begin|>"
def test_streaming_end_token_id_buffered(mock_kimi_k2_tokenizer):
"""When stop sequences buffer text, </think> ID arrives before its text.
The token ID is present in delta_token_ids but the actual string is not
yet in delta_text (still buffered). The parser must return None to wait
for the next delta, instead of calling find() which returns -1 and
silently corrupting the text split.
"""
parser = KimiK2ReasoningParser(mock_kimi_k2_tokenizer)
think_id = parser._start_token_id
end_think_id = parser._end_token_id
# Simulate: </think> ID arrived but text not yet flushed.
# Two token IDs in delta to bypass the single-special-token guard.
result = parser.extract_reasoning_streaming(
previous_text="some reasoning",
current_text="some reasoning extra",
delta_text="extra", # </think> text not yet flushed
previous_token_ids=[think_id],
current_token_ids=[think_id, end_think_id, 999],
delta_token_ids=[end_think_id, 999],
)
assert result is None
def test_streaming_tool_section_id_buffered(mock_kimi_k2_tokenizer):
"""When stop sequences buffer text, tool section start ID arrives before its text.
Same buffering scenario as above but for <|tool_calls_section_begin|>.
Without the guard, find() returns -1 and delta_text[:tool_index] silently
drops the last character of reasoning.
"""
parser = KimiK2ReasoningParser(mock_kimi_k2_tokenizer)
think_id = parser._start_token_id
tool_begin_id = parser._tool_section_start_token_id
result = parser.extract_reasoning_streaming(
previous_text="some reasoning",
current_text="some reasoning extra",
delta_text="extra", # tool section text not yet flushed
previous_token_ids=[think_id],
current_token_ids=[think_id, tool_begin_id, 999],
delta_token_ids=[tool_begin_id, 999],
)
assert result is None
@@ -6,6 +6,15 @@
import json
from unittest.mock import MagicMock
import pytest
from xgrammar import StructuralTag
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionNamedFunction,
ChatCompletionNamedToolChoiceParam,
ChatCompletionRequest,
ChatCompletionToolsParam,
)
from vllm.tool_parsers import ToolParserManager
from vllm.tool_parsers.deepseekv4_tool_parser import DeepSeekV4ToolParser
@@ -20,6 +29,43 @@ PARAM_START = '<DSMLparameter name="'
PARAM_END = "</DSMLparameter>"
@pytest.fixture
def sample_tools() -> list[ChatCompletionToolsParam]:
return [
ChatCompletionToolsParam(
type="function",
function={
"name": "get_current_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "The city name"},
"state": {"type": "string", "description": "The state code"},
"unit": {"type": "string", "enum": ["fahrenheit", "celsius"]},
},
"required": ["city", "state"],
},
},
),
ChatCompletionToolsParam(
type="function",
function={
"name": "calculate_area",
"description": "Calculate area of a shape",
"parameters": {
"type": "object",
"properties": {
"shape": {"type": "string"},
"dimensions": {"type": "object"},
"precision": {"type": "integer"},
},
},
},
),
]
def make_parser(tools=None) -> DeepSeekV4ToolParser:
return DeepSeekV4ToolParser(MOCK_TOKENIZER, tools=tools)
@@ -121,3 +167,39 @@ def test_streaming_extracts_complete_invokes():
]
assert names == ["search"]
assert json.loads(reconstruct_args(deltas)) == {"query": "deepseek v4"}
def test_get_vllm_registry_structural_tag_returns_structural_tag(
sample_tools: list[ChatCompletionToolsParam],
) -> None:
parser = make_parser()
req = ChatCompletionRequest(
messages=[],
model="m",
tools=sample_tools,
tool_choice="auto",
)
tag = parser.get_structural_tag(req)
assert isinstance(tag, StructuralTag)
req = ChatCompletionRequest(
messages=[],
model="m",
tools=sample_tools,
tool_choice="required",
)
tag = parser.get_structural_tag(req)
assert isinstance(tag, StructuralTag)
if sample_tools:
tool = sample_tools[0]
req = ChatCompletionRequest(
messages=[],
model="m",
tools=sample_tools,
)
req.tool_choice = ChatCompletionNamedToolChoiceParam(
function=ChatCompletionNamedFunction(name=tool.function.name)
)
tag = parser.get_structural_tag(req)
assert isinstance(tag, StructuralTag)
@@ -6,8 +6,11 @@ from collections.abc import Generator
import pytest
from openai.types.responses.function_tool import FunctionTool
from xgrammar import StructuralTag
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionNamedFunction,
ChatCompletionNamedToolChoiceParam,
ChatCompletionRequest,
ChatCompletionToolsParam,
)
@@ -108,6 +111,27 @@ def sample_tools(request):
]
def _as_chat_completion_tools(
tools: list[ChatCompletionToolsParam | FunctionTool],
) -> list[ChatCompletionToolsParam]:
normalized: list[ChatCompletionToolsParam] = []
for tool in tools:
if isinstance(tool, ChatCompletionToolsParam):
normalized.append(tool)
else:
normalized.append(
ChatCompletionToolsParam(
type="function",
function={
"name": tool.name,
"description": tool.description,
"parameters": tool.parameters,
},
)
)
return normalized
def assert_tool_calls(
actual_tool_calls: list[ToolCall], expected_tool_calls: list[ToolCall]
):
@@ -1146,3 +1170,88 @@ def test_no_double_serialization_string_args(qwen3_tool_parser):
args = json.loads(raw_arguments)
assert args["message"] == "hello world"
assert '\\"hello world\\"' not in raw_arguments
def test_get_vllm_registry_structural_tag_returns_structural_tag(
qwen3_tool_parser: Qwen3CoderToolParser,
sample_tools: list[ChatCompletionToolsParam],
) -> None:
request_tools = _as_chat_completion_tools(sample_tools)
req = ChatCompletionRequest(
messages=[],
model="m",
tools=request_tools,
tool_choice="auto",
)
tag = qwen3_tool_parser.get_structural_tag(req)
assert isinstance(tag, StructuralTag)
req = ChatCompletionRequest(
messages=[],
model="m",
tools=request_tools,
tool_choice="required",
)
tag = qwen3_tool_parser.get_structural_tag(req)
assert isinstance(tag, StructuralTag)
if request_tools:
tool = request_tools[0]
req = ChatCompletionRequest(
messages=[],
model="m",
tools=request_tools,
)
req.tool_choice = ChatCompletionNamedToolChoiceParam(
function=ChatCompletionNamedFunction(name=tool.function.name)
)
tag = qwen3_tool_parser.get_structural_tag(req)
assert isinstance(tag, StructuralTag)
@pytest.mark.parametrize("include_reasoning", [True, False])
def test_adjust_request_auto_uses_vllm_registry_structural_tag(
monkeypatch: pytest.MonkeyPatch,
qwen3_tool_parser: Qwen3CoderToolParser,
sample_tools: list[ChatCompletionToolsParam],
include_reasoning: bool,
) -> None:
monkeypatch.setattr(
"vllm.tool_parsers.abstract_tool_parser.VLLM_ENFORCE_STRICT_TOOL_CALLING",
True,
)
request_tools = _as_chat_completion_tools(sample_tools)
req = ChatCompletionRequest(
messages=[],
model="m",
tools=request_tools,
tool_choice="auto",
include_reasoning=include_reasoning,
)
out = qwen3_tool_parser.adjust_request(req)
assert out.structured_outputs is not None
assert out.structured_outputs.structural_tag is not None
assert isinstance(out.structured_outputs.structural_tag, str)
loaded = json.loads(out.structured_outputs.structural_tag)
assert isinstance(loaded, dict)
def test_adjust_request_required_prefers_structural_tag(
monkeypatch: pytest.MonkeyPatch,
qwen3_tool_parser: Qwen3CoderToolParser,
sample_tools: list[ChatCompletionToolsParam],
) -> None:
monkeypatch.setattr(
"vllm.tool_parsers.abstract_tool_parser.VLLM_ENFORCE_STRICT_TOOL_CALLING",
True,
)
request_tools = _as_chat_completion_tools(sample_tools)
req = ChatCompletionRequest(
messages=[],
model="m",
tools=request_tools,
tool_choice="required",
)
out = qwen3_tool_parser.adjust_request(req)
assert out.structured_outputs is not None
assert out.structured_outputs.structural_tag is not None
+126 -28
View File
@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import asyncio
import atexit
import contextlib
import copy
import functools
@@ -134,6 +135,11 @@ class RemoteVLLMServer:
"""
DUMMY_API_KEY = "token-abc123" # vLLM's OpenAI server does not need API key
_active_servers: set["RemoteVLLMServer"] = set()
_active_servers_lock = threading.RLock()
_cleanup_hooks_registered = False
_signal_hooks_registered = False
_previous_signal_handlers: dict[int, Any] = {}
proc: subprocess.Popen
def _create_cli_subcommand(self):
@@ -209,6 +215,7 @@ class RemoteVLLMServer:
)
self._pre_download_model(model, args)
self._shutdown_complete = False
# Record GPU memory before server start so we know what
# "released" looks like.
@@ -221,6 +228,7 @@ class RemoteVLLMServer:
)
self._start_server(model, vllm_serve_args, env_dict)
self._register_active_server()
max_wait_seconds = max_wait_seconds or 480
try:
self._wait_for_server(url=self.url_for("health"), timeout=max_wait_seconds)
@@ -246,8 +254,70 @@ class RemoteVLLMServer:
(when the server fails to start). Must be safe to call even if
the process is already dead.
"""
self._terminate_process_tree()
self._wait_for_gpu_memory_release()
if self._shutdown_complete:
return
self._shutdown_complete = True
try:
self._terminate_process_tree()
self._wait_for_gpu_memory_release()
finally:
self._unregister_active_server()
@classmethod
def _ensure_cleanup_hooks_registered(cls) -> None:
"""Register process-exit cleanup for detached server subprocesses."""
root_cls = RemoteVLLMServer
with root_cls._active_servers_lock:
if not root_cls._cleanup_hooks_registered:
atexit.register(root_cls._shutdown_active_servers)
root_cls._cleanup_hooks_registered = True
if (
threading.current_thread() is threading.main_thread()
and not root_cls._signal_hooks_registered
):
for signum in (signal.SIGTERM, signal.SIGINT):
root_cls._previous_signal_handlers[signum] = signal.getsignal(
signum
)
signal.signal(signum, root_cls._handle_parent_signal)
root_cls._signal_hooks_registered = True
def _register_active_server(self) -> None:
"""Track this server so parent-process exits still clean it up."""
RemoteVLLMServer._ensure_cleanup_hooks_registered()
with RemoteVLLMServer._active_servers_lock:
RemoteVLLMServer._active_servers.add(self)
def _unregister_active_server(self) -> None:
with RemoteVLLMServer._active_servers_lock:
RemoteVLLMServer._active_servers.discard(self)
@classmethod
def _shutdown_active_servers(cls) -> None:
"""Best-effort shutdown for all live RemoteVLLMServer instances."""
with cls._active_servers_lock:
servers = list(cls._active_servers)
for server in servers:
with contextlib.suppress(Exception):
server._shutdown()
@classmethod
def _handle_parent_signal(cls, signum, frame) -> None:
"""Clean up detached servers before letting the signal terminate pytest."""
cls._shutdown_active_servers()
previous_handler = cls._previous_signal_handlers.get(signum, signal.SIG_DFL)
if callable(previous_handler):
previous_handler(signum, frame)
elif previous_handler == signal.SIG_IGN:
return
elif signum == signal.SIGINT:
raise KeyboardInterrupt
else:
raise SystemExit(128 + signum)
def _terminate_process_tree(self) -> None:
"""Kill the server process tree without waiting for GPU memory release.
@@ -315,6 +385,9 @@ class RemoteVLLMServer:
if not servers:
return
for server in servers:
server._shutdown_complete = True
threads = [
threading.Thread(
target=s._terminate_process_tree,
@@ -339,7 +412,11 @@ class RemoteVLLMServer:
else s._pre_server_gpu_memory
),
)
earliest._wait_for_gpu_memory_release()
try:
earliest._wait_for_gpu_memory_release()
finally:
for server in servers:
server._unregister_active_server()
def _kill_process_group_survivors(
self, pgid: int | None, timeout: float = 15.0
@@ -705,6 +782,7 @@ def _test_completion(
model: str,
prompt: str,
token_ids: list[int],
include_seeded_sampling: bool = True,
):
results = []
@@ -739,33 +817,40 @@ def _test_completion(
}
)
# test seeded random sampling
completion = client.completions.create(
model=model, prompt=prompt, max_tokens=5, seed=33, temperature=1.0
)
if include_seeded_sampling:
# test seeded random sampling
completion = client.completions.create(
model=model, prompt=prompt, max_tokens=5, seed=33, temperature=1.0
)
results.append(
{
"test": "seeded_sampling",
"text": completion.choices[0].text,
"finish_reason": completion.choices[0].finish_reason,
"usage": completion.usage,
}
)
results.append(
{
"test": "seeded_sampling",
"text": completion.choices[0].text,
"finish_reason": completion.choices[0].finish_reason,
"usage": completion.usage,
}
)
# test seeded random sampling with multiple prompts
completion = client.completions.create(
model=model, prompt=[prompt, prompt], max_tokens=5, seed=33, temperature=1.0
)
# test seeded random sampling with multiple prompts
completion = client.completions.create(
model=model,
prompt=[prompt, prompt],
max_tokens=5,
seed=33,
temperature=1.0,
)
results.append(
{
"test": "seeded_sampling",
"text": [choice.text for choice in completion.choices],
"finish_reason": [choice.finish_reason for choice in completion.choices],
"usage": completion.usage,
}
)
results.append(
{
"test": "seeded_sampling",
"text": [choice.text for choice in completion.choices],
"finish_reason": [
choice.finish_reason for choice in completion.choices
],
"usage": completion.usage,
}
)
# test simple list
batch = client.completions.create(
@@ -960,6 +1045,7 @@ def compare_two_settings(
*,
method: str = "generate",
max_wait_seconds: float | None = None,
include_seeded_sampling: bool = True,
) -> None:
"""
Launch API server with two different sets of arguments/environments
@@ -971,6 +1057,8 @@ def compare_two_settings(
arg2: The second set of arguments to pass to the API server.
env1: The first set of environment variables to pass to the API server.
env2: The second set of environment variables to pass to the API server.
include_seeded_sampling: Whether to include temperature=1.0 seeded
sampling checks in the default generate comparison.
"""
compare_all_settings(
@@ -979,6 +1067,7 @@ def compare_two_settings(
[env1, env2],
method=method,
max_wait_seconds=max_wait_seconds,
include_seeded_sampling=include_seeded_sampling,
)
@@ -989,6 +1078,7 @@ def compare_all_settings(
*,
method: str = "generate",
max_wait_seconds: float | None = None,
include_seeded_sampling: bool = True,
) -> None:
"""
Launch API server with several different sets of arguments/environments
@@ -997,6 +1087,8 @@ def compare_all_settings(
model: The model to test.
all_args: A list of argument lists to pass to the API server.
all_envs: A list of environment dictionaries to pass to the API server.
include_seeded_sampling: Whether to include temperature=1.0 seeded
sampling checks in the default generate comparison.
"""
trust_remote_code = False
@@ -1057,7 +1149,13 @@ def compare_all_settings(
)
if method == "generate":
results += _test_completion(client, model, prompt, token_ids)
results += _test_completion(
client,
model,
prompt,
token_ids,
include_seeded_sampling=include_seeded_sampling,
)
elif method == "generate_close":
results += _test_completion_close(client, model, prompt)
elif method == "generate_chat":
@@ -0,0 +1,162 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for canonicalize_singleton_dim_strides.
Background
----------
When num_kv_heads_per_rank == 1 (e.g. Qwen3.5-397B with TP=8 → 1 KV head
per rank), PyTorch's is_contiguous() returns True for *any* stride on the
size-1 dimension. The KV cache allocator can therefore produce a tensor
where that singleton dim has stride = 1 element (2 bytes for bf16) instead
of the canonical product-of-remaining-dims value.
CUDA TMA (used by FlashInfer XQA SM90 and Flash-Attention 3/4 on H100+)
requires all non-outermost strides to be multiples of 16 bytes. A 2-byte
stride triggers cudaErrorIllegalInstruction.
canonicalize_singleton_dim_strides() patches degenerate strides on all
size-1 dimensions via torch.as_strided — zero-copy.
The degenerate stride manifests at different positions in different backends:
- FlashInfer: stride(-3) after kv_cache.permute() → shape [..., 1, B, D]
- FlashAttention: stride(-2) after kv_cache.unbind(0) → shape [N, B, 1, D]
"""
import torch
from vllm.utils.torch_utils import canonicalize_singleton_dim_strides
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _inject_degenerate_stride(t: torch.Tensor, dim: int) -> torch.Tensor:
"""Return a view of t with a degenerate (stride=1) on a size-1 dim."""
assert t.shape[dim] == 1, f"dim {dim} must have size 1"
strides = list(t.stride())
strides[dim] = 1 # inject the bug
return t.as_strided(t.shape, strides)
# ---------------------------------------------------------------------------
# Tests: canonicalize_singleton_dim_strides
# ---------------------------------------------------------------------------
class TestCanonicalizeSingletonDimStrides:
def test_flashinfer_layout_dim_neg3(self):
"""FlashInfer path: degenerate stride at dim -3 (num_kv_heads)."""
# Shape after permute: [num_blocks, 2, num_kv_heads, block_size, head_size]
num_blocks, block_size, head_size = 64, 16, 128
t = torch.zeros(num_blocks, 2, 1, block_size, head_size, dtype=torch.bfloat16)
t_deg = _inject_degenerate_stride(t, dim=-3)
assert t_deg.stride(-3) == 1 # confirm degenerate
assert t_deg.is_contiguous() # PyTorch doesn't notice
fixed = canonicalize_singleton_dim_strides(t_deg)
assert fixed.stride(-3) == block_size * head_size # canonical = 2048
assert fixed.stride(-2) == head_size # inner dims unchanged
assert fixed.stride(-1) == 1
def test_flash_attn_layout_dim_neg2(self):
"""FlashAttention path: degenerate stride at dim -2 (num_kv_heads)."""
# Shape after unbind(0): [num_blocks, block_size, num_kv_heads, head_size]
num_blocks, block_size, head_size = 64, 16, 128
t = torch.zeros(num_blocks, block_size, 1, head_size, dtype=torch.bfloat16)
t_deg = _inject_degenerate_stride(t, dim=-2)
assert t_deg.stride(-2) == 1
assert t_deg.is_contiguous()
fixed = canonicalize_singleton_dim_strides(t_deg)
assert fixed.stride(-2) == head_size # canonical = 128
assert fixed.stride(-1) == 1
def test_canonical_strides_returned_as_is(self):
"""No degenerate strides → same object returned (no copy, no new view)."""
t = torch.zeros(64, 2, 1, 16, 128, dtype=torch.bfloat16)
result = canonicalize_singleton_dim_strides(t)
assert result is t
def test_multi_kv_heads_unchanged(self):
"""num_kv_heads > 1 → strides are already canonical → unchanged."""
t = torch.zeros(16, 2, 4, 16, 128, dtype=torch.bfloat16)
original_strides = t.stride()
result = canonicalize_singleton_dim_strides(t)
assert result.stride() == original_strides
def test_data_pointer_preserved(self):
"""Fix is zero-copy: same underlying storage."""
t = torch.zeros(8, 2, 1, 16, 128, dtype=torch.bfloat16)
t_deg = _inject_degenerate_stride(t, dim=-3)
fixed = canonicalize_singleton_dim_strides(t_deg)
assert fixed.data_ptr() == t_deg.data_ptr()
assert fixed.storage_offset() == t_deg.storage_offset()
def test_multiple_singleton_dims(self):
"""All size-1 dims with degenerate strides are fixed."""
# Shape: [1, 1, 8, 32] — two size-1 dims
t = torch.zeros(1, 1, 8, 32, dtype=torch.float16)
# Both size-1 dims get degenerate strides
t_deg = t.as_strided(t.shape, (1, 1, 32, 1)) # both leading dims = 1
fixed = canonicalize_singleton_dim_strides(t_deg)
assert fixed.stride(0) == 1 * 8 * 32 # canonical: 256
assert fixed.stride(1) == 1 * 8 * 32 # canonical: 256 (same since size-1)
assert fixed.stride(2) == 32
assert fixed.stride(3) == 1
def test_various_shapes_flashinfer(self):
"""Correctness across different block_size / head_size for FlashInfer layout."""
for block_size, head_size in [(16, 64), (16, 128), (32, 128), (16, 256)]:
t = torch.zeros(8, 2, 1, block_size, head_size, dtype=torch.bfloat16)
t_deg = _inject_degenerate_stride(t, dim=-3)
fixed = canonicalize_singleton_dim_strides(t_deg)
assert fixed.stride(-3) == block_size * head_size, (
f"Failed for block_size={block_size}, head_size={head_size}: "
f"got stride(-3)={fixed.stride(-3)}"
)
def test_various_shapes_flash_attn(self):
"""Correctness across different shapes for FlashAttention layout."""
for block_size, head_size in [(16, 64), (16, 128), (32, 128)]:
t = torch.zeros(8, block_size, 1, head_size, dtype=torch.bfloat16)
t_deg = _inject_degenerate_stride(t, dim=-2)
fixed = canonicalize_singleton_dim_strides(t_deg)
assert fixed.stride(-2) == head_size, (
f"Failed for block_size={block_size}, head_size={head_size}: "
f"got stride(-2)={fixed.stride(-2)}"
)
def test_tma_alignment_satisfied_after_fix_bf16(self):
"""After fix, all strides meet 16-byte TMA alignment for bf16."""
t = torch.zeros(64, 2, 1, 16, 128, dtype=torch.bfloat16)
t_deg = _inject_degenerate_stride(t, dim=-3)
fixed = canonicalize_singleton_dim_strides(t_deg)
element_size = fixed.element_size() # 2 bytes for bf16
for i, s in enumerate(fixed.stride()):
assert (s * element_size) % 16 == 0 or i == len(fixed.stride()) - 1, (
f"dim {i} stride {s} * {element_size} bytes not 16-byte aligned"
)
def test_non_contiguous_outer_dims_preserved(self):
"""Outer (non-size-1) non-contiguous strides are left unchanged."""
# Simulate cross-layer unified allocation: num_blocks stride is non-canonical
# but the inner dims should be fixed.
base = torch.zeros(200, 2, 1, 16, 128, dtype=torch.bfloat16)
# Slice every 2nd block → non-canonical outer stride
t_sliced = base[::2] # shape [100, 2, 1, 16, 128], stride[0] = 2*canonical
t_deg = _inject_degenerate_stride(t_sliced, dim=-3)
fixed = canonicalize_singleton_dim_strides(t_deg)
# Outer stride should be unchanged (not a size-1 dim)
assert fixed.stride(0) == t_sliced.stride(0)
# Inner degenerate stride should be fixed
assert fixed.stride(-3) == 16 * 128
+10 -1
View File
@@ -57,6 +57,8 @@ def test_without_spec_decoding(
dict(bad_words=["the", " the"]),
dict(logprobs=2),
dict(logprobs=2, frequency_penalty=-1.0),
dict(prompt_logprobs=2),
dict(prompt_logprobs=2, logprobs=2),
dict(structured_outputs=struct_outputs),
dict(
structured_outputs=struct_outputs,
@@ -126,6 +128,8 @@ def test_with_eagle3_spec_decoding(sample_json_schema, monkeypatch: pytest.Monke
dict(bad_words=["the", " the"]),
dict(logprobs=2),
dict(logprobs=2, frequency_penalty=-1.0),
dict(prompt_logprobs=2),
dict(prompt_logprobs=2, logprobs=2),
dict(structured_outputs=struct_outputs),
dict(
structured_outputs=struct_outputs,
@@ -413,7 +417,12 @@ def _all_logprobs_match(req_a, req_b) -> bool:
)
def _logprobs_match(lps_a: dict[int, Logprob], lps_b: dict[int, Logprob]) -> bool:
def _logprobs_match(
lps_a: dict[int, Logprob] | None,
lps_b: dict[int, Logprob] | None,
) -> bool:
if lps_a is None or lps_b is None:
return lps_a is lps_b
rel_tol, abs_tol = 1e-3, 1e-6
return (
len(lps_a) == len(lps_b)
+16 -19
View File
@@ -33,11 +33,10 @@ PROMPT = BatchLogprobsComposition.PROMPT
SAMPLE_PROMPT = BatchLogprobsComposition.SAMPLE_PROMPT
# On ROCm, floating-point reductions in attention and GEMM kernels are
# non-associative and sensitive to batch geometry. The ref LLM (no spec
# decode, default scheduling) and the spec-decode LLM (chunked prefill,
# different effective batch sizes) follow different reduction orders,
# producing numerically divergent logprobs that get misattributed to
# spec-decode incorrectness.
# non-associative and sensitive to batch geometry. If the ref LLM and
# spec-decode LLM use different scheduling or batch geometry, they can
# follow different reduction orders and produce numerically divergent
# logprobs that get misattributed to spec-decode incorrectness.
#
# Force LLM instances into an identical, deterministic execution
# mode so the test isolates spec-decode correctness only:
@@ -1086,18 +1085,25 @@ def test_spec_decode_logprobs(
)
max_model_len = 256
# Run base LLM.
ref_llm = LLM(
model=model_name,
llm_kwargs = dict(
max_logprobs=5,
max_model_len=max_model_len,
seed=42,
logprobs_mode=logprobs_mode,
gpu_memory_utilization=0.4,
# Force the same prefill chunking for both the base model and
# spec decode model so the comparison isolates spec decode.
enable_chunked_prefill=True,
max_num_batched_tokens=32,
enable_prefix_caching=False,
**ROCM_DETERMINISM_KWARGS,
)
# Run base LLM.
ref_llm = LLM(
model=model_name,
**llm_kwargs,
)
ref_results = ref_llm.generate(
[prompt, prompt], [sampling_params, penalty_sampling_params]
)
@@ -1117,16 +1123,7 @@ def test_spec_decode_logprobs(
spec_llm = LLM(
model_name,
speculative_config=spec_config_with_len,
max_logprobs=5,
max_model_len=max_model_len,
seed=42,
logprobs_mode=logprobs_mode,
gpu_memory_utilization=0.4,
# Force prefill chunking
enable_chunked_prefill=True,
max_num_batched_tokens=32,
enable_prefix_caching=False,
**ROCM_DETERMINISM_KWARGS,
**llm_kwargs,
)
spec_results = spec_llm.generate(
[prompt, prompt], [sampling_params, penalty_sampling_params]
@@ -810,6 +810,9 @@ def analyze_backend(backend_name: str, class_path: str) -> dict[str, Any] | None
"compute_capability": compute_cap,
"is_mla": is_mla_backend or check_method_overrides(class_node, "is_mla"),
"supports_sink": check_method_overrides(class_node, "supports_sink"),
"supports_non_causal": check_method_overrides(
class_node, "supports_non_causal"
),
"is_sparse": check_method_overrides(class_node, "is_sparse"),
"supports_mm_prefix": check_method_overrides(class_node, "supports_mm_prefix"),
"supports_dcp": supports_dcp,
@@ -1311,6 +1314,10 @@ _COL_KV_DTYPES: TableColumn = (
_COL_BLOCK_SIZES: TableColumn = ("Block Sizes", lambda b: b["block_sizes"])
_COL_HEAD_SIZES: TableColumn = ("Head Sizes", lambda b: b["head_sizes"])
_COL_SINK: TableColumn = ("Sink", lambda b: bool_to_emoji(b["supports_sink"]))
_COL_NON_CAUSAL: TableColumn = (
"Non-Causal",
lambda b: bool_to_emoji(b["supports_non_causal"]),
)
_COL_SPARSE: TableColumn = ("Sparse", lambda b: bool_to_emoji(b["is_sparse"]))
_COL_MM_PREFIX: TableColumn = (
"MM Prefix",
@@ -1344,6 +1351,7 @@ def _build_columns(is_mla: bool, has_versions: bool) -> list[TableColumn]:
cols.append(_COL_VERSION)
cols.extend([_COL_DTYPES, _COL_KV_DTYPES, _COL_BLOCK_SIZES, _COL_HEAD_SIZES])
cols.append(_COL_SINK)
cols.append(_COL_NON_CAUSAL)
if is_mla:
cols.append(_COL_SPARSE)
cols.extend([_COL_MM_PREFIX, _COL_DCP, _COL_ATTN_TYPES, _COL_COMPUTE_CAP])
@@ -1554,6 +1562,7 @@ def generate_legend() -> str:
| **Block Sizes** | Supported KV cache block sizes (%N means multiples of N) |
| **Head Sizes** | Supported attention head sizes |
| **Sink** | Attention sink support (for StreamingLLM) |
| **Non-Causal** | Non-causal (bidirectional) attention support for decoder models |
| **Sparse** | Sparse attention support (MLA only) |
| **MM Prefix** | Multimodal prefix full attention support |
| **DCP** | Decode Context Parallelism support (`--decode-context-parallel-size`) |
+6 -2
View File
@@ -1803,7 +1803,9 @@ def get_samples(args, tokenizer: TokenizerLike) -> list[SampleRequest]:
if args.dataset_name == "custom":
dataset = CustomDataset(
dataset_path=args.dataset_path, disable_shuffle=args.disable_shuffle
dataset_path=args.dataset_path,
disable_shuffle=args.disable_shuffle,
random_seed=args.seed,
)
input_requests = dataset.sample(
num_requests=args.num_prompts,
@@ -1816,7 +1818,9 @@ def get_samples(args, tokenizer: TokenizerLike) -> list[SampleRequest]:
elif args.dataset_name == "custom_mm":
dataset = CustomMMDataset(
dataset_path=args.dataset_path, disable_shuffle=args.disable_shuffle
dataset_path=args.dataset_path,
disable_shuffle=args.disable_shuffle,
random_seed=args.seed,
)
input_requests = dataset.sample(
num_requests=args.num_prompts,
+7 -3
View File
@@ -209,7 +209,9 @@ OPTIMIZATION_LEVEL_01 = {
"use_inductor_graph_partition": False,
},
"kernel_config": {
"enable_flashinfer_autotune": True,
# Disabled for now due to correctness issues:
# https://github.com/flashinfer-ai/flashinfer/issues/3197
"enable_flashinfer_autotune": False,
},
}
OPTIMIZATION_LEVEL_02 = {
@@ -229,7 +231,9 @@ OPTIMIZATION_LEVEL_02 = {
"use_inductor_graph_partition": False,
},
"kernel_config": {
"enable_flashinfer_autotune": True,
# Disabled for now due to correctness issues:
# https://github.com/flashinfer-ai/flashinfer/issues/3197
"enable_flashinfer_autotune": False,
},
}
OPTIMIZATION_LEVEL_03 = {
@@ -1613,7 +1617,7 @@ class VllmConfig:
max_size = rocm_aiter_ops.get_aiter_allreduce_max_size()
else:
max_size = compilation_config.pass_config.flashinfer_max_size(tp_size)
if max_size is not None:
if max_size is not None and self.model_config is not None:
assert isinstance(self.model_config.dtype, torch.dtype)
max_token_num = max_size // (
self.model_config.get_hidden_size()
+15
View File
@@ -321,6 +321,21 @@ async def init_app_state(
supported_tasks: tuple["SupportedTask", ...] | None = None,
) -> None:
vllm_config = engine_client.vllm_config
# Propagate enable_in_reasoning to the API-server process. The engine core
# runs in a separate process, so the contextvar that backs
# `get_current_vllm_config_or_none()` is None on this stack. Tool parsers
# call `get_enable_structured_outputs_in_reasoning()` during request
# handling and need to see the real flag, otherwise they silently fall
# back to False and mismatch the engine-side bitmask gating.
from vllm.tool_parsers.structural_tag_registry import (
set_enable_structured_outputs_in_reasoning,
)
set_enable_structured_outputs_in_reasoning(
vllm_config.structured_outputs_config.enable_in_reasoning
)
if supported_tasks is None:
warnings.warn(
"The 'supported_tasks' parameter was not provided to "
+7
View File
@@ -226,6 +226,7 @@ if TYPE_CHECKING:
VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS: bool = False
VLLM_SYSTEM_START_DATE: str | None = None
VLLM_TOOL_JSON_ERROR_AUTOMATIC_RETRY: bool = False
VLLM_ENFORCE_STRICT_TOOL_CALLING: bool = False
VLLM_CUSTOM_SCOPES_FOR_PROFILING: bool = False
VLLM_NVTX_SCOPES_FOR_PROFILING: bool = False
VLLM_KV_EVENTS_USE_INT_BLOCK_HASHES: bool = True
@@ -1593,6 +1594,12 @@ environment_variables: dict[str, Callable[[], Any]] = {
"VLLM_TOOL_JSON_ERROR_AUTOMATIC_RETRY": lambda: bool(
int(os.getenv("VLLM_TOOL_JSON_ERROR_AUTOMATIC_RETRY", "0"))
),
# When 1,the model structural tags will be used to enforce the model
# output conforming to the model's tool-calling format and schema.
# Default 0 (off).
"VLLM_ENFORCE_STRICT_TOOL_CALLING": lambda: bool(
int(os.getenv("VLLM_ENFORCE_STRICT_TOOL_CALLING", "0"))
),
# Add optional custom scopes for profiling, disable to avoid overheads
"VLLM_CUSTOM_SCOPES_FOR_PROFILING": lambda: bool(
int(os.getenv("VLLM_CUSTOM_SCOPES_FOR_PROFILING", "0"))
@@ -381,7 +381,7 @@ def convert_to_nvfp4_moe_kernel_format(
elif nvfp4_backend == NvFp4MoeBackend.EMULATION:
# Move the E2M1 lookup table to the device now, because
# `.to(device)` is not allowed during CUDA graph capture.
kE2M1ToFloat_handle.val = kE2M1ToFloat_handle.val.to(layer.weight.device)
kE2M1ToFloat_handle.val = kE2M1ToFloat_handle.val.to(w13.device)
if a13_scale is None or a2_scale is None:
raise ValueError(
@@ -52,8 +52,8 @@ class AllPool(TokenPoolingMethod):
# DispatchPooler passes the full hidden_states tensor.
# slice out the subgroup once, then split it by
# per-request token counts
group_start = int(pooling_cursor.first_token_indices_gpu[0].item())
group_end = int(pooling_cursor.last_token_indices_gpu[-1].item()) + 1
group_start = int(pooling_cursor.first_token_indices_cpu[0])
group_end = group_start + sum(split_sizes)
hidden_states_group = hidden_states[group_start:group_end]
hidden_states_lst = list(hidden_states_group.split(split_sizes))
else:
@@ -102,7 +102,7 @@ def materialize_layer(layer: torch.nn.Module, info: LayerReloadingInfo):
with info.restore_device:
for name, tensor in get_layer_tensors(layer).items():
if name not in SKIP_TENSORS:
if name not in SKIP_TENSORS and tensor.is_meta:
setattr(layer, name, materialize_meta_tensor(tensor))
+15 -3
View File
@@ -1499,6 +1499,11 @@ class NemotronH_Nano_VL_V2(
return self.language_model.compute_logits(hidden_states)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
mm_config = self.model_config.multimodal_config
load_multimodal_weights = not all(
mm_config.get_limit_per_prompt(modality) == 0
for modality in ("image", "video", "audio")
)
adapter_dict = dict(self.mlp1.named_parameters())
def is_llm(name: str) -> bool:
@@ -1523,23 +1528,30 @@ class NemotronH_Nano_VL_V2(
# Strip 'language_model.' prefix for LLM weights
llm_weights.append((".".join(name.split(".")[1:]), w))
elif is_adapter_weights((name, w)):
if not load_multimodal_weights:
continue
# Load vision-language adapter weights directly
trimmed_name = ".".join(name.split(".")[1:])
param = adapter_dict[trimmed_name]
with torch.no_grad():
default_weight_loader(param, w)
elif is_vision_weights(name):
if not load_multimodal_weights:
continue
# Convert: vision_model.radio_model.* → radio_model.*
hf_key = name[len("vision_model.") :] # Remove "vision_model." prefix
vision_weights.append((hf_key, w))
elif is_sound_weights(name):
if not load_multimodal_weights:
continue
assert self.sound_encoder is not None
sound_weights.append((name, w))
self.language_model.load_weights(llm_weights)
self.vision_model.load_weights(vision_weights)
if self.sound_encoder is not None and len(sound_weights) > 0:
self.sound_encoder.load_weights(sound_weights)
if load_multimodal_weights:
self.vision_model.load_weights(vision_weights)
if self.sound_encoder is not None and len(sound_weights) > 0:
self.sound_encoder.load_weights(sound_weights)
def get_vit_model_from_radio_config(self, hf_config):
hf_config_vision = hf_config.vision_config
@@ -221,6 +221,10 @@ class KimiK2ReasoningParser(ReasoningParser):
return None
if self._end_token_id in delta_token_ids:
if self._end_token not in delta_text:
# Token ID arrived before text was flushed (stop-sequence buffering).
# Wait for the next delta when the text becomes visible.
return None
end_index = delta_text.find(self._end_token)
reasoning = delta_text[:end_index]
content = delta_text[end_index + len(self._end_token) :]
@@ -229,6 +233,9 @@ class KimiK2ReasoningParser(ReasoningParser):
)
if self._tool_section_start_token_id in delta_token_ids:
if self._tool_section_start_token not in delta_text:
# Token ID arrived before text was flushed (stop-sequence buffering).
return None
tool_index = delta_text.find(self._tool_section_start_token)
reasoning = delta_text[:tool_index]
content = delta_text[tool_index:]
+36 -4
View File
@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import importlib
import json
import os
from collections.abc import Callable, Sequence
from functools import cached_property
@@ -13,6 +14,7 @@ from openai.types.responses import (
from openai.types.responses.function_tool import FunctionTool
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionNamedToolChoiceParam,
ChatCompletionRequest,
ChatCompletionToolsParam,
)
@@ -23,6 +25,7 @@ from vllm.entrypoints.openai.engine.protocol import (
from vllm.entrypoints.openai.responses.protocol import (
ResponsesRequest,
)
from vllm.envs import VLLM_ENFORCE_STRICT_TOOL_CALLING
from vllm.logger import init_logger
from vllm.sampling_params import (
StructuredOutputsParams,
@@ -83,13 +86,39 @@ class ToolParser:
return self.model_tokenizer.get_vocab()
def adjust_request(
self, request: ChatCompletionRequest | ResponsesRequest
self,
request: ChatCompletionRequest | ResponsesRequest,
) -> ChatCompletionRequest | ResponsesRequest:
"""
Static method that used to adjust the request parameters.
"""
# If there are no tools, return the request as is.
if not request.tools:
return request
# Step 1 (highest priority for ChatCompletionRequest): apply
# vLLM-owned structural tag support for model-specific tool formats.
if (
isinstance(request, ChatCompletionRequest)
and VLLM_ENFORCE_STRICT_TOOL_CALLING
):
need_tool_calling = (
request.tool_choice == "auto"
or request.tool_choice == "required"
or isinstance(request.tool_choice, ChatCompletionNamedToolChoiceParam)
)
if need_tool_calling:
structure_tag = self.get_structural_tag(request)
if structure_tag is not None:
if request.structured_outputs is None:
request.structured_outputs = StructuredOutputsParams(
structural_tag=json.dumps(structure_tag.model_dump()),
)
else:
request.structured_outputs.structural_tag = json.dumps(
structure_tag.model_dump()
)
return request
# Step 2: set structured output params when tool constraints are
# derived from the tool schema.
json_schema_from_tool = get_json_schema_from_tools(
tool_choice=request.tool_choice, tools=request.tools
)
@@ -121,6 +150,9 @@ class ToolParser:
return request
def get_structural_tag(self, request: ChatCompletionRequest):
return None
def extract_tool_calls(
self, model_output: str, request: ChatCompletionRequest
) -> ExtractedToolCallInformation:
@@ -1,7 +1,14 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionRequest,
)
from vllm.tool_parsers.deepseekv32_tool_parser import DeepSeekV32ToolParser
from vllm.tool_parsers.structural_tag_registry import (
get_enable_structured_outputs_in_reasoning,
get_model_structural_tag,
)
class DeepSeekV4ToolParser(DeepSeekV32ToolParser):
@@ -14,3 +21,11 @@ class DeepSeekV4ToolParser(DeepSeekV32ToolParser):
tool_call_start_token: str = "<DSMLtool_calls>"
tool_call_end_token: str = "</DSMLtool_calls>"
def get_structural_tag(self, request: ChatCompletionRequest):
return get_model_structural_tag(
model="deepseek_v4",
tools=request.tools,
tool_choice=request.tool_choice,
reasoning=get_enable_structured_outputs_in_reasoning(),
)
@@ -25,12 +25,18 @@ from vllm.tool_parsers.abstract_tool_parser import (
Tool,
ToolParser,
)
from vllm.tool_parsers.structural_tag_registry import (
get_enable_structured_outputs_in_reasoning,
get_model_structural_tag,
)
from vllm.tool_parsers.utils import find_tool_properties
logger = init_logger(__name__)
class Qwen3CoderToolParser(ToolParser):
supports_required_and_named: bool = False
def __init__(self, tokenizer: TokenizerLike, tools: list[Tool] | None = None):
super().__init__(tokenizer, tools)
@@ -681,3 +687,11 @@ class Qwen3CoderToolParser(ToolParser):
return result
return None
def get_structural_tag(self, request: ChatCompletionRequest):
return get_model_structural_tag(
model="qwen_3_5",
tools=request.tools,
tool_choice=request.tool_choice,
reasoning=get_enable_structured_outputs_in_reasoning(),
)
@@ -0,0 +1,330 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# Model-specific structural tag builders adapted from XGrammar's
# builtin structural tag implementations:
# https://github.com/mlc-ai/xgrammar/blob/main/python/xgrammar/builtin_structural_tag.py
from collections.abc import Callable
from typing import Any, Literal
from xgrammar import StructuralTag
from xgrammar.structural_tag import (
AnyTextFormat,
ConstStringFormat,
JSONSchemaFormat,
SequenceFormat,
TagFormat,
TagsWithSeparatorFormat,
TriggeredTagsFormat,
)
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionNamedToolChoiceParam,
ChatCompletionToolsParam,
)
SimplifiedToolChoice = Literal["auto", "required", "forced"]
ToolChoice = (
Literal["none", "auto", "required"] | ChatCompletionNamedToolChoiceParam | None
)
StructuralTagBuilder = Callable[
[list[ChatCompletionToolsParam], SimplifiedToolChoice, bool],
StructuralTag,
]
_structural_tag_registry: dict[str, StructuralTagBuilder] = {}
def register_model_structural_tag(name: str):
"""Register a vLLM-owned model-specific structural tag builder."""
def decorator(func: StructuralTagBuilder) -> StructuralTagBuilder:
_structural_tag_registry[name] = func
return func
return decorator
def get_model_structural_tag(
model: str,
tools: list[ChatCompletionToolsParam] | None,
tool_choice: ToolChoice,
reasoning: bool,
) -> StructuralTag | None:
"""Build a structural tag from vLLM-owned model-specific builders."""
builder = _structural_tag_registry.get(model)
if builder is None:
supported = list(_structural_tag_registry.keys())
raise ValueError(f"Unknown format type: {model}, supported types: {supported}")
normalized_tools, simplified_tool_choice = _normalize_tool_choice(
tools=tools,
tool_choice=tool_choice,
)
if not normalized_tools:
return None
return builder(normalized_tools, simplified_tool_choice, reasoning)
def _normalize_tool_choice(
tools: list[ChatCompletionToolsParam] | None,
tool_choice: ToolChoice,
) -> tuple[list[ChatCompletionToolsParam], SimplifiedToolChoice]:
"""Normalize vLLM ChatCompletion tool_choice for structural tag builders."""
if not tools:
return [], "auto"
if tool_choice is None or tool_choice == "none":
return [], "auto"
if tool_choice == "auto":
return tools, "auto"
if tool_choice == "required":
return tools, "required"
if isinstance(tool_choice, ChatCompletionNamedToolChoiceParam):
tool_name = tool_choice.function.name
filtered_tools = [tool for tool in tools if tool.function.name == tool_name]
if not filtered_tools:
raise ValueError(
f"The tool with name '{tool_name}' is not found in the tools list."
)
return filtered_tools, "forced"
raise ValueError(f"Unsupported tool_choice for structural tag: {tool_choice}")
def _get_function_parameters(function: Any) -> dict[str, Any] | bool:
"""Return the JSON schema used for constrained tool arguments."""
if getattr(function, "strict", None) is False:
return True
if function.parameters is None:
return True
return function.parameters
_enable_structured_outputs_in_reasoning: bool = False
def set_enable_structured_outputs_in_reasoning(enabled: bool) -> None:
"""Publish the engine's ``enable_in_reasoning`` flag to tool parsers.
Called once during APIServer startup so request-time parsers can read
it without going through the EngineCore-only contextvar.
"""
global _enable_structured_outputs_in_reasoning
_enable_structured_outputs_in_reasoning = bool(enabled)
def get_enable_structured_outputs_in_reasoning() -> bool:
"""Whether structured outputs are active during the reasoning phase.
When ``True``, the structural tag will cover the reasoning part:
``<think>...</think>`` prefix (if available); when ``False`` (default), the tag only
constrains the post-reasoning suffix.
"""
return _enable_structured_outputs_in_reasoning
@register_model_structural_tag("deepseek_v4")
def get_deepseek_v4_structural_tag(
tools: list[ChatCompletionToolsParam],
tool_choice: SimplifiedToolChoice,
reasoning: bool,
) -> StructuralTag:
"""Build DeepSeek V4 structural tags."""
invoke_begin_prefix = '<DSMLinvoke name="'
invoke_begin_suffix = '">\n'
invoke_end = "</DSMLinvoke>\n"
tool_calls_prefix = "\n\n"
function_calls_begin = "<DSMLtool_calls>\n"
function_calls_end = "</DSMLtool_calls>"
function_calls_trigger = "<DSMLtool_calls>"
think_tag_end = "</think>"
think_exclude_tokens = ["<think>", "</think>"]
xml_style = "deepseek_xml"
if tool_choice == "auto":
tags = []
for tool in tools:
function = tool.function
parameters = _get_function_parameters(function)
tags.append(
TagFormat(
begin=invoke_begin_prefix + function.name + invoke_begin_suffix,
content=JSONSchemaFormat(
json_schema=parameters,
style=xml_style,
),
end=invoke_end,
)
)
if tags:
function_calling_tags = TagsWithSeparatorFormat(
tags=tags,
separator="\n",
at_least_one=True,
)
suffix_tag = TriggeredTagsFormat(
triggers=[function_calls_trigger],
tags=[
TagFormat(
begin=function_calls_begin,
content=function_calling_tags,
end=function_calls_end,
)
],
excludes=think_exclude_tokens,
)
else:
suffix_tag = AnyTextFormat(excludes=think_exclude_tokens)
elif tool_choice == "forced":
if not tools:
raise ValueError("Forced tool choice must resolve to exactly one tool.")
function = tools[0].function
suffix_tag = SequenceFormat(
elements=[
ConstStringFormat(value=tool_calls_prefix + function_calls_begin),
TagFormat(
begin=invoke_begin_prefix + function.name + invoke_begin_suffix,
content=JSONSchemaFormat(
json_schema=_get_function_parameters(function),
style=xml_style,
),
end=invoke_end,
),
ConstStringFormat(value=function_calls_end),
]
)
elif tool_choice == "required":
tags = []
for tool in tools:
function = tool.function
parameters = _get_function_parameters(function)
tags.append(
TagFormat(
begin=invoke_begin_prefix + function.name + invoke_begin_suffix,
content=JSONSchemaFormat(
json_schema=parameters,
style=xml_style,
),
end=invoke_end,
)
)
assert len(tags) > 0
suffix_tag = SequenceFormat(
elements=[
ConstStringFormat(value=tool_calls_prefix + function_calls_begin),
TagsWithSeparatorFormat(
tags=tags,
separator="\n",
at_least_one=True,
),
ConstStringFormat(value=function_calls_end),
]
)
if not reasoning:
return StructuralTag(format=suffix_tag)
prefix_tag = TagFormat(begin="", content=AnyTextFormat(), end=think_tag_end)
return StructuralTag(format=SequenceFormat(elements=[prefix_tag, suffix_tag]))
@register_model_structural_tag("qwen_3_5")
def get_qwen_3_5_structural_tag(
tools: list[ChatCompletionToolsParam],
tool_choice: SimplifiedToolChoice,
reasoning: bool,
) -> StructuralTag:
"""Build Qwen XML structural tags.
This format is used for Qwen3-Coder/Qwen3.5/Qwen3.6 and is compatible with
Qwen variants that use the same XML tool-call format.
"""
tool_call_begin_prefix = "<tool_call>\n<function="
tool_call_begin_suffix = ">\n"
tool_call_end = "\n</function>\n</tool_call>"
tool_call_trigger = "<tool_call>\n<function="
think_tag_end = "</think>"
think_suffix = "\n\n"
think_exclude_tokens = ["<think>", "</think>"]
if tool_choice == "auto":
tags = []
for tool in tools:
function = tool.function
parameters = _get_function_parameters(function)
tags.append(
TagFormat(
begin=f"{tool_call_begin_prefix}{function.name}{tool_call_begin_suffix}",
content=JSONSchemaFormat(json_schema=parameters, style="qwen_xml"),
end=tool_call_end,
)
)
if tags:
suffix_tag = TriggeredTagsFormat(
triggers=[tool_call_trigger],
tags=tags,
excludes=think_exclude_tokens,
)
else:
suffix_tag = AnyTextFormat(excludes=think_exclude_tokens)
elif tool_choice == "forced":
if not tools:
raise ValueError("Forced tool choice must resolve to exactly one tool.")
function = tools[0].function
suffix_tag = TagFormat(
begin=f"{tool_call_begin_prefix}{function.name}{tool_call_begin_suffix}",
content=JSONSchemaFormat(
json_schema=_get_function_parameters(function),
style="qwen_xml",
),
end=tool_call_end,
)
elif tool_choice == "required":
tags = []
for tool in tools:
function = tool.function
parameters = _get_function_parameters(function)
tags.append(
TagFormat(
begin=f"{tool_call_begin_prefix}{function.name}{tool_call_begin_suffix}",
content=JSONSchemaFormat(json_schema=parameters, style="qwen_xml"),
end=tool_call_end,
)
)
assert len(tags) > 0
suffix_tag = TagsWithSeparatorFormat(
tags=tags,
separator="",
at_least_one=True,
)
if not reasoning:
result = StructuralTag(format=suffix_tag)
else:
prefix_tag = SequenceFormat(
elements=[
TagFormat(begin="", content=AnyTextFormat(), end=think_tag_end),
ConstStringFormat(value=think_suffix),
]
)
result = StructuralTag(format=SequenceFormat(elements=[prefix_tag, suffix_tag]))
return result
+4 -2
View File
@@ -101,6 +101,7 @@ _CONFIG_REGISTRY: dict[str, type[PretrainedConfig]] = LazyConfigDict(
fireredlid="FireRedLIDConfig",
funaudiochat="FunAudioChatConfig",
granite4_vision="Granite4VisionConfig",
hyperclovax_vlm="HCXVisionConfig",
hunyuan_vl="HunYuanVLConfig",
hy_v3="HYV3Config",
isaac="IsaacConfig",
@@ -217,8 +218,9 @@ class HFConfigParser(ConfigParserBase):
)
else:
if model_type in _CONFIG_REGISTRY:
# Register the config class to AutoConfig to ensure it's used in future
# calls to `from_pretrained`
# Register the config class to AutoConfig to ensure it's used
# in future calls to `from_pretrained` (e.g. from
# AutoTokenizer or AutoProcessor).
config_class = _CONFIG_REGISTRY[model_type]
config_class.model_type = model_type
AutoConfig.register(model_type, config_class, exist_ok=True)
@@ -37,6 +37,7 @@ _CLASS_TO_MODULE: dict[str, str] = {
"HunYuanVLConfig": "vllm.transformers_utils.configs.hunyuan_vl",
"HunYuanVLTextConfig": "vllm.transformers_utils.configs.hunyuan_vl",
"HunYuanVLVisionConfig": "vllm.transformers_utils.configs.hunyuan_vl",
"HCXVisionConfig": "vllm.transformers_utils.configs.hyperclovax",
"HYV3Config": "vllm.transformers_utils.configs.hy_v3",
"HyperCLOVAXConfig": "vllm.transformers_utils.configs.hyperclovax",
"IsaacConfig": "vllm.transformers_utils.configs.isaac",
@@ -104,6 +105,7 @@ __all__ = [
"HunYuanVLConfig",
"HunYuanVLTextConfig",
"HunYuanVLVisionConfig",
"HCXVisionConfig",
"HYV3Config",
"HyperCLOVAXConfig",
"IsaacConfig",
@@ -17,6 +17,7 @@
# limitations under the License.
"""HyperCLOVA X model configuration."""
from transformers import AutoConfig
from transformers.configuration_utils import PretrainedConfig
@@ -275,3 +276,74 @@ class HyperCLOVAXConfig(PretrainedConfig):
auto_map=auto_map,
**kwargs,
)
class HCXVisionConfig(PretrainedConfig):
"""Vendored HyperCLOVAX Vision config with transformers v5 fix.
The original remote code config does not handle empty initialization
(text_config=None), which breaks transformers v5's @strict validation.
TODO: Remove this class once HyperCLOVAX is upstreamed to transformers.
Tracking PR: https://github.com/huggingface/transformers/pull/44956
"""
model_type = "hyperclovax_vlm"
keys_to_ignore_at_inference = ["past_key_values"]
text_config_attribute_map = {
"n_embd": "hidden_size",
"n_positions": "max_position_embeddings",
"n_head": "num_attention_heads",
"n_layer": "num_hidden_layers",
}
def __init__(
self,
text_config=None,
vision_config=None,
use_nth_layer=-2,
img_start_id=100009,
decoder_max_length=4096,
anyres=False,
unpad=False,
max_num_grids=-1,
num_queries_vis_abstractor=-1,
ignore_index=-100,
proj_pos_emb=True,
proj_prenorm=False,
use_1x1_grid=False,
**kwargs,
):
for key, val in self.text_config_attribute_map.items():
if text_config is not None and key in text_config:
text_config[val] = text_config.pop(key)
self.text_config = None
if text_config is not None:
_text_config = AutoConfig.for_model(text_config["model_type"])
self.text_config = _text_config.from_dict(text_config)
self.hidden_size = self.text_config.hidden_size
self.vision_config = None
if vision_config is not None:
_vision_config = AutoConfig.for_model(vision_config["model_type"])
self.vision_config = _vision_config.from_dict(vision_config)
self.use_nth_layer = use_nth_layer
self.decoder_max_length = decoder_max_length
self.anyres = anyres
self.unpad = unpad
self.max_num_grids = max_num_grids
self.num_queries_vis_abstractor = num_queries_vis_abstractor
self.img_start_id = img_start_id
self.ignore_index = ignore_index
self.proj_pos_emb = proj_pos_emb
self.proj_prenorm = proj_prenorm
self.use_1x1_grid = use_1x1_grid
super().__init__(**kwargs)
def get_text_config(self, decoder=False):
if self.text_config is not None:
return self.text_config
return self
+1 -1
View File
@@ -125,7 +125,7 @@ def get_allowed_cpu_list() -> list[LogicalCPUInfo]:
if platform.system() == "Darwin":
return cpu_list
global_allowed_cpu_id_list = os.sched_getaffinity(0)
global_allowed_cpu_id_list = os.sched_getaffinity(0) # type: ignore[attr-defined]
logical_cpu_list = [x for x in cpu_list if x.id in global_allowed_cpu_id_list]
return logical_cpu_list
+26
View File
@@ -110,6 +110,32 @@ def is_strictly_contiguous(t: torch.Tensor) -> bool:
return True
def canonicalize_singleton_dim_strides(t: torch.Tensor) -> torch.Tensor:
"""Fix degenerate strides on size=1 dimensions for CUDA TMA compatibility.
PyTorch allows any stride on a size=1 dim (is_contiguous() is always True
there), so a size=1 dim may have stride=1 (2 bytes for bf16) instead of
the canonical product(shape[i+1:]). CUDA TMA on H100+ requires all
non-outermost strides to be 16-byte aligned; stride=1 triggers
cudaErrorIllegalInstruction. Zero-copy: patches stride metadata only via
as_strided; returns t unchanged if all size=1 strides are already canonical.
"""
if 1 not in t.shape:
return t
strides = list(t.stride())
shape = t.shape
prev_stride = 1
changed = False
for i in range(len(shape) - 1, -1, -1):
if shape[i] == 1 and strides[i] != prev_stride:
strides[i] = prev_stride
changed = True
prev_stride = strides[i] * shape[i]
if not changed:
return t
return t.as_strided(t.shape, strides)
@contextlib.contextmanager
def set_default_torch_dtype(dtype: torch.dtype):
"""Sets the default torch dtype to the given dtype."""
+23 -1
View File
@@ -11,7 +11,10 @@ import torch
from vllm.model_executor.layers.attention import Attention
from vllm.platforms import current_platform
from vllm.utils.torch_utils import is_quantized_kv_cache
from vllm.utils.torch_utils import (
canonicalize_singleton_dim_strides,
is_quantized_kv_cache,
)
from vllm.v1.attention.backend import (
AttentionBackend,
AttentionImpl,
@@ -747,6 +750,23 @@ class FlashAttentionImpl(AttentionImpl):
# For decoder and cross-attention, use KV cache as before
key_cache, value_cache = kv_cache.unbind(0)
# Fix degenerate strides on size-1 dims (e.g. num_kv_heads=1 with TP).
# FA3/4 on H100+ uses TMA, which requires ≥16-byte stride alignment.
# See vllm.utils.torch_utils.canonicalize_singleton_dim_strides.
fixed_k = canonicalize_singleton_dim_strides(key_cache)
fixed_v = canonicalize_singleton_dim_strides(value_cache)
if fixed_k is not key_cache or fixed_v is not value_cache:
logger.debug(
"Canonicalized degenerate KV cache strides (FlashAttention): "
"shape=%s, key strides before=%s after=%s, "
"value strides before=%s after=%s",
key_cache.shape,
key_cache.stride(),
fixed_k.stride(),
value_cache.stride(),
fixed_v.stride(),
)
key_cache, value_cache = fixed_k, fixed_v
if is_quantized_kv_cache(self.kv_cache_dtype):
# queries are quantized in the attention layer
@@ -861,6 +881,8 @@ class FlashAttentionImpl(AttentionImpl):
# we use direct Q, K, V tensors without caching
return
# Scatter write into the KV cache using slot_mapping indices.
# No TMA kernel is invoked here, so stride canonicalization is not needed.
key_cache, value_cache = kv_cache.unbind(0)
# Reshape the input keys and values and store them in the cache.
@@ -4,7 +4,11 @@
import torch
from vllm.utils.torch_utils import is_quantized_kv_cache
from vllm.logger import init_logger
from vllm.utils.torch_utils import (
canonicalize_singleton_dim_strides,
is_quantized_kv_cache,
)
from vllm.v1.attention.backend import AttentionType
from vllm.v1.attention.backends.fa_utils import (
get_flash_attn_version,
@@ -25,6 +29,8 @@ from .flash_attn import (
cascade_attention,
)
logger = init_logger(__name__)
class FlashAttentionDiffKVBackend(FlashAttentionBackend):
# Default to 128 for this backend
@@ -204,6 +210,23 @@ class FlashAttentionDiffKVImpl(FlashAttentionImpl):
# Different head_size for K and V
key_cache = kv_cache[..., : self.head_size]
value_cache = kv_cache[..., self.head_size :]
# Fix degenerate strides on size-1 dims (e.g. num_kv_heads=1 with TP).
# FA3/4 on H100+ uses TMA, which requires ≥16-byte stride alignment.
# See vllm.utils.torch_utils.canonicalize_singleton_dim_strides.
fixed_k = canonicalize_singleton_dim_strides(key_cache)
fixed_v = canonicalize_singleton_dim_strides(value_cache)
if fixed_k is not key_cache or fixed_v is not value_cache:
logger.debug(
"Canonicalized degenerate KV cache strides (FlashAttentionDiffKV): "
"shape=%s, key strides before=%s after=%s, "
"value strides before=%s after=%s",
key_cache.shape,
key_cache.stride(),
fixed_k.stride(),
value_cache.stride(),
fixed_v.stride(),
)
key_cache, value_cache = fixed_k, fixed_v
if is_quantized_kv_cache(self.kv_cache_dtype):
# queries are quantized in the attention layer
+31 -19
View File
@@ -43,6 +43,7 @@ from vllm.utils.flashinfer import (
from vllm.utils.math_utils import cdiv
from vllm.utils.platform_utils import is_pin_memory_available
from vllm.utils.torch_utils import (
canonicalize_singleton_dim_strides,
is_quantized_kv_cache,
is_strictly_contiguous,
nvfp4_kv_cache_full_dim,
@@ -1479,6 +1480,21 @@ class FlashInferImpl(AttentionImpl):
stride_order = FlashInferBackend.get_kv_cache_stride_order()
kv_cache_permute = kv_cache.permute(*stride_order) # HND and contiguous
# Fix degenerate strides on any size-1 dimension (e.g. num_kv_heads=1
# with TP=8). PyTorch permits non-canonical strides on size-1 dims;
# CUDA TMA requires ≥16-byte alignment on all non-outermost strides.
# canonicalize_singleton_dim_strides patches metadata via as_strided —
# zero-copy. See vllm.utils.torch_utils.
fixed = canonicalize_singleton_dim_strides(kv_cache_permute)
if fixed is not kv_cache_permute:
logger.debug(
"Canonicalized degenerate KV cache strides (FlashInfer): "
"shape=%s, strides before=%s, strides after=%s",
kv_cache_permute.shape,
kv_cache_permute.stride(),
fixed.stride(),
)
kv_cache_permute = fixed
# For NVFP4, the kv_cache last dim is full_dim (data + scale packed).
# Split into correctly-strided data and scale views.
@@ -1568,10 +1584,11 @@ class FlashInferImpl(AttentionImpl):
else:
assert isinstance(attn_metadata.prefill, TRTLLMPrefill)
# prefill_query may be non-contiguous or have degenerate strides
# First ensure memory contiguity, then fix degenerate strides
# with reshape. contiguous() alone doesn't fix degenerate
# strides when a dimension has size 1.
prefill_query = prefill_query.contiguous().reshape(prefill_query.shape)
# on size=1 dims. contiguous() ensures memory layout; then
# canonicalize_singleton_dim_strides fixes any remaining
# degenerate strides on size=1 dims for TMA alignment.
prefill_query = prefill_query.contiguous()
prefill_query = canonicalize_singleton_dim_strides(prefill_query)
workspace_buffer = _get_trtllm_gen_workspace_buffer()
block_tables_prefill = attn_metadata.prefill.block_tables
seq_lens_prefill = attn_metadata.prefill.seq_lens
@@ -1621,11 +1638,9 @@ class FlashInferImpl(AttentionImpl):
# with fp8 kv cache, we can construct a mock block
# and mock kv cache with BF16 KV involved in the prefill
#
# The inner (block_size, head_size) dims must be
# contiguous; outer dims may have non-canonical strides
# (e.g. cross-layer unified allocation).
# Degenerate strides on outer dims break TMA descriptors
# (see flashinfer-ai/flashinfer#2232).
kv_cache_permute = canonicalize_singleton_dim_strides(
kv_cache_permute
)
kv_strides = kv_cache_permute.stride()
assert (
kv_strides[-1] == 1
@@ -1732,12 +1747,13 @@ class FlashInferImpl(AttentionImpl):
if needs_fp8_out:
output[:num_decode_tokens].copy_(out_decode.to(output.dtype))
else:
# decode_query may be non-contiguous or have degenerate strides
assert isinstance(attn_metadata.decode, TRTLLMDecode)
# First ensure memory contiguity, then fix degenerate strides
# with reshape. contiguous() alone doesn't fix degenerate
# strides when a dimension has size 1.
decode_query = decode_query.contiguous().reshape(decode_query.shape)
# decode_query may be non-contiguous or have degenerate strides
# on size=1 dims. contiguous() ensures memory layout; then
# canonicalize_singleton_dim_strides fixes any remaining
# degenerate strides on size=1 dims for TMA alignment.
decode_query = decode_query.contiguous()
decode_query = canonicalize_singleton_dim_strides(decode_query)
workspace_buffer = _get_trtllm_gen_workspace_buffer()
block_tables_decode = attn_metadata.decode.block_tables
seq_lens_decode = attn_metadata.decode.seq_lens
@@ -1748,11 +1764,7 @@ class FlashInferImpl(AttentionImpl):
assert is_strictly_contiguous(workspace_buffer)
assert is_strictly_contiguous(block_tables_decode)
assert is_strictly_contiguous(seq_lens_decode)
# kv_cache outer dims may be non-contiguous (e.g.
# cross-layer unified allocation), but inner dims
# (block_size, head_size) must be contiguous and
# strides must be canonical to avoid TMA descriptor
# failures (see flashinfer-ai/flashinfer#2232).
kv_cache_permute = canonicalize_singleton_dim_strides(kv_cache_permute)
kv_strides = kv_cache_permute.stride()
assert (
kv_strides[-1] == 1 and kv_strides[-2] == kv_cache_permute.shape[-1]
+5
View File
@@ -403,6 +403,11 @@ class CoreEngineActorManager:
range(dp_size), local_dp_ranks, placement_groups
):
dp_vllm_config = copy.deepcopy(vllm_config)
if dp_size > 1:
# Append the DP rank to instance_id so that per-engine
# identifiers (e.g. Ray actor names in RayExecutorV2) are
# unique across DP replicas.
dp_vllm_config.instance_id = f"{dp_vllm_config.instance_id}_dp{index}"
dp_vllm_config.parallel_config.placement_group = pg
local_client = index < local_engine_count
+12 -8
View File
@@ -16,6 +16,7 @@ pin_memory = is_pin_memory_available()
class PoolingCursor:
first_token_indices_gpu: torch.Tensor
last_token_indices_gpu: torch.Tensor
first_token_indices_cpu: torch.Tensor
prompt_lens_cpu: torch.Tensor
seq_lens_cpu: torch.Tensor
num_scheduled_tokens_cpu: torch.Tensor
@@ -24,6 +25,7 @@ class PoolingCursor:
return PoolingCursor(
first_token_indices_gpu=self.first_token_indices_gpu[indices],
last_token_indices_gpu=self.last_token_indices_gpu[indices],
first_token_indices_cpu=self.first_token_indices_cpu[indices],
prompt_lens_cpu=self.prompt_lens_cpu[indices],
seq_lens_cpu=self.seq_lens_cpu[indices],
num_scheduled_tokens_cpu=self.num_scheduled_tokens_cpu[indices],
@@ -117,12 +119,13 @@ class PoolingMetadata:
assert len(prompt_lens) == n_seq
num_scheduled_tokens_cpu = torch.from_numpy(num_scheduled_tokens_np)
cumsum_cpu = torch.zeros(
n_seq + 1, dtype=torch.int64, pin_memory=pin_memory, device="cpu"
)
torch.cumsum(num_scheduled_tokens_cpu, dim=0, out=cumsum_cpu[1:])
if query_start_loc_gpu is None:
cumsum = torch.zeros(
n_seq + 1, dtype=torch.int64, pin_memory=pin_memory, device="cpu"
)
torch.cumsum(num_scheduled_tokens_cpu, dim=0, out=cumsum[1:])
cumsum = cumsum.to(device, non_blocking=True)
cumsum_gpu = cumsum_cpu.to(device, non_blocking=True)
else:
if query_start_loc_gpu.shape[0] != n_seq + 1:
raise ValueError(
@@ -135,10 +138,11 @@ class PoolingMetadata:
"query_start_loc_gpu must be on the same device as the "
f"hidden states: {query_start_loc_gpu.device} != {device}."
)
cumsum = query_start_loc_gpu
cumsum_gpu = query_start_loc_gpu
self.pooling_cursor = PoolingCursor(
first_token_indices_gpu=cumsum[:n_seq],
last_token_indices_gpu=cumsum[1:] - 1,
first_token_indices_gpu=cumsum_gpu[:n_seq],
last_token_indices_gpu=cumsum_gpu[1:] - 1,
first_token_indices_cpu=cumsum_cpu[:n_seq],
prompt_lens_cpu=prompt_lens,
seq_lens_cpu=seq_lens_cpu,
num_scheduled_tokens_cpu=num_scheduled_tokens_cpu,
+1 -3
View File
@@ -55,10 +55,8 @@ class PromptLogprobsWorker:
num_prompt_logprobs = self.num_prompt_logprobs[idx_mapping_np]
prompt_lens = prompt_lens[idx_mapping_np]
# NOTE(woosuk): -1 because the last prompt token's hidden state is not
# needed for prompt logprobs.
computed_prefill = num_computed_prefill_tokens[idx_mapping_np]
includes_prompt = computed_prefill < prompt_lens - 1
includes_prompt = computed_prefill < prompt_lens
# NOTE(woosuk): If the request was resumed after preemption, its prompt
# logprobs must have been computed before preemption. Skip.
resumed_after_prompt = prompt_lens < prefill_lens[idx_mapping_np]
+2 -4
View File
@@ -49,6 +49,8 @@ class CachedRequestState:
lora_request: LoRARequest | None = None
prompt_embeds: torch.Tensor | None = None
# To accumulate prompt logprobs tensor chunks across prefill steps.
in_progress_prompt_logprobs_cpu: LogprobsTensors | None = None
# Per-position mask for mixed-mode inputs (e.g chat completion with
# prompt_embeds content parts). See `Request.prompt_is_token_ids`.
@@ -255,9 +257,6 @@ class InputBatch:
# More efficient than num_logprobs=-1 when only a few tokens are needed
self.logprob_token_ids: dict[str, list[int]] = {}
# To accumulate prompt logprobs tensor chunks across prefill steps.
self.in_progress_prompt_logprobs_cpu: dict[str, LogprobsTensors] = {}
# Internal representation of per-step batch state changes, used for
# reordering persistent batch and generating logitsprocs batch state
# updates. Should reset each step.
@@ -552,7 +551,6 @@ class InputBatch:
self.generators.pop(req_index, None)
self.num_logprobs.pop(req_id, None)
self.logprob_token_ids.pop(req_id, None)
self.in_progress_prompt_logprobs_cpu.pop(req_id, None)
if self.prev_req_id_to_index is not None:
self.prev_req_id_to_index.pop(req_id, None)
+4 -5
View File
@@ -5094,7 +5094,6 @@ class GPUModelRunner(
if not num_prompt_logprobs_dict:
return {}
in_progress_dict = self.input_batch.in_progress_prompt_logprobs_cpu
prompt_logprobs_dict: dict[str, LogprobsTensors | None] = {}
# Since prompt logprobs are a rare feature, prioritize simple,
@@ -5118,14 +5117,14 @@ class GPUModelRunner(
)
# Set up target LogprobsTensors object.
logprobs_tensors = in_progress_dict.get(req_id)
if not logprobs_tensors:
logprobs_tensors = request.in_progress_prompt_logprobs_cpu
if logprobs_tensors is None:
# Create empty logprobs CPU tensors for the entire prompt.
# If chunked, we'll copy in slice by slice.
logprobs_tensors = LogprobsTensors.empty_cpu(
num_prompt_tokens - 1, num_prompt_logprobs + 1
)
in_progress_dict[req_id] = logprobs_tensors
request.in_progress_prompt_logprobs_cpu = logprobs_tensors
# Determine number of logits to retrieve.
start_idx = request.num_computed_tokens
@@ -5182,7 +5181,7 @@ class GPUModelRunner(
# num_prompt_logprobs_dict.
for req_id in completed_prefill_reqs:
del num_prompt_logprobs_dict[req_id]
del in_progress_dict[req_id]
self.requests[req_id].in_progress_prompt_logprobs_cpu = None
# Must synchronize the non-blocking GPU->CPU transfers.
if prompt_logprobs_dict: