Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b870c8edb4 | ||
|
|
be5983b874 | ||
|
|
9c07342fdc | ||
|
|
844df54269 | ||
|
|
422dd02598 | ||
|
|
8c780943b4 | ||
|
|
e724b0ea8d | ||
|
|
712ad0286c | ||
|
|
321fa2d6d1 | ||
|
|
3e1ad4435f | ||
|
|
8decbfa02c | ||
|
|
62ba7516e8 | ||
|
|
6f53753fc9 | ||
|
|
6ec9bbec38 | ||
|
|
01d4d1ad37 | ||
|
|
c103c02a1a | ||
|
|
67058ca326 | ||
|
|
894a02500b | ||
|
|
66dfee7121 | ||
|
|
db9a84e0cd | ||
|
|
cb03fee32b | ||
|
|
c51df43005 | ||
|
|
54dc64d5d3 |
@@ -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"
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 = '<|DSML|parameter name="'
|
||||
PARAM_END = "</|DSML|parameter>"
|
||||
|
||||
|
||||
@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
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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`) |
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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:]
|
||||
|
||||
@@ -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 = "<|DSML|tool_calls>"
|
||||
tool_call_end_token: str = "</|DSML|tool_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 = '<|DSML|invoke name="'
|
||||
invoke_begin_suffix = '">\n'
|
||||
invoke_end = "</|DSML|invoke>\n"
|
||||
tool_calls_prefix = "\n\n"
|
||||
function_calls_begin = "<|DSML|tool_calls>\n"
|
||||
function_calls_end = "</|DSML|tool_calls>"
|
||||
function_calls_trigger = "<|DSML|tool_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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user