Compare commits

...
Author SHA1 Message Date
yewentao256 7d98b72bd3 fix ci pre-commit
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-07-23 18:15:27 +00:00
75ccdf3145 [Core] Update PyTorch to 2.13.0, torchvision to 0.28.0, triton to 3.7.1 (#48155)
Signed-off-by: Andrey Talman <atalman@users.noreply.github.com>
Co-authored-by: Andrey Talman <atalman@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 11:09:36 -07:00
Andrey TalmanandGitHub c6fe94b4d5 [CI] Bump PyTorch Compilation Unit Tests timeout to 150 min (#49606) 2026-07-23 11:08:45 -07:00
46f01a50ac [CI][Bugfix] Fix test isolation in block_int8/ptpc_fp8 MoE kernel tests (#49609)
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:26:02 -05:00
Andreas KaratzasandGitHub f00efc5265 [CI] Isolate cudagraph tests in child processes (#49510)
Signed-off-by: Andreas Karatzas <Andreas.Karatzas@amd.com>
2026-07-23 17:16:19 +00:00
Wentao YeandGitHub b0cb1da1bd [DSv4 Perf] Skip topk and router when not needed, 3.4% E2E TTFT improvement for Decode case (#49486)
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-07-23 13:08:08 -04:00
Andreas KaratzasandGitHub 0e36e3bbd1 [CI] Use explicit devices in quantization tests (#49512)
Signed-off-by: Andreas Karatzas <Andreas.Karatzas@amd.com>
2026-07-23 10:54:26 -06:00
494845e79f Revert "[MRV2] Always build attn metadata at capture time" (#49364) (#49451)
Co-authored-by: vllm-agent CI bot <ci-bot@vllm-agent.local>
2026-07-23 09:51:33 -07:00
yue.yuandGitHub 0416dab275 [Bugfix][Structured Output][Spec Decode] Advance grammar across reasoning boundary (#44993)
Signed-off-by: Allen.Yu <yuyue0225sc@163.com>
2026-07-23 09:14:15 -07:00
c8db00b16c Fix GPTQ quantized Qwen3.5 MTP weight loading with spec decode (#48816)
Signed-off-by: vllmellm <vllm.ellm@embeddedllm.com>
Co-authored-by: TJian <tunjian.tan@embeddedllm.com>
Co-authored-by: noobHappylife <64898326+noobHappylife@users.noreply.github.com>
2026-07-23 06:59:48 -07:00
Guan-Ming ChiuandGitHub 80c7683923 [Perf] Defer MM embeds loading off the event loop (#49477)
Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com>
2026-07-23 13:58:50 +00:00
638d6e9757 [Bugfix][CI/Build] Fix Plamo2 HF runner crash on transformers v5 (_tied_weights_keys list→dict) (#44239)
Signed-off-by: Nikhil Kulkarni <nikhilkulkarni1755@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
2026-07-23 12:25:14 +00:00
Junpu YuandGitHub 1ad84fea86 [Bugfix][Spec Decode] Select earliest-completing stop string in check_stop_strings (#49391)
Signed-off-by: Junpu Yu <davidyu@nvidia.com>
2026-07-23 18:14:06 +08:00
12213c6795 [Bugfix] handle grammar compilation failures to avoid engine crash (#47312)
Signed-off-by: zhuhaoran <zhuhaoran.zhr@alibaba-inc.com>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
2026-07-23 18:13:49 +08:00
10c75477b0 [Bugfix][Core] shm_broadcast: bound idle reader waits and release read slots (#45224)
Signed-off-by: Chaemin Lim <chaemin.lim@mangoboost.io>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Edwin Lim <edwin.lim@mangoboost.io>
Co-authored-by: Jaeyoun Kim <jaeyoun.kim@mangoboost.io>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
2026-07-23 18:13:29 +08:00
ac36a7a1e7 [MRV2][Spec Decode] Avoid rejection sampler OOM by chunking (#48630)
Signed-off-by: mgoin <mgoin64@gmail.com>
Signed-off-by: Michael Goin <mgoin64@gmail.com>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
2026-07-23 18:13:13 +08:00
Nick HillGitHubmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
521aa80f71 [Core] Simplify KVBlockZeroer index tensor handling (#48399)
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-07-23 18:12:46 +08:00
a76df87db8 [MooncakeStore] Re-derive full external hits on stored boundaries (#49481)
Signed-off-by: Dao Le <Dao007forever@gmail.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 18:12:28 +08:00
a4904ba903 [Perf][KVConnector][Mooncake] Vectorize prepare_value on the KV load path (#48531)
Signed-off-by: girasoley <girasoley@inferact.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: girasoley <girasoley@inferact.ai>
Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-23 18:12:00 +08:00
66 changed files with 1532 additions and 472 deletions
@@ -1,10 +1,11 @@
#!/bin/bash
set -euox pipefail
export VLLM_CPU_KVCACHE_SPACE=1
export VLLM_CPU_KVCACHE_SPACE=1
export VLLM_CPU_CI_ENV=1
# Reduce sub-processes for acceleration
export TORCH_COMPILE_DISABLE=1
# Skip torch.compile via vLLM's --enforce-eager flag (passed below) instead of
# TORCH_COMPILE_DISABLE=1, which torch 2.12 no longer treats as a silent no-op
# when callers specify fullgraph=True.
export VLLM_ENABLE_V1_MULTIPROCESSING=0
SDE_ARCHIVE="sde-external-10.7.0-2026-02-18-lin.tar.xz"
@@ -49,15 +50,15 @@ wait_for_pid_and_check_log() {
}
# Test Sky Lake (AVX512F)
./sde/sde64 -skl -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 > test_0.log 2>&1 &
./sde/sde64 -skl -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 --enforce-eager > test_0.log 2>&1 &
PID_TEST_0=$!
# Test Cascade Lake (AVX512F + VNNI)
./sde/sde64 -clx -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 > test_1.log 2>&1 &
./sde/sde64 -clx -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 --enforce-eager > test_1.log 2>&1 &
PID_TEST_1=$!
# Test Cooper Lake (AVX512F + VNNI + BF16)
./sde/sde64 -cpx -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 > test_2.log 2>&1 &
./sde/sde64 -cpx -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 --enforce-eager > test_2.log 2>&1 &
PID_TEST_2=$!
wait_for_pid_and_check_log $PID_TEST_0 test_0.log
+1 -1
View File
@@ -5,7 +5,7 @@ steps:
- label: PyTorch Compilation Unit Tests
device: h200_35gb
key: pytorch-compilation-unit-tests
timeout_in_minutes: 110
timeout_in_minutes: 150
source_file_dependencies:
- vllm/__init__.py
- vllm/_aiter_ops.py
+2 -2
View File
@@ -68,8 +68,8 @@ endif()
# requirements.txt files and should be kept consistent. The ROCm torch
# versions are derived from docker/Dockerfile.rocm
#
set(TORCH_SUPPORTED_VERSION_CUDA "2.11.0")
set(TORCH_SUPPORTED_VERSION_ROCM "2.11.0")
set(TORCH_SUPPORTED_VERSION_CUDA "2.13.0")
set(TORCH_SUPPORTED_VERSION_ROCM "2.13.0")
# TORCH_NIGHTLY=1 builds run against unpinned nightly wheels, so the supported-
# version check would always warn. Only treat it as a nightly build when the
# value is exactly "1" (the bootstrap exports TORCH_NIGHTLY=0 by default, which
+1 -1
View File
@@ -22,7 +22,7 @@
# docker buildx bake -f docker/docker-bake.hcl -f docker/versions.json
# =============================================================================
ARG CUDA_VERSION=13.0.2
ARG CUDA_VERSION=13.0.3
ARG PYTHON_VERSION=3.12
ARG UBUNTU_VERSION=22.04
+3 -3
View File
@@ -2,7 +2,7 @@
"_comment": "Auto-generated from Dockerfile ARGs. Do not edit manually. Run: python tools/generate_versions_json.py",
"variable": {
"CUDA_VERSION": {
"default": "13.0.2"
"default": "13.0.3"
},
"PYTHON_VERSION": {
"default": "3.12"
@@ -11,10 +11,10 @@
"default": "22.04"
},
"BUILD_BASE_IMAGE": {
"default": "nvidia/cuda:13.0.2-devel-ubuntu22.04"
"default": "nvidia/cuda:13.0.3-devel-ubuntu22.04"
},
"FINAL_BASE_IMAGE": {
"default": "nvidia/cuda:13.0.2-base-ubuntu22.04"
"default": "nvidia/cuda:13.0.3-base-ubuntu22.04"
},
"BUILD_OS": {
"default": "ubuntu"
+1 -1
View File
@@ -7,7 +7,7 @@ requires = [
"setuptools>=77.0.3,<81.0.0",
"setuptools-scm>=8.0",
"setuptools-rust>=1.9.0",
"torch == 2.11.0",
"torch == 2.13.0",
"wheel",
"jinja2",
]
+2 -2
View File
@@ -4,8 +4,8 @@ packaging>=24.2
setuptools==77.0.3 # this version can reuse CMake build dir
setuptools-scm>=8
setuptools-rust>=1.9.0
torch==2.11.0+cpu; platform_machine == "x86_64" or platform_machine == "s390x" or platform_machine == "aarch64"
torch==2.11.0; platform_system == "Darwin" or platform_machine == "ppc64le" or platform_machine == "riscv64"
torch==2.13.0+cpu; platform_machine == "x86_64" or platform_machine == "s390x" or platform_machine == "aarch64"
torch==2.13.0; platform_system == "Darwin" or platform_machine == "ppc64le" or platform_machine == "riscv64"
wheel
jinja2>=3.1.6
regex
+1 -1
View File
@@ -5,7 +5,7 @@ packaging>=24.2
setuptools>=77.0.3,<81.0.0
setuptools-scm>=8
setuptools-rust>=1.9.0
torch==2.11.0
torch==2.13.0
wheel
jinja2>=3.1.6
regex
+2 -2
View File
@@ -6,8 +6,8 @@ setuptools==77.0.3 # this version can reuse CMake build dir
numba == 0.65.0; platform_machine != "s390x" # Required for N-gram speculative decoding
# Dependencies for CPUs
torch==2.11.0+cpu; platform_machine == "x86_64" or platform_machine == "s390x" or platform_machine == "aarch64"
torch==2.11.0; platform_system == "Darwin" or platform_machine == "ppc64le" or platform_machine == "riscv64"
torch==2.13.0+cpu; platform_machine == "x86_64" or platform_machine == "s390x" or platform_machine == "aarch64"
torch==2.13.0; platform_system == "Darwin" or platform_machine == "ppc64le" or platform_machine == "riscv64"
# required for the image processor of minicpm-o-2_6, this must be updated alongside torch
torchaudio; platform_machine != "s390x" and platform_machine != "riscv64"
+2 -2
View File
@@ -4,10 +4,10 @@
numba == 0.65.0 # Required for N-gram speculative decoding
# Dependencies for NVIDIA GPUs
torch==2.11.0
torch==2.13.0
torchaudio==2.11.0
# These must be updated alongside torch
torchvision==0.26.0 # Required for phi3v processor. See https://github.com/pytorch/vision?tab=readme-ov-file#installation for corresponding version
torchvision==0.28.0 # Required for phi3v processor. See https://github.com/pytorch/vision?tab=readme-ov-file#installation for corresponding version
torchcodec >= 0.14
PyNvVideoCodec==2.0.4
# FlashInfer should be updated together with the Dockerfile
+2 -2
View File
@@ -1107,7 +1107,7 @@ tokenizers==0.22.2
# -r requirements/test/../common.txt
# -r requirements/test/cuda.in
# transformers
torch==2.11.0+cpu
torch==2.13.0+cpu
# via
# -r requirements/test/cuda.in
# accelerate
@@ -1134,7 +1134,7 @@ torchaudio==2.11.0+cpu
# vocos
torchcodec==0.14.0+cpu
# via -r requirements/test/cuda.in
torchvision==0.26.0+cpu
torchvision==0.28.0+cpu
# via
# -r requirements/test/cuda.in
# open-clip-torch
+2 -2
View File
@@ -28,9 +28,9 @@ soundfile # required for audio tests
jiwer # required for audio tests
tblib # for pickling test exceptions
timm >=1.0.17 # required for internvl and gemma3n-mm test
torch==2.11.0
torch==2.13.0
torchaudio==2.11.0
torchvision==0.26.0
torchvision==0.28.0
transformers_stream_generator # required for qwen-vl test
matplotlib # required for qwen-vl test
mistral_common[image,audio] >= 1.11.5 # required for voxtral test
+11 -9
View File
@@ -159,7 +159,7 @@ cuda-bindings==13.0.3
# via torch
cuda-pathfinder==1.3.3
# via cuda-bindings
cuda-toolkit==13.0.2
cuda-toolkit==13.0.3.0
# via torch
cupy-cuda12x==13.6.0
# via ray
@@ -599,7 +599,7 @@ numpy==2.2.6
# tritonclient
# vocos
# xgrammar
nvidia-cublas==13.1.0.3
nvidia-cublas==13.1.1.3
# via
# cuda-toolkit
# nvidia-cudnn-cu13
@@ -607,10 +607,12 @@ nvidia-cublas==13.1.0.3
nvidia-cuda-cupti==13.0.85
# via cuda-toolkit
nvidia-cuda-nvrtc==13.0.88
# via cuda-toolkit
# via
# cuda-toolkit
# nvidia-cublas
nvidia-cuda-runtime==13.0.96
# via cuda-toolkit
nvidia-cudnn-cu13==9.19.0.56
nvidia-cudnn-cu13==9.20.0.48
# via torch
nvidia-cufft==12.0.0.61
# via cuda-toolkit
@@ -624,9 +626,9 @@ nvidia-cusparse==12.6.3.3
# via
# cuda-toolkit
# nvidia-cusolver
nvidia-cusparselt-cu13==0.8.0
nvidia-cusparselt-cu13==0.8.1
# via torch
nvidia-nccl-cu13==2.28.9
nvidia-nccl-cu13==2.29.7
# via torch
nvidia-nvjitlink==13.0.88
# via
@@ -1202,7 +1204,7 @@ tokenizers==0.22.2
# -r requirements/test/../common.txt
# -r requirements/test/cuda.in
# transformers
torch==2.11.0+cu130
torch==2.13.0+cu130
# via
# -c requirements/cuda.txt
# -r requirements/test/cuda.in
@@ -1233,7 +1235,7 @@ torchcodec==0.14.0+cu130
# via
# -c requirements/cuda.txt
# -r requirements/test/cuda.in
torchvision==0.26.0+cu130
torchvision==0.28.0+cu130
# via
# -c requirements/cuda.txt
# -r requirements/test/cuda.in
@@ -1270,7 +1272,7 @@ transformers==5.13.1
# xgrammar
transformers-stream-generator==0.0.5
# via -r requirements/test/cuda.in
triton==3.6.0
triton==3.7.1
# via
# torch
# xgrammar
+30
View File
@@ -349,6 +349,20 @@ _T = TypeVar("_T", nn.Module, torch.Tensor, BatchEncoding, BatchFeature, dict)
_R = TypeVar("_R")
def _fix_v4_tied_weights_keys(model_cls: type) -> None:
"""Convert a v4 list-format _tied_weights_keys to the transformers v5 dict form."""
tied = getattr(model_cls, "_tied_weights_keys", None)
if not isinstance(tied, list) or not tied:
return
result = {
k: "model.embed_tokens.weight"
for k in tied
if "lm_head" in k and k.endswith(".weight")
}
if result:
setattr(model_cls, "_tied_weights_keys", result)
class HfRunner:
def get_default_device(self):
from vllm.platforms import current_platform
@@ -474,6 +488,22 @@ class HfRunner:
trust_remote_code=trust_remote_code,
)
else:
if trust_remote_code and hasattr(self.config, "auto_map"):
cls_ref = self.config.auto_map.get(auto_cls.__name__)
if cls_ref is not None:
from vllm.transformers_utils.dynamic_module import (
try_get_class_from_dynamic_module,
)
model_cls = try_get_class_from_dynamic_module(
cls_ref,
model_name,
trust_remote_code=trust_remote_code,
warn_on_fail=False,
)
if model_cls is not None:
_fix_v4_tied_weights_keys(model_cls)
model = cast(
nn.Module,
auto_cls.from_pretrained(
@@ -0,0 +1,76 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for check_stop_strings.
These are pure-function tests (no model / GPU). They pin down which stop
string is selected when several stop strings match within the text that was
appended in a single step -- which happens under speculative decoding, where
multiple tokens (and therefore multiple stop strings) can be appended at once.
"""
import pytest
from vllm.v1.engine.detokenizer import check_stop_strings
@pytest.mark.parametrize("stop", [["a", "is"], ["is", "a"]])
def test_earliest_completing_stop_wins_regardless_of_list_order(stop):
# " The user is a": " is a" (5 chars) was appended in one step. Both "is"
# (index 10) and " a" (index 13) land in the same window. "is" completes
# earlier in the text, so it must win over list order.
text = " The user is a"
new_char_count = len(" is a")
assert check_stop_strings(text, new_char_count, stop, include_in_output=False) == (
"is",
10,
)
@pytest.mark.parametrize("stop", [["a", "is"], ["is", "a"]])
def test_earliest_completing_stop_include_in_output(stop):
text = " The user is a"
new_char_count = len(" is a")
# Truncate to the end of "is" (index 12) -> " The user is".
assert check_stop_strings(text, new_char_count, stop, include_in_output=True) == (
"is",
12,
)
def test_completion_position_not_start_position():
# "b" starts later than "abc" but completes earlier, so it must win.
text = "abc"
assert check_stop_strings(
text, len(text), ["abc", "b"], include_in_output=False
) == ("b", 1)
@pytest.mark.parametrize(
"stop,expected",
[
(["ab", "b"], ("ab", 0)),
(["b", "ab"], ("b", 1)),
],
)
def test_ties_broken_by_list_order(stop, expected):
# "ab" and "b" both complete at index 2; list order decides the winner.
text = "ab"
assert (
check_stop_strings(text, len(text), stop, include_in_output=False) == expected
)
def test_single_stop_in_window_unchanged():
# The common case (one stop in the window) is unaffected by the change.
text = "hello world."
assert check_stop_strings(text, 1, ["."], include_in_output=False) == (".", 11)
# Stop completes at the very end -> no truncation needed (-1).
assert check_stop_strings(text, 1, ["."], include_in_output=True) == (".", -1)
def test_no_match_and_empty_inputs_return_none():
assert check_stop_strings("hello", 5, ["zzz"], include_in_output=False) is None
assert check_stop_strings("hello", 0, ["h"], include_in_output=False) is None
assert check_stop_strings("hello", 5, [], include_in_output=False) is None
+130
View File
@@ -348,6 +348,136 @@ def test_message_queue_busy_to_idle():
distributed_run(worker_fn_test_busy_to_idle, 4)
@pytest.mark.parametrize("should_warn", [False, True])
def test_reader_timeout_caps_indefinite_waits(should_warn):
with (
mock.patch(
"vllm.distributed.device_communicators.shm_broadcast."
"SHM_READER_RECHECK_INTERVAL_MS",
new=7,
),
mock.patch(
"vllm.distributed.device_communicators.shm_broadcast."
"VLLM_RINGBUFFER_WARNING_INTERVAL",
new=60,
),
):
timeout = MessageQueue.ReadTimeoutWithWarnings(
timeout=None, should_warn=should_warn
)
assert timeout.timeout_ms() == 7
def test_reader_rechecks_shm_after_idle_wait_timeout_without_notify():
writer = MessageQueue(
n_reader=1,
n_local_reader=1,
max_chunk_bytes=1024 * 1024,
max_chunks=1,
)
reader = MessageQueue.create_from_handle(writer.export_handle(), rank=0)
payload = 123
poll_started = threading.Event()
allow_timeout = threading.Event()
result = {}
def acquire_read_in_thread():
try:
with reader.acquire_read(indefinite=True) as buf:
result["value"] = buf[0]
except Exception as exc:
result["exc"] = exc
def poll_timeout(*, timeout: int | None = None):
poll_started.set()
assert allow_timeout.wait(timeout=5)
return []
try:
writer.wait_until_ready()
reader.wait_until_ready()
reader._spin_condition.last_read = 0
reader._spin_condition.busy_loop_s = 0
with (
mock.patch(
"vllm.distributed.device_communicators.shm_broadcast."
"SHM_READER_RECHECK_INTERVAL_MS",
new=50,
),
mock.patch(
"vllm.distributed.device_communicators.shm_broadcast."
"VLLM_RINGBUFFER_WARNING_INTERVAL",
new=60,
),
mock.patch.object(
reader._spin_condition.poller,
"poll",
side_effect=poll_timeout,
) as poll,
):
read_thread = threading.Thread(target=acquire_read_in_thread, daemon=True)
read_thread.start()
assert poll_started.wait(timeout=5)
with writer.acquire_write(timeout=0.1) as buf:
buf[0] = payload
allow_timeout.set()
read_thread.join(timeout=5)
assert not read_thread.is_alive()
poll.assert_called_once_with(timeout=50)
if "exc" in result:
raise result["exc"]
assert result["value"] == payload
with writer.buffer.get_metadata(0) as metadata_buffer:
assert metadata_buffer[0] == 1
assert metadata_buffer[1] == 1
finally:
writer.shutdown()
reader.shutdown()
for socket in (
writer.local_socket,
writer._spin_condition.local_notify_socket,
reader.local_socket,
reader._spin_condition.local_notify_socket,
reader._spin_condition.read_cancel_socket,
reader._spin_condition.write_cancel_socket,
):
socket.close(linger=0)
def test_acquire_read_releases_slot_when_reader_raises():
writer = MessageQueue(
n_reader=1,
n_local_reader=1,
max_chunk_bytes=1024 * 1024,
max_chunks=1,
)
reader = MessageQueue.create_from_handle(writer.export_handle(), rank=0)
try:
writer.wait_until_ready()
reader.wait_until_ready()
writer.enqueue({"payload": "first"})
with (
pytest.raises(RuntimeError, match="reader failed"),
reader.acquire_read(timeout=0.1),
):
raise RuntimeError("reader failed")
with writer.buffer.get_metadata(0) as metadata_buffer:
assert metadata_buffer[0] == 1
assert metadata_buffer[1] == 1
with writer.acquire_write(timeout=0.1) as buf:
buf[0] = 0
finally:
writer.shutdown()
reader.shutdown()
def test_warning_logs(caplog_vllm):
"""
Test that warning logs are emitted at VLLM_RINGBUFFER_WARNING_INTERVAL intervals
@@ -142,8 +142,19 @@ def qwen2audio_aligned_content_and_embeds_b64() -> tuple[str, str]:
@pytest.mark.asyncio
@pytest.mark.parametrize(
"audio_first",
[True, False],
ids=["audio_embeds-then-text", "text-then-audio_embeds"],
[
pytest.param(True, id="audio_embeds-then-text"),
pytest.param(
False,
id="text-then-audio_embeds",
marks=pytest.mark.xfail(
reason="torch 2.12 regression: prompt_embeds output diverges "
"from raw-text when text precedes audio; "
"https://github.com/pytorch/pytorch/issues/184431",
strict=True,
),
),
],
)
async def test_text_content_and_prompt_embeds_match_with_audio_embeds(
qwen2audio_client: openai.AsyncOpenAI,
+2 -2
View File
@@ -82,9 +82,9 @@ def torch_w8a8_block_int8_moe(a, w1, w2, w1_s, w2_s, score, topk, block_shape):
).sum(dim=1)
@pytest.fixture(autouse=True, scope="module")
@pytest.fixture(autouse=True)
def setup_cuda():
"""Sets the default CUDA device for all tests in this module."""
"""Sets the default CUDA device before each test in this module."""
torch.set_default_device("cuda")
@@ -102,9 +102,9 @@ def torch_w8a8_per_column_moe(a, w1, w2, w1_s, w2_s, score, topk):
).sum(dim=1)
@pytest.fixture(autouse=True, scope="module")
@pytest.fixture(autouse=True)
def setup_cuda():
"""Sets the default CUDA device for all tests in this module."""
"""Sets the default CUDA device before each test in this module."""
torch.set_default_device("cuda")
+10 -10
View File
@@ -28,12 +28,6 @@ BLOCK_SIZE = [[128, 128]]
SEEDS = [0]
@pytest.fixture(autouse=True, scope="module")
def setup_cuda():
"""Sets the default CUDA device for all tests in this module."""
torch.set_default_device("cuda")
@pytest.mark.parametrize(
"M,N,K,block_size,out_dtype,seed",
itertools.product(M, N, K, BLOCK_SIZE, DTYPES, SEEDS),
@@ -41,22 +35,28 @@ def setup_cuda():
@torch.inference_mode()
def test_w8a8_block_int8_matmul(M, N, K, block_size, out_dtype, seed):
torch.manual_seed(seed)
device = current_platform.device_type
factor_for_scale = 1e-2
int8_info = torch.iinfo(torch.int8)
int8_max, int8_min = int8_info.max, int8_info.min
A_fp32 = (torch.rand(M, K, dtype=torch.float32) - 0.5) * 2 * int8_max
A_fp32 = torch.rand(M, K, dtype=torch.float32, device=device)
A_fp32 = (A_fp32 - 0.5) * 2 * int8_max
A_fp8 = A_fp32.clamp(min=int8_min, max=int8_max).to(torch.float8_e4m3fn)
B_fp32 = (torch.rand(N, K, dtype=torch.float32) - 0.5) * 2 * int8_max
B_fp32 = torch.rand(N, K, dtype=torch.float32, device=device)
B_fp32 = (B_fp32 - 0.5) * 2 * int8_max
B_fp8 = B_fp32.clamp(min=int8_min, max=int8_max).to(torch.float8_e4m3fn)
block_n, block_k = block_size[0], block_size[1]
n_tiles = (N + block_n - 1) // block_n
k_tiles = (K + block_k - 1) // block_k
As = torch.rand(M, k_tiles, dtype=torch.float32) * factor_for_scale
Bs = torch.rand(n_tiles, k_tiles, dtype=torch.float32) * factor_for_scale
As = torch.rand(M, k_tiles, dtype=torch.float32, device=device) * factor_for_scale
Bs = (
torch.rand(n_tiles, k_tiles, dtype=torch.float32, device=device)
* factor_for_scale
)
ref_out = native_w8a8_block_matmul(A_fp8, B_fp8, As, Bs, block_size, out_dtype)
out = w8a8_block_int8_matmul(A_fp8, B_fp8, As, Bs, block_size, out_dtype)
+12 -10
View File
@@ -82,12 +82,6 @@ def torch_w8a8_per_column_moe(a, w1, w2, w1_s, w2_s, topk, topk_weight, topk_ids
).sum(dim=1)
@pytest.fixture(autouse=True, scope="module")
def setup_cuda():
"""Sets the default CUDA device for all tests in this module."""
torch.set_default_device("cuda")
DTYPES = [torch.half, torch.bfloat16]
M = [1, 33]
N = [128, 1024]
@@ -104,6 +98,7 @@ SEEDS = [0]
@torch.inference_mode()
def test_w8a8_fp8_fused_moe(default_vllm_config, M, N, K, E, topk, dtype, seed):
torch.manual_seed(seed)
device = current_platform.device_type
# Initialize int8 quantization parameters
factor_for_scale = 1e-2
int8_max = 127
@@ -111,19 +106,26 @@ def test_w8a8_fp8_fused_moe(default_vllm_config, M, N, K, E, topk, dtype, seed):
# Input tensor
# M * K
a = torch.randn((M, K), dtype=dtype) / 10
a = torch.randn((M, K), dtype=dtype, device=device) / 10
# Generate int8 weights
w1_fp32 = (torch.rand((E, 2 * N, K), dtype=torch.float32) - 0.5) * 2
w1_fp32 = (
torch.rand(
(E, 2 * N, K),
dtype=torch.float32,
device=device,
)
- 0.5
) * 2
w1 = (w1_fp32 * int8_max).clamp(min=int8_min, max=int8_max).to(torch.int8)
w2_fp32 = (torch.rand((E, K, N), dtype=torch.float32) - 0.5) * 2
w2_fp32 = (torch.rand((E, K, N), dtype=torch.float32, device=device) - 0.5) * 2
w2 = (w2_fp32 * int8_max).clamp(min=int8_min, max=int8_max).to(torch.int8)
# Generate scale for each column (per-column quantization)
w1_s = torch.rand(E, 2 * N, device=w1_fp32.device) * factor_for_scale
w2_s = torch.rand(E, K, device=w2_fp32.device) * factor_for_scale
score = torch.randn((M, E), dtype=dtype)
score = torch.randn((M, E), dtype=dtype, device=device)
score = torch.softmax(score, dim=-1, dtype=torch.float32)
topk_weights, topk_ids = torch.topk(score, topk)
@@ -908,7 +908,15 @@ VLM_TEST_SETTINGS = {
multi_image_prompt="Picture 1: <vlm_image>\nPicture 2: <vlm_image>\nDescribe these two images with one paragraph respectively.", # noqa: E501
max_model_len=4096,
max_num_seqs=2,
num_logprobs=10,
# torch 2.13 accumulates CPU numerical drift in the qwen2_vl multi-image
# path: HF and vLLM agree for a long prefix (~69 tokens) then a token
# flips outside vLLM's top-N only near the end of the generation. The
# window is already at the max_logprobs=20 cap, so widening it further is
# not possible. Treat this as acceptable drift and cap max_tokens on CPU
# so the compared prefix stays before the divergence, keeping the
# multi-image path under test. See pytorch/pytorch#187735.
max_tokens=64 if current_platform.is_cpu() else 128,
num_logprobs=20 if current_platform.is_cpu() else 10,
auto_cls=AutoModelForImageTextToText,
vllm_output_post_proc=model_utils.qwen2_vllm_to_hf_output,
image_size_factors=[(0.25,), (0.25, 0.25, 0.25), (0.25, 0.2, 0.15)],
-7
View File
@@ -487,13 +487,6 @@ _TEXT_GENERATION_EXAMPLE_MODELS = {
"Plamo2ForCausalLM": _HfExamplesInfo(
"pfnet/plamo-2-1b",
trust_remote_code=True,
max_transformers_version="4.57",
transformers_version_reason={
"hf": (
"Custom model code uses `_tied_weight_keys: list[str]` but "
"Transformers v5 now expects `_tied_weight_keys: dict[str, str]`"
)
},
),
"Plamo3ForCausalLM": _HfExamplesInfo(
"pfnet/plamo-3-nict-2b-base",
+3 -1
View File
@@ -9,6 +9,7 @@ from vllm.v1.core.sched.async_scheduler import AsyncScheduler
from vllm.v1.core.sched.output import CachedRequestData, SchedulerOutput
from vllm.v1.outputs import ModelRunnerOutput
from vllm.v1.request import RequestStatus
from vllm.v1.structured_output import StructuredOutputGrammar
from vllm.v1.utils import ConstantList
from .utils import create_requests, create_scheduler
@@ -262,7 +263,7 @@ def test_abort_request_when_structured_output_fsm_cannot_advance():
scheduler = object.__new__(AsyncScheduler)
request = create_requests(num_requests=1, num_tokens=1)[0]
request.structured_output_request = Mock()
request.structured_output_request.grammar = Mock()
request.structured_output_request.grammar = Mock(spec=StructuredOutputGrammar)
request.structured_output_request.grammar.accept_tokens.return_value = False
request.status = RequestStatus.RUNNING
request.num_computed_tokens = request.num_tokens
@@ -284,6 +285,7 @@ def test_abort_request_when_structured_output_fsm_cannot_advance():
scheduler.kv_event_publisher = Mock()
scheduler.finished_req_ids = set()
scheduler.finished_req_ids_dict = None
scheduler.grammar_compile_error_reqs = set()
scheduler.vllm_config = Mock()
scheduler.vllm_config.model_config.enable_return_routed_experts = False
scheduler.enable_return_routed_experts = False
+56 -2
View File
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import dataclasses
from concurrent.futures import Future
from unittest.mock import Mock
import pytest
@@ -39,7 +40,7 @@ from vllm.v1.kv_cache_interface import (
)
from vllm.v1.outputs import DraftTokenIds, KVConnectorOutput, ModelRunnerOutput
from vllm.v1.request import Request, RequestStatus
from vllm.v1.structured_output import StructuredOutputManager
from vllm.v1.structured_output import StructuredOutputGrammar, StructuredOutputManager
from .utils import EOS_TOKEN_ID, create_requests, create_scheduler, mock_kv
@@ -3144,6 +3145,58 @@ def test_schedule_skip_tokenizer_init_structured_output_request():
assert len(scheduler.skipped_waiting) == 1
@pytest.mark.parametrize("async_grammar", [True, False])
def test_grammar_compile_error_finishes_only_request(async_grammar: bool):
scheduler = create_scheduler()
manager = scheduler.structured_output_manager
manager.backend = Mock()
manager.backend.compile_grammar.side_effect = RuntimeError(
"forced FSM compilation error"
)
manager._use_async_grammar_compilation = async_grammar
sampling_params = SamplingParams(
max_tokens=16,
structured_outputs=StructuredOutputsParams(json='{"type": "object"}'),
)
sampling_params.update_from_generation_config({}, EOS_TOKEN_ID)
request = Request(
request_id="grammar-error",
prompt_token_ids=[0, 1],
sampling_params=sampling_params,
pooling_params=None,
)
manager.grammar_init(request)
assert request.structured_output_request is not None
grammar_future = request.structured_output_request._grammar
assert isinstance(grammar_future, Future)
assert isinstance(grammar_future.exception(timeout=5), RuntimeError)
scheduler.add_request(request)
scheduler_output = scheduler.schedule()
assert not scheduler_output.num_scheduled_tokens
engine_core_outputs = scheduler.update_from_output(
scheduler_output,
ModelRunnerOutput(req_ids=[], req_id_to_index={}),
)
assert request.status == RequestStatus.FINISHED_ERROR
assert request.request_id not in scheduler.requests
output = engine_core_outputs[0].outputs[0]
assert output.request_id == request.request_id
assert output.finish_reason == FinishReason.ERROR
assert output.stop_reason is None
healthy_request = create_requests(num_requests=1, req_ids=["healthy-request"])[0]
scheduler.add_request(healthy_request)
next_output = scheduler.schedule()
assert [req.req_id for req in next_output.scheduled_new_reqs] == [
healthy_request.request_id
]
def test_abort_request_when_structured_output_fsm_cannot_advance():
scheduler = object.__new__(Scheduler)
sampling_params = SamplingParams(ignore_eos=True, max_tokens=4)
@@ -3157,7 +3210,7 @@ def test_abort_request_when_structured_output_fsm_cannot_advance():
pooling_params=None,
)
request.structured_output_request = Mock()
request.structured_output_request.grammar = Mock()
request.structured_output_request.grammar = Mock(spec=StructuredOutputGrammar)
request.structured_output_request.grammar.accept_tokens.return_value = False
request.status = RequestStatus.RUNNING
request.num_computed_tokens = request.num_tokens
@@ -3178,6 +3231,7 @@ def test_abort_request_when_structured_output_fsm_cannot_advance():
scheduler.kv_event_publisher = Mock()
scheduler.finished_req_ids = set()
scheduler.finished_req_ids_dict = None
scheduler.grammar_compile_error_reqs = set()
scheduler.vllm_config = Mock()
scheduler.vllm_config.model_config.enable_return_routed_experts = False
scheduler.enable_return_routed_experts = False
+3 -24
View File
@@ -1,11 +1,10 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import weakref
from contextlib import ExitStack
import pytest
from tests.utils import wait_for_gpu_memory_to_clear
from tests.utils import create_new_process_for_each_test
from tests.v1.attention.utils import full_cg_backend_configs as backend_configs
from vllm import LLM
from vllm.config import CompilationConfig, CompilationMode
@@ -32,6 +31,7 @@ else:
@pytest.mark.parametrize("backend_name, cudagraph_mode, supported", combo_cases_1)
@create_new_process_for_each_test("spawn")
def test_backend_and_cudagraph_mode_combo(backend_name, cudagraph_mode, supported):
if backend_name == "FlashInfer":
try:
@@ -64,17 +64,6 @@ def test_backend_and_cudagraph_mode_combo(backend_name, cudagraph_mode, supporte
),
)
llm.generate(["Hello, my name is"] * 10)
# when above code raises, `llm` may be undefined, so we need to catch that
try:
llm = weakref.proxy(llm)
del llm
except UnboundLocalError:
pass
wait_for_gpu_memory_to_clear(
devices=[0],
threshold_ratio=0.1,
)
# test cudagraph_mode with different compilation mode.
@@ -98,6 +87,7 @@ combo_cases_2 = [
@pytest.mark.parametrize(
"backend_name,cudagraph_mode,compilation_mode,supported", combo_cases_2
)
@create_new_process_for_each_test("spawn")
def test_cudagraph_compilation_combo(
backend_name, cudagraph_mode, compilation_mode, supported
):
@@ -120,14 +110,3 @@ def test_cudagraph_compilation_combo(
),
)
llm.generate(["Hello, my name is"] * 10)
# when above code raises, `llm` may be undefined, so we need to catch that
try:
llm = weakref.proxy(llm)
del llm
except UnboundLocalError:
pass
finally:
wait_for_gpu_memory_to_clear(
devices=[0],
threshold_ratio=0.1,
)
@@ -61,7 +61,17 @@ def test_nixl_and_nixl_ep_imports() -> None:
importlib.import_module("nixl._bindings")
# Exercise the NIXL EP extension used by fused MoE expert parallelism.
nixl_ep = importlib.import_module("nixl_ep")
try:
nixl_ep = importlib.import_module("nixl_ep")
except ImportError as e:
if "materialize_cow_storage" in str(e) or "undefined symbol" in str(e):
pytest.xfail(
"nixl_ep prebuilt extension is ABI-incompatible with this torch "
"(undefined symbol c10::impl::cow::materialize_cow_storage); "
"needs a nixl rebuild against torch 2.13. "
"See pytorch/pytorch#187727 and ai-dynamo/nixl#1798."
)
raise
print(f"nixl_ep: {nixl_ep.__file__}")
assert nixl_ep.__file__ is not None
@@ -410,7 +410,7 @@ def test_lookup_key_client_lookup_prepends_typed_tag():
# Blocking lookup (non_block defaults to False) runs on the executor and
# returns the resolved hit length.
assert client.lookup("req0", token_len=128, block_hashes=[]) == 5
assert client.lookup("req0", num_tokens=128, block_hashes=[]) == 5
sent_frames = fake_socket.send_multipart.call_args[0][0]
assert sent_frames[0] == protocol.LOOKUP_MSG
@@ -439,11 +439,11 @@ def test_lookup_key_client_reset_uses_typed_protocol():
assert client.reset() is False
def _poll_lookup(client, req_id, token_len=128, block_hashes=(), timeout=5.0):
def _poll_lookup(client, req_id, num_tokens=128, block_hashes=(), timeout=5.0):
"""Drive non-blocking lookup until the executor completes it."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
result = client.lookup(req_id, token_len, list(block_hashes), non_block=True)
result = client.lookup(req_id, num_tokens, list(block_hashes), non_block=True)
if result is not None:
return result
time.sleep(0.005)
@@ -240,7 +240,10 @@ def test_e2e_swa_plus_full_save_then_lookup_hits():
worker.store = store
# Both groups stored all 4 blocks -> full hit.
assert worker.lookup(token_len=64, block_hashes=hs) == 64
assert worker.lookup(num_tokens=65, block_hashes=hs) == 64
# Exact-multiple prompt: the full hit is re-derived one block lower,
# where both groups' stored blocks still cover the SWA window.
assert worker.lookup(num_tokens=64, block_hashes=hs) == 48
# Evict SWA's first two blocks (outside its window of 32 tokens = 2 blocks).
swa_keys_outside_window = [
@@ -253,7 +256,12 @@ def test_e2e_swa_plus_full_save_then_lookup_hits():
# SWA window=32 -> only last 2 blocks must be present in SWA group.
# Full has all 4. Coordinator should still return 64.
assert worker.lookup(token_len=64, block_hashes=hs) == 64
assert worker.lookup(num_tokens=65, block_hashes=hs) == 64
# Exact-multiple prompt after eviction: the boundary one block lower
# needs SWA block 1, which is gone — no usable stored boundary remains
# (the pre-fix arithmetic clamp would have returned 48 and livelocked
# on load failure -> recompute -> same lookup).
assert worker.lookup(num_tokens=64, block_hashes=hs) == 0
def test_recv_skips_swa_blocks_before_window():
@@ -0,0 +1,90 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for ChunkedTokenDatabase.prepare_values."""
import random
import pytest
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store.data import (
ChunkedTokenDatabase,
KeyMetadata,
)
from vllm.utils.math_utils import cdiv
BLOCK_SIZE = 128
def _reference_prepare_value(
db: ChunkedTokenDatabase, start: int, end: int, block_ids: list[int]
) -> tuple[list[int], list[int], int]:
"""Compute a token range with the original scalar implementation."""
addr_list = []
size_list = []
block_id = block_ids[start // db.block_size]
length = len(db.block_len)
for index, base_addr in enumerate(db.kv_caches_base_addr):
addr = base_addr + block_id * db.block_len[index % length]
assert (end - start) % db.block_size == 0
size = db.block_len[index % length] * cdiv(end - start, db.block_size)
addr_list.append(addr)
size_list.append(size)
return addr_list, size_list, block_id
def _make_db(num_regions: int, num_block_lens: int) -> ChunkedTokenDatabase:
md = KeyMetadata(model_name="t", tp_rank=1, pcp_rank=0, dcp_rank=0, pp_rank=0)
db = ChunkedTokenDatabase(md, BLOCK_SIZE)
db.set_kv_caches_base_addr(
[0x7F00_0000_0000 + i * (1 << 30) for i in range(num_regions)]
)
# Exercise repeated block lengths when there are more cache regions.
db.set_block_len([30_208 + 512 * i for i in range(num_block_lens)])
return db
@pytest.mark.parametrize("num_regions,num_block_lens", [(96, 96), (96, 2), (1, 1)])
def test_prepare_values_matches_reference(num_regions: int, num_block_lens: int):
db = _make_db(num_regions, num_block_lens)
rng = random.Random(0)
n_blocks = 300
block_ids = [rng.randrange(0, 1 << 20) for _ in range(n_blocks)]
chunks = []
b = 0
while b < n_blocks - 4:
span = rng.choice([1, 1, 1, 2, 4])
chunks.append((b * BLOCK_SIZE, (b + span) * BLOCK_SIZE))
b += span + rng.choice([0, 1])
addrs, sizes, bids = db.prepare_values(chunks, block_ids)
assert len(addrs) == len(sizes) == len(bids) == len(chunks)
for (start, end), addr, size, bid in zip(chunks, addrs, sizes, bids):
ref_addr, ref_size, ref_bid = _reference_prepare_value(
db, start, end, block_ids
)
assert addr == ref_addr
assert size == ref_size
assert bid == ref_bid
# Native bindings require Python ints rather than numpy scalars.
assert all(type(a) is int for a in addr)
assert type(bid) is int
def test_prepare_value_single_matches_reference():
db = _make_db(8, 8)
block_ids = list(range(64))
got = db.prepare_value(5 * BLOCK_SIZE, 7 * BLOCK_SIZE, block_ids)
assert got == _reference_prepare_value(
db, 5 * BLOCK_SIZE, 7 * BLOCK_SIZE, block_ids
)
def test_prepare_values_empty():
db = _make_db(4, 4)
assert db.prepare_values([], [1, 2, 3]) == ([], [], [])
def test_prepare_values_rejects_unaligned_chunk():
db = _make_db(4, 4)
with pytest.raises(AssertionError):
db.prepare_values([(0, BLOCK_SIZE + 1)], [0, 1])
@@ -473,27 +473,25 @@ def test_from_request_tracker_no_load_saves_normally():
class _StubLookupClient:
def __init__(self, hit_tokens: int) -> None:
self._hit_tokens = hit_tokens
self.num_tokens: list[int] = []
def lookup(
self,
req_id: str,
token_len: int,
num_tokens: int,
block_hashes: list[bytes],
non_block: bool = False,
) -> int:
self.num_tokens.append(num_tokens)
return self._hit_tokens
def test_full_external_hit_keeps_kvpool_cached_tokens_block_aligned():
# When the external store hits the entire prompt, scheduler must leave at
# least one token uncomputed for sampling but stay on a block boundary.
# Otherwise the recv-side load mask floors token_len to
# (num_tokens-1)//block_size, the tail partial chunk is dropped, and -- if
# the local cache covers the aligned prefix -- key_list ends up empty
# (ZeroDivisionError in the recv thread's `tp_rank % len(key_list)`).
# The worker re-derives a full external hit below the request end on an
# existing boundary, so the scheduler receives the usable aligned hit.
scheduler = _make_bare_scheduler()
scheduler.load_async = True
scheduler.client = _StubLookupClient(hit_tokens=48) # full hit on 48-token prompt
scheduler.client = _StubLookupClient(hit_tokens=32)
request = SimpleNamespace(
request_id="req-0",
@@ -510,6 +508,7 @@ def test_full_external_hit_keeps_kvpool_cached_tokens_block_aligned():
assert need_to_allocate == 16
assert load_async is True
load_spec = scheduler.load_specs["req-0"]
assert scheduler.client.num_tokens == [48]
assert load_spec.vllm_cached_tokens == 16
assert load_spec.kvpool_cached_tokens == 32
assert load_spec.kvpool_cached_tokens % 16 == 0
@@ -522,7 +521,7 @@ def test_full_external_hit_with_full_local_hit_skips_load():
# into any block-aligned key.
scheduler = _make_bare_scheduler()
scheduler.load_async = True
scheduler.client = _StubLookupClient(hit_tokens=48)
scheduler.client = _StubLookupClient(hit_tokens=32)
request = SimpleNamespace(
request_id="req-0",
@@ -655,6 +655,68 @@ def test_store_sending_thread_delta_saves_only_new_masked_chunks():
assert masked_hashes == [b"a2".hex()]
def test_store_sending_thread_prepares_missing_chunks_once_per_group():
store = MagicMock()
store.batch_is_exist.return_value = [0, 1, 0, 1, 0, 0]
store.batch_put_from_multi_buffers.return_value = [256, 256, 512, 512]
coord = SimpleNamespace(
lcm_block_size=16,
store_mask=lambda token_len, start_token, num_prompt_tokens=None: (
None,
None,
),
)
db0 = ChunkedTokenDatabase(
KeyMetadata("test-model", 0, 0, 0, 0, group_id=0),
block_size=16,
)
db0.set_kv_caches_base_addr([0x1000])
db0.set_block_len([256])
db0.prepare_values = MagicMock(wraps=db0.prepare_values)
db0.prepare_value = MagicMock(side_effect=AssertionError("scalar path called"))
db1 = ChunkedTokenDatabase(
KeyMetadata("test-model", 0, 0, 0, 0, group_id=1),
block_size=16,
)
db1.set_kv_caches_base_addr([0x2000])
db1.set_block_len([512])
db1.prepare_values = MagicMock(wraps=db1.prepare_values)
db1.prepare_value = MagicMock(side_effect=AssertionError("scalar path called"))
thread = _make_store_sending_thread(
store,
coord=coord,
token_databases=[db0, db1],
)
thread.add_stored_request("req-a")
thread._handle_request(
ReqMeta(
req_id="req-a",
token_len_chunk=48,
block_ids=([0, 1, 2], [2, 1, 0]),
block_hashes=[b"a0", b"a1", b"a2"],
can_save=True,
)
)
db0.prepare_value.assert_not_called()
db1.prepare_value.assert_not_called()
db0.prepare_values.assert_called_once_with([(0, 16), (32, 48)], [0, 1, 2])
db1.prepare_values.assert_called_once_with([(16, 32), (32, 48)], [2, 1, 0])
keys, addrs, sizes, _ = store.batch_put_from_multi_buffers.call_args.args
assert [key.rsplit("@", 1)[-1] for key in keys] == [
"6130",
"6132",
"6131",
"6132",
]
assert addrs == [[0x1000], [0x1200], [0x2200], [0x2000]]
assert sizes == [[256], [256], [512], [512]]
def test_store_sending_thread_only_skips_on_no_available_handle():
store = MagicMock()
store.batch_is_exist.side_effect = lambda keys: [0] * len(keys)
@@ -1675,6 +1737,61 @@ def test_lookup_partial_prefix_returns_first_hit_length():
assert worker.lookup(48, [b"a0", b"a1", b"a2"]) == 32
def test_lookup_full_hit_reuses_existing_boundary():
"""A full hit is re-derived below the request end without another RPC."""
worker = _make_bare_worker(block_size=16)
worker.store.batch_is_exist.return_value = [1, 1]
assert worker.lookup(32, [b"h0", b"h1"]) == 16
assert worker.store.batch_is_exist.call_count == 1
def test_lookup_full_hit_with_eagle_pops_once_not_twice():
"""Eagle already leaves the last block for the drafter, so a
full-prompt re-derivation must never fire for eagle-governed hits:
firing would anchor the search one block lower and pop a second
block, regressing the hit by an extra producer boundary."""
worker = _make_bare_worker(block_size=16)
worker.coord = mooncake_store_worker.MooncakeStoreCoordinator(
worker._kv_cache_groups,
scheduler_block_size=16,
hash_block_size=16,
use_eagle=True,
)
worker.store.batch_is_exist.return_value = [1, 1, 1, 1]
# 64-token exact-multiple prompt, all 4 blocks stored: one eagle pop
# gives 48; a spurious re-derivation (anchored at 48) would pop again
# and return 32.
assert worker.lookup(64, [b"h0", b"h1", b"h2", b"h3"]) == 48
assert worker.store.batch_is_exist.call_count == 1
def test_lookup_full_hit_swa_degrades_when_no_stored_boundary_is_usable():
"""The motivating livelock: the producer of a 64-token prompt stored
only its SWA tail window (blocks 2-3). The old arithmetic clamp turned
the full hit into 48, whose SWA window needs the never-written block 1,
so every load failed and the recompute re-entered the same lookup. The
re-derivation must report that no stored boundary below the request end
is usable."""
from vllm.v1.kv_cache_interface import KVCacheGroupSpec, SlidingWindowSpec
worker = _make_bare_worker(block_size=16)
swa = SlidingWindowSpec(
block_size=16, num_kv_heads=8, head_size=64, dtype=None, sliding_window=32
)
worker._kv_cache_groups = [KVCacheGroupSpec(["layer0"], swa)]
worker.coord = mooncake_store_worker.MooncakeStoreCoordinator(
worker._kv_cache_groups,
scheduler_block_size=worker.hash_block_size,
hash_block_size=worker.hash_block_size,
)
worker.store.batch_is_exist.return_value = [0, 0, 1, 1]
assert worker.lookup(64, [b"h0", b"h1", b"h2", b"h3"]) == 0
assert worker.store.batch_is_exist.call_count == 1
def test_lookup_swa_single_group_returns_full_when_tail_window_present():
"""Single-SWA, sliding_window=32 (= 2 blocks): producer stored only the
tail. Coordinator-driven lookup returns full prefix even though the
@@ -1692,7 +1809,7 @@ def test_lookup_swa_single_group_returns_full_when_tail_window_present():
hash_block_size=worker.hash_block_size,
)
worker.store.batch_is_exist.return_value = [0, 0, 1, 1]
assert worker.lookup(64, [b"h0", b"h1", b"h2", b"h3"]) == 64
assert worker.lookup(65, [b"h0", b"h1", b"h2", b"h3"]) == 64
def test_lookup_checks_all_potential_swa_hit_boundaries():
@@ -2157,7 +2274,7 @@ def test_lookup_records_mooncake_metrics():
worker = _make_bare_worker()
worker.store.batch_is_exist.return_value = [1, 1]
result = worker.lookup(32, [b"a0", b"a1"])
result = worker.lookup(33, [b"a0", b"a1"])
stats = worker.get_kv_connector_stats()
assert result == 32
@@ -412,3 +412,65 @@ def test_block_verification_accepts_at_least_as_many(num_speculative_steps: int)
f"Block verification mean accepted length {mean_block:.4f} is worse "
f"than standard {mean_standard:.4f}."
)
@pytest.mark.parametrize("has_draft_logits", [True, False])
def test_chunked_requests_match_full_batch(has_draft_logits: bool):
torch.manual_seed(7)
device = "cuda"
num_reqs = 5
num_speculative_steps = 3
vocab_size = 257
target_logits = torch.randn(vocab_size, device=device)
draft_logits = torch.randn(vocab_size, device=device)
inputs = _build_rejection_sample_inputs(
target_logits,
draft_logits,
num_speculative_steps,
temperature=0.6,
num_trials=num_reqs,
)
padded_target_logits = torch.empty(
inputs["target_logits"].shape[0], vocab_size + 3, device=device
)
padded_target_logits[:, :vocab_size].copy_(inputs["target_logits"])
inputs["target_logits"] = padded_target_logits[:, :vocab_size]
assert inputs["target_logits"].stride(-1) == 1
assert not inputs["target_logits"].is_contiguous()
if not has_draft_logits:
inputs["draft_logits"] = None
sampled, num_sampled = rejection_sample(
**inputs, num_speculative_steps=num_speculative_steps
)
sampled_chunks = []
num_sampled_chunks = []
for start, end in ((0, 2), (2, 5)):
lo = start * (num_speculative_steps + 1)
hi = end * (num_speculative_steps + 1)
chunk_inputs = dict(inputs)
for name in (
"target_logits",
"draft_sampled",
"pos",
"expanded_idx_mapping",
"expanded_local_pos",
):
chunk_inputs[name] = inputs[name][lo:hi]
chunk_inputs["cu_num_logits"] = inputs["cu_num_logits"][start : end + 1] - lo
chunk_inputs["idx_mapping"] = inputs["idx_mapping"][start:end]
chunk_sampled, chunk_num_sampled = rejection_sample(
**chunk_inputs, num_speculative_steps=num_speculative_steps
)
sampled_chunks.append(chunk_sampled)
num_sampled_chunks.append(chunk_num_sampled)
chunked_sampled = torch.cat(sampled_chunks)
chunked_num_sampled = torch.cat(num_sampled_chunks)
assert torch.equal(chunked_num_sampled, num_sampled)
steps = torch.arange(num_speculative_steps + 1, device=device)
valid = steps.unsqueeze(0) < num_sampled.unsqueeze(1)
assert torch.equal(chunked_sampled[valid], sampled[valid])
@@ -73,6 +73,7 @@ class TestReasoningStructuredOutput:
request.all_token_ids = [1, 2, 3, 4, 5, 6, 7, 8]
request.num_computed_tokens = 5
request.num_output_placeholders = 0
request.request_id = "mock_req"
return request
@pytest.fixture
@@ -208,37 +209,11 @@ class TestReasoningStructuredOutput:
mock_request_with_structured_output
)
# Should set reasoning_ended to True but return False for this step
# The scheduler trims the reasoning prefix before advancing the grammar.
assert (
mock_request_with_structured_output.structured_output_request.reasoning_ended
is True
)
assert result is False
def test_should_advance_reasoning_just_ended_with_spec_decode_structural_tag(
self,
manager_with_reasoner,
mock_request_with_structured_output,
):
"""When reasoning ends this step, advance immediately for structural
tags with speculative decoding."""
structured_req = mock_request_with_structured_output.structured_output_request
structured_req.reasoning_ended = False
structured_req.structured_output_key = (
StructuredOutputOptions.STRUCTURAL_TAG,
"{}",
)
reasoner = MockReasoner(tokenizer=Mock())
reasoner.is_reasoning_end_streaming.return_value = True
structured_req.reasoner = reasoner
manager_with_reasoner.vllm_config.speculative_config = Mock()
result = manager_with_reasoner.should_advance(
mock_request_with_structured_output
)
assert structured_req.reasoning_ended is True
assert result is True
def test_should_advance_reasoning_already_ended(
@@ -258,3 +233,120 @@ class TestReasoningStructuredOutput:
# Should return True since reasoning has ended
assert result is True
def test_should_advance_uses_new_token_ids_when_provided(
self,
manager_with_reasoner,
mock_request_with_structured_output,
):
"""Regression for #43388: when caller passes new_token_ids, the
reasoner sees the exact multi-token delta rather than the
placeholder-derived window.
"""
structured_req = mock_request_with_structured_output.structured_output_request
structured_req.reasoning_ended = False
end_token_id = 248069
reasoner = MockReasoner(tokenizer=Mock())
# Detection mirrors the real Qwen3 parser: end token in the delta.
reasoner.is_reasoning_end_streaming = Mock(
side_effect=lambda input_ids, delta_ids: end_token_id in list(delta_ids)
)
structured_req.reasoner = reasoner
# Scenario from #43388: async + spec decode K=4, 4 tokens accepted
# but only 1 placeholder remains (some drafts were rejected).
# The placeholder math would yield delta=[271] and miss </think>.
# Passing new_token_ids must override that.
new_token_ids = [9, 198, end_token_id, 271]
mock_request_with_structured_output.all_token_ids = [
1,
2,
3,
4,
5,
] + new_token_ids
mock_request_with_structured_output.num_computed_tokens = 9
mock_request_with_structured_output.num_output_placeholders = 1
result = manager_with_reasoner.should_advance(
mock_request_with_structured_output,
new_token_ids=new_token_ids,
)
# First call to is_reasoning_end_streaming was with the full
# new_token_ids (not the truncated placeholder window).
first_call = reasoner.is_reasoning_end_streaming.call_args_list[0]
_, called_delta = first_call.args
assert list(called_delta) == new_token_ids
assert structured_req.reasoning_ended is True
assert result is True
def test_should_advance_without_new_token_ids_falls_back(
self,
manager_with_reasoner,
mock_request_with_structured_output,
):
"""Backward compat: callers that don't pass new_token_ids keep
the original placeholder-derived delta window.
"""
structured_req = mock_request_with_structured_output.structured_output_request
structured_req.reasoning_ended = False
reasoner = MockReasoner(tokenizer=Mock())
reasoner.is_reasoning_end_streaming.return_value = False
structured_req.reasoner = reasoner
mock_request_with_structured_output.all_token_ids = [1, 2, 3, 4, 5]
mock_request_with_structured_output.num_computed_tokens = 5
mock_request_with_structured_output.num_output_placeholders = 2
result = manager_with_reasoner.should_advance(
mock_request_with_structured_output
)
# placeholder window: start = 5 - 2 = 3, delta = [4, 5]
_, called_delta = reasoner.is_reasoning_end_streaming.call_args[0]
assert list(called_delta) == [4, 5]
assert result is False
def test_should_advance_trims_reasoning_prefix_for_json(
self,
manager_with_reasoner,
mock_request_with_structured_output,
):
"""JSON uses the common trim-then-advance path at the boundary."""
structured_req = mock_request_with_structured_output.structured_output_request
structured_req.reasoning_ended = False
structured_req.structured_output_key = (
StructuredOutputOptions.JSON_OBJECT,
"{}",
)
marker = 248069
class MarkerReasoner:
def __init__(self, *_, **__):
pass
def is_reasoning_end_streaming(self, input_ids, delta_ids):
return marker in list(delta_ids)
structured_req.reasoner = MarkerReasoner()
new_token_ids = [9, 198, marker, 271, 5005]
mock_request_with_structured_output.all_token_ids = [1, 2, 3] + new_token_ids
result = manager_with_reasoner.should_advance(
mock_request_with_structured_output,
new_token_ids=new_token_ids,
)
structured_req.grammar.accept_tokens.assert_not_called()
assert structured_req.reasoning_ended is True
assert result is True
assert structured_req.reasoning_end_token_index == 5
assert manager_with_reasoner.trim_reasoning_for_advance(
mock_request_with_structured_output, new_token_ids
) == [271, 5005]
+26 -1
View File
@@ -2,7 +2,32 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from unittest import TestCase
from vllm.v1.outputs import LogprobsLists
import torch
from vllm.v1.outputs import LogprobsLists, LogprobsTensors
def test_logprobs_tensors_cat():
first = LogprobsTensors(
torch.tensor([[1, 2]]),
torch.tensor([[0.1, 0.2]]),
torch.tensor([1]),
)
second = LogprobsTensors(
torch.tensor([[3, 4]]),
torch.tensor([[0.3, 0.4]]),
torch.tensor([2]),
)
result = LogprobsTensors.cat([first, second], [0, 1, 2])
assert result.logprob_token_ids.tolist() == [[1, 2], [3, 4]]
assert result.logprobs.tolist() == (
first.logprobs.tolist() + second.logprobs.tolist()
)
assert result.selected_token_ranks.tolist() == [1, 2]
assert result.cu_num_generated_tokens == [0, 1, 2]
assert LogprobsTensors.cat([first]) is first
class TestLogprobsLists(TestCase):
@@ -0,0 +1,109 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from types import MethodType, SimpleNamespace
from typing import get_args
import numpy as np
import pytest
import torch
from vllm.config.model import PROCESSED_LOGPROBS_MODES, LogprobsMode
from vllm.platforms import current_platform
from vllm.v1.worker.gpu.spec_decode.rejection_sampler import (
RejectionSampler,
_iter_request_chunks,
)
def test_iter_request_chunks_preserves_request_boundaries():
cu_num_logits = np.array([0, 3, 4, 11, 13], dtype=np.int32)
assert list(_iter_request_chunks(cu_num_logits, max_chunk_logits=5)) == [
(0, 2),
(2, 3),
(3, 4),
]
@pytest.mark.skipif(not current_platform.is_cuda(), reason="Requires CUDA")
@pytest.mark.parametrize("logprobs_mode", get_args(LogprobsMode))
def test_chunked_scores_match_full_batch(logprobs_mode: str):
device = torch.device("cuda")
cu_num_logits_np = np.array([0, 3, 4, 8, 10], dtype=np.int32)
num_logits_per_req = np.diff(cu_num_logits_np)
idx_mapping_np = np.array([7, 2, 9, 1], dtype=np.int32)
input_batch = SimpleNamespace(
num_reqs=4,
cu_num_logits_np=cu_num_logits_np,
cu_num_logits=torch.from_numpy(cu_num_logits_np).to(device),
idx_mapping_np=idx_mapping_np,
idx_mapping=torch.from_numpy(idx_mapping_np).to(device),
expanded_idx_mapping=torch.from_numpy(
np.repeat(idx_mapping_np, num_logits_per_req)
).to(device),
expanded_local_pos=torch.from_numpy(
np.concatenate(
[np.arange(count, dtype=np.int32) for count in num_logits_per_req]
)
).to(device),
)
rejection_sampler = object.__new__(RejectionSampler)
rejection_sampler.sampler = SimpleNamespace(logprobs_mode=logprobs_mode)
rejection_sampler.num_speculative_steps = 3
def fake_verify(
self,
logits,
_draft_logits,
_draft_sampled,
_pos,
cu_num_logits,
idx_mapping,
*_mappings,
):
num_sampled = torch.diff(cu_num_logits).to(torch.int32)
sampled = (
idx_mapping.to(torch.int64).unsqueeze(1) + torch.arange(4, device=device)
) % logits.shape[1]
return logits.float() + 1, sampled, num_sampled
rejection_sampler._verify = MethodType(fake_verify, rejection_sampler)
logits = torch.arange(170, dtype=torch.float32, device=device).view(10, 17)
sampled, num_sampled, chunked_logprobs = rejection_sampler._verify_in_chunks(
logits,
input_batch,
draft_logits=None,
draft_sampled=torch.arange(10, device=device),
pos=torch.arange(10, device=device),
max_chunk_logits=5,
max_num_logprobs=2,
)
score_logits = logits + 1 if logprobs_mode in PROCESSED_LOGPROBS_MODES else logits
full_logprobs = rejection_sampler._get_logprobs_tensors(
sampled,
num_sampled,
score_logits,
input_batch.cu_num_logits,
input_batch.cu_num_logits_np,
max_num_logprobs=2,
)
assert sampled[:, 0].tolist() == idx_mapping_np.tolist()
assert num_sampled.tolist() == num_logits_per_req.tolist()
assert chunked_logprobs is not None
assert full_logprobs is not None
assert torch.equal(
chunked_logprobs.logprob_token_ids,
full_logprobs.logprob_token_ids,
)
assert torch.equal(chunked_logprobs.logprobs, full_logprobs.logprobs)
assert torch.equal(
chunked_logprobs.selected_token_ranks,
full_logprobs.selected_token_ranks,
)
assert (
chunked_logprobs.cu_num_generated_tokens
== full_logprobs.cu_num_generated_tokens
)
+4 -7
View File
@@ -14,14 +14,10 @@ def test_block_ids_are_not_overwritten_while_copy_is_in_flight():
page_size_el = 4
storage = torch.ones((num_blocks, page_size_el), dtype=torch.int32, device=device)
# Build the minimal zeroer state directly so the test can focus on ID-buffer
# lifetime without constructing model attention groups.
# Build the minimal zeroer state directly so the test can focus on the
# in-flight copy behavior without constructing model attention groups.
zeroer = KVBlockZeroer.__new__(KVBlockZeroer)
zeroer.device = device
zeroer.pin_memory = True
zeroer.max_concurrency = 2
zeroer._id_cap = 8
zeroer._allocate_id_buffers()
zeroer._meta = (
torch.tensor([storage.data_ptr()], dtype=torch.uint64, device=device),
page_size_el,
@@ -32,7 +28,8 @@ def test_block_ids_are_not_overwritten_while_copy_is_in_flight():
stream = torch.cuda.Stream()
with torch.cuda.stream(stream):
# Keep the first nonblocking H2D copy pending while the host submits the
# second call. A single shared pinned source would be overwritten here.
# second call. Each call must stage from its own pinned source so the
# first copy is not corrupted before it runs.
torch.cuda._sleep(10_000_000)
zeroer.zero_block_ids([1])
zeroer.zero_block_ids([2])
+4
View File
@@ -90,6 +90,10 @@ ModelDType = Literal["auto", "half", "float16", "bfloat16", "float", "float32"]
LogprobsMode = Literal[
"raw_logits", "raw_logprobs", "processed_logits", "processed_logprobs"
]
PROCESSED_LOGPROBS_MODES: tuple[LogprobsMode, ...] = (
"processed_logits",
"processed_logprobs",
)
HfOverrides = dict[str, Any] | Callable[[PretrainedConfig], PretrainedConfig]
ModelImpl = Literal["auto", "vllm", "transformers", "terratorch"]
LayerBlockType = Literal["attention", "linear_attention", "mamba"]
@@ -57,6 +57,11 @@ if TYPE_CHECKING:
from _typeshed import SizedBuffer
VLLM_RINGBUFFER_WARNING_INTERVAL = envs.VLLM_RINGBUFFER_WARNING_INTERVAL
# Cap on how long an idle reader parks before re-reading the authoritative SHM
# written-flag. Bounds lost-notify recovery latency to ~5s while the periodic
# wakeup stays negligible (one flag check per reader every 5s).
SHM_READER_RECHECK_INTERVAL_MS = 5000
from_bytes_big = functools.partial(int.from_bytes, byteorder="big")
@@ -631,25 +636,22 @@ class MessageQueue:
self.n_warning = 1
self.timeout = timeout
def timeout_ms(self) -> int | None:
"""Returns a timeout that is:
def timeout_ms(self) -> int:
"""Returns a timeout, capped at the recheck interval, that is:
- min(time to deadline, time to next warning) if we're logging warnings
- time to deadline, if we're not logging warnings
- None if the timeout is None and we're not logging warnings
- recheck interval if the timeout is None and we're not logging warnings
- raise TimeoutError if we are past the deadline
"""
warning_wait_time = self.warning_wait_time_ms
wait_ms = SHM_READER_RECHECK_INTERVAL_MS
if self.warning_wait_time_ms is not None:
wait_ms = min(wait_ms, self.warning_wait_time_ms)
if self.timeout is None:
return warning_wait_time
return wait_ms
time_left_ms = int((self.deadline - time.monotonic()) * 1000)
if time_left_ms <= 0:
raise TimeoutError
if warning_wait_time and warning_wait_time < time_left_ms:
return warning_wait_time
return time_left_ms
return min(wait_ms, time_left_ms)
def should_warn(self) -> bool:
"""Returns true if it's time to log a warning for a timeout that is not
@@ -710,18 +712,18 @@ class MessageQueue:
# found a block that is not read by this reader
# let caller read from the buffer
with self.buffer.get_data(self.current_idx) as buf:
yield buf
# caller has read from the buffer
# set the read flag
metadata_buffer[self.local_reader_rank + 1] = 1
# Memory fence ensures the read flag is visible to the writer.
# Without this, writer may not see our read completion and
# could wait indefinitely for all readers to finish.
memory_fence()
self.current_idx = (self.current_idx + 1) % self.buffer.max_chunks
self._spin_condition.record_read()
try:
yield buf
finally:
# caller has read from the buffer; set the read flag.
metadata_buffer[self.local_reader_rank + 1] = 1
# Memory fence ensures the read flag is visible to the writer.
# Without this, writer may not see our read completion and
# could wait indefinitely for all readers to finish.
memory_fence()
next_idx = self.current_idx + 1
self.current_idx = next_idx % self.buffer.max_chunks
self._spin_condition.record_read()
break
def enqueue(self, obj, timeout: float | None = None):
@@ -93,6 +93,9 @@ class MooncakeStoreCoordinator:
self.eagle_group_ids = set(range(len(kv_cache_groups)))
self._verify_and_split_kv_cache_groups()
def align_lookup_length(self, length: int) -> int:
return length // self.lcm_block_size * self.lcm_block_size
def _verify_and_split_kv_cache_groups(self) -> None:
"""Mirrors KVCacheCoordinator.verify_and_split_kv_cache_groups but
dispatches via spec_manager_map (we don't allocate managers).
@@ -9,6 +9,7 @@ from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from typing import cast
import numpy as np
import torch
from vllm.distributed.kv_transfer.kv_connector.v1.base import (
@@ -196,22 +197,46 @@ class ChunkedTokenDatabase:
def prepare_value(
self, start: int, end: int, block_ids: list[int]
) -> tuple[list[int], list[int], int]:
"""Compute memory addresses and sizes for a token range.
"""Compute memory addresses and sizes for a single token range.
Returns:
(addr_list, size_list, block_id)
"""
addr_list = []
size_list = []
block_id = block_ids[start // self.block_size]
addr_lists, size_lists, chunk_block_ids = self.prepare_values(
((start, end),), block_ids
)
return addr_lists[0], size_lists[0], chunk_block_ids[0]
def prepare_values(
self,
chunks: Sequence[tuple[int, int]],
block_ids: list[int],
) -> tuple[list[list[int]], list[list[int]], list[int]]:
"""Compute memory addresses and sizes for multiple token ranges.
Returns:
(addr_lists, size_lists, chunk_block_ids), one entry per chunk.
"""
if not chunks:
return [], [], []
base = np.asarray(self.kv_caches_base_addr, dtype=np.int64)
length = len(self.block_len)
for index, base_addr in enumerate(self.kv_caches_base_addr):
addr = base_addr + block_id * self.block_len[index % length]
assert (end - start) % self.block_size == 0
size = self.block_len[index % length] * cdiv(end - start, self.block_size)
addr_list.append(addr)
size_list.append(size)
return addr_list, size_list, block_id
blen = np.asarray(
[self.block_len[i % length] for i in range(base.shape[0])],
dtype=np.int64,
)
n = len(chunks)
starts = np.fromiter((c[0] for c in chunks), dtype=np.int64, count=n)
spans = np.fromiter((c[1] for c in chunks), dtype=np.int64, count=n) - starts
assert not (spans % self.block_size).any()
bids = np.fromiter(
(block_ids[i] for i in (starts // self.block_size).tolist()),
dtype=np.int64,
count=n,
)
addrs = base[None, :] + bids[:, None] * blen[None, :]
sizes = blen[None, :] * (spans // self.block_size)[:, None]
return addrs.tolist(), sizes.tolist(), bids.tolist()
def process_tokens(
self,
@@ -10,7 +10,8 @@ Wire format (REQ/REP over IPC):
Request: [msg_type: bytes] [payload_frames...]
msg_type == LOOKUP_MSG:
frame 1: token_len (u32 big-endian, 4 bytes)
frame 1: num_tokens (u32 big-endian, 4 bytes); the worker derives
the aligned lookup length
frame 2: hash_len (u16 big-endian, 2 bytes) byte length of each
fixed-size block hash (0 when there are no hashes)
frame 3: raw block hashes concatenated back-to-back (each hash_len
@@ -80,14 +80,12 @@ class MooncakeStoreScheduler:
Returns ``(None, False)`` when an async lookup is still in flight,
signaling the scheduler to retry this request on a later step.
"""
# Look up against the full prefill range, not just the prompt.
token_len = request.num_tokens // self._block_size * self._block_size
if token_len < self._block_size:
if request.num_tokens < self._block_size:
return 0, False
num_external_hit_tokens = self.client.lookup(
request.request_id,
token_len,
request.num_tokens,
request.block_hashes,
non_block=self.lookup_async,
)
@@ -95,14 +93,6 @@ class MooncakeStoreScheduler:
# Lookup not ready yet; scheduler will retry on a later step.
return None, False
if num_external_hit_tokens == request.num_tokens:
# Leave a sub-block tail uncomputed for sampling, on a block
# boundary so the recv-side load mask covers every yielded chunk.
num_external_hit_tokens = max(
0,
(request.num_tokens - 1) // self._block_size * self._block_size,
)
if num_external_hit_tokens < num_computed_tokens:
need_to_allocate = 0
else:
@@ -634,6 +634,21 @@ class KVCacheStoreSendingThread(KVTransferThread):
addrs: list[list[int]] = []
sizes: list[list[int]] = []
stored_events: list[BlockStored] = []
chunks_per_group: list[list[tuple[int, int]]] = [
[] for _ in self.token_databases
]
for start, end, g_idx in zip(starts, ends, group_indices, strict=True):
chunks_per_group[g_idx].append((start, end))
for g_idx, chunks in enumerate(chunks_per_group):
if not chunks:
continue
db = self.token_databases[g_idx]
group_addrs, group_sizes, _ = db.prepare_values(
chunks, block_ids_per_group[g_idx]
)
addrs.extend(group_addrs)
sizes.extend(group_sizes)
# parent_block_hash chains live within a group, not across.
if self.enable_kv_event:
prev_key_per_group: dict[int, Any] = {}
@@ -645,10 +660,6 @@ class KVCacheStoreSendingThread(KVTransferThread):
zip(starts, ends, group_indices, strict=True)
):
db = self.token_databases[g_idx]
addr, size, _ = db.prepare_value(s, e, block_ids_per_group[g_idx])
addrs.append(addr)
sizes.append(size)
if self.enable_kv_event:
token_ids = (
req_meta.token_ids[s:e]
@@ -805,19 +816,21 @@ class KVCacheStoreRecvingThread(KVTransferThread):
block_id_list: list[int] = []
for g_idx, db in enumerate(self.token_databases):
mask = load_mask_per_group[g_idx]
chunks: list[tuple[int, int]] = []
for start, end, block_hash in db.process_tokens(
token_len, req_meta.block_hashes, mask_num
):
chunk_idx = start // db.block_size
if chunk_idx >= len(mask) or not mask[chunk_idx]:
continue
addr, size, block_id = db.prepare_value(
start, end, req_meta.block_ids[g_idx]
)
key_list.append(db.key_for(block_hash))
addr_list.append(addr)
size_list.append(size)
block_id_list.append(block_id)
chunks.append((start, end))
g_addrs, g_sizes, g_block_ids = db.prepare_values(
chunks, req_meta.block_ids[g_idx]
)
addr_list.extend(g_addrs)
size_list.extend(g_sizes)
block_id_list.extend(g_block_ids)
# Rotate aligned lists by tp_rank for load balancing.
rotation = self.tp_rank % len(key_list)
@@ -1454,11 +1467,14 @@ class MooncakeStoreWorker:
return finished_sending
def lookup(self, token_len: int, block_hashes: Sequence[BlockHash]) -> int:
def lookup(self, num_tokens: int, block_hashes: Sequence[BlockHash]) -> int:
"""Check how many prefix tokens exist in the store.
Checks across all rank-specific key namespaces that may be loaded.
Checks across all rank-specific key namespaces that may be loaded. A
hit covering all ``num_tokens`` is re-derived below the request end so
the last token is recomputed for sampling.
"""
token_len = self.coord.align_lookup_length(num_tokens)
if not block_hashes or token_len <= 0:
return 0
@@ -1522,11 +1538,24 @@ class MooncakeStoreWorker:
)
}
cached_block_pool = ExternalCachedBlockPool(
self.hash_block_size,
exists_set,
)
_masks, hit_length = self.coord.find_longest_cache_hit(
block_hashes,
token_len,
ExternalCachedBlockPool(self.hash_block_size, exists_set),
cached_block_pool,
)
if hit_length >= num_tokens:
usable_length = self.coord.align_lookup_length(num_tokens - 1)
if usable_length <= 0:
return 0
_masks, hit_length = self.coord.find_longest_cache_hit(
block_hashes,
usable_length,
cached_block_pool,
)
return hit_length
def get_kv_events(self) -> list[BlockStored]:
@@ -1592,11 +1621,11 @@ class LookupKeyServer:
msg_type = bytes(all_frames[0])
if msg_type == LOOKUP_MSG:
token_len = int.from_bytes(all_frames[1], byteorder="big")
num_tokens = int.from_bytes(all_frames[1], byteorder="big")
hash_len = int.from_bytes(all_frames[2], byteorder="big")
blob = all_frames[3].buffer
block_hashes = BlobBlockHashes(blob, hash_len)
result = self.store_worker.lookup(token_len, block_hashes)
result = self.store_worker.lookup(num_tokens, block_hashes)
self.socket.send(result.to_bytes(4, "big"))
elif msg_type == RESET_MSG:
@@ -1659,11 +1688,11 @@ class LookupKeyClient:
)
self.futures: dict[str, Future[int]] = {}
def _lookup(self, token_len: int, block_hashes: list[BlockHash]) -> int:
def _lookup(self, num_tokens: int, block_hashes: list[BlockHash]) -> int:
hash_len = len(block_hashes[0]) if block_hashes else 0
all_frames = (
LOOKUP_MSG,
token_len.to_bytes(4, byteorder="big"),
num_tokens.to_bytes(4, byteorder="big"),
hash_len.to_bytes(2, byteorder="big"),
b"".join(block_hashes),
)
@@ -1674,7 +1703,7 @@ class LookupKeyClient:
def lookup(
self,
req_id: str,
token_len: int,
num_tokens: int,
block_hashes: list[BlockHash],
non_block: bool = False,
) -> int | None:
@@ -1682,7 +1711,7 @@ class LookupKeyClient:
so the caller retries on a later step."""
future = self.futures.get(req_id)
if future is None:
future = self.executor.submit(self._lookup, token_len, list(block_hashes))
future = self.executor.submit(self._lookup, num_tokens, list(block_hashes))
self.futures[req_id] = future
if non_block and not future.done():
return None
+42 -24
View File
@@ -1162,22 +1162,31 @@ class AsyncMultiModalContentParser(BaseMultiModalContentParser):
parameter="image_embeds",
)
if isinstance(image_embeds, dict):
embeds = {
k: self._connector.fetch_image_embedding(v)
for k, v in image_embeds.items()
}
elif isinstance(image_embeds, str):
embedding = self._connector.fetch_image_embedding(image_embeds)
embeds = embedding
else:
embeds = None
placeholder = self._tracker.add(
"image_embeds", partial(self._item_with_uuid_async, embeds, uuid)
"image_embeds",
partial(self._image_embeds_with_uuid_async, image_embeds, uuid),
)
self._add_placeholder("image", placeholder)
async def _image_embeds_with_uuid_async(
self,
image_embeds: str | dict[str, str] | None,
uuid: str | None,
):
if isinstance(image_embeds, dict):
tensors = await asyncio.gather(
*(
self._connector.fetch_image_embedding_async(v)
for v in image_embeds.values()
)
)
embeds = dict(zip(image_embeds, tensors))
elif isinstance(image_embeds, str):
embeds = await self._connector.fetch_image_embedding_async(image_embeds)
else:
embeds = None
return embeds, uuid
def parse_audio_embeds(
self,
audio_embeds: str | dict[str, str] | None,
@@ -1190,22 +1199,31 @@ class AsyncMultiModalContentParser(BaseMultiModalContentParser):
parameter="audio_embeds",
)
if isinstance(audio_embeds, dict):
embeds = {
k: self._connector.fetch_audio_embedding(v)
for k, v in audio_embeds.items()
}
elif isinstance(audio_embeds, str):
embedding = self._connector.fetch_audio_embedding(audio_embeds)
embeds = embedding
else:
embeds = None
placeholder = self._tracker.add(
"audio_embeds", partial(self._item_with_uuid_async, embeds, uuid)
"audio_embeds",
partial(self._audio_embeds_with_uuid_async, audio_embeds, uuid),
)
self._add_placeholder("audio", placeholder)
async def _audio_embeds_with_uuid_async(
self,
audio_embeds: str | dict[str, str] | None,
uuid: str | None,
):
if isinstance(audio_embeds, dict):
tensors = await asyncio.gather(
*(
self._connector.fetch_audio_embedding_async(v)
for v in audio_embeds.values()
)
)
embeds = dict(zip(audio_embeds, tensors))
elif isinstance(audio_embeds, str):
embeds = await self._connector.fetch_audio_embedding_async(audio_embeds)
else:
embeds = None
return embeds, uuid
def parse_image_pil(
self,
image_pil: Image.Image | None,
+12 -2
View File
@@ -103,6 +103,16 @@ class Qwen3_5MultiTokenPredictor(nn.Module):
prefix=f"{prefix}.fc",
)
# GPTQ: quantized checkpoints may exclude MTP from quantization via
# quantization_config.dynamic with "-:pattern" entries. When detected,
# disable quantization for MTP layers so they use unquantized params.
original_quant = vllm_config.quant_config
if quant_config and quant_config.get_name() not in ("modelopt_fp4",):
hf_qc = getattr(model_config.hf_config, "quantization_config", None)
if isinstance(hf_qc, dict):
dynamic = hf_qc.get("dynamic", {})
if any(k.startswith("-:") and "mtp" in k for k in dynamic):
vllm_config.quant_config = None
self.layers = torch.nn.ModuleList(
Qwen3_5DecoderLayer(
vllm_config,
@@ -111,11 +121,10 @@ class Qwen3_5MultiTokenPredictor(nn.Module):
)
for idx in range(self.num_mtp_layers)
)
vllm_config.quant_config = original_quant
self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
["hidden_states", "residual"], config.hidden_size
)
self.norm = Qwen3_5RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.pre_fc_norm_hidden = Qwen3_5RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
@@ -170,6 +179,7 @@ class Qwen3_5MultiTokenPredictor(nn.Module):
positions.shape[-1],
self.config.hidden_size,
)
hidden_states, _ = self.norm(hidden_states, residual)
return hidden_states
+43
View File
@@ -47,6 +47,7 @@ from vllm.model_executor.layers.quantization import QuantizationConfig
from vllm.model_executor.models.utils import extract_layer_index
from vllm.models.deepseek_v4.common.rope import build_deepseek_v4_rope
from vllm.models.deepseek_v4.compressor import DeepseekCompressor
from vllm.triton_utils import tl, triton
from vllm.utils.multi_stream_utils import (
execute_in_parallel,
maybe_execute_in_parallel,
@@ -66,6 +67,25 @@ from vllm.v1.kv_cache_interface import (
logger = init_logger(__name__)
@triton.jit
def _fill_short_context_topk_indices(
output,
positions,
TOP_K: tl.constexpr,
COMPRESS_RATIO: tl.constexpr,
PADDED_TOP_K: tl.constexpr,
):
# small triton kernel that selects every candidate, -1 otherwise
row = tl.program_id(0)
offsets = tl.arange(0, PADDED_TOP_K)
num_compressed = (tl.load(positions + row) + 1) // COMPRESS_RATIO
tl.store(
output + row * TOP_K + offsets,
tl.where(offsets < num_compressed, offsets, -1),
mask=offsets < TOP_K,
)
def _resolve_dsv4_kv_cache_dtype(
use_fp8_ds_mla_layout: bool,
kv_cache_dtype: str,
@@ -787,6 +807,29 @@ class DeepseekV4Indexer(nn.Module):
) -> torch.Tensor:
compressor = self.compressor
attn_metadata = get_forward_context().attn_metadata
if isinstance(attn_metadata, dict):
indexer_metadata = cast(Any, attn_metadata[self.k_cache.prefix])
if indexer_metadata.max_seq_len // self.compress_ratio <= self.topk_tokens:
# candidates num smaller than topk, every candidate is selected
# but we still need to build k cache
compressor(compressed_kv_score, positions, rotary_emb)
assert self.topk_indices_buffer is not None
num_tokens = (
indexer_metadata.num_decode_tokens
+ indexer_metadata.num_prefill_tokens
)
if num_tokens > 0:
_fill_short_context_topk_indices[(num_tokens,)](
self.topk_indices_buffer,
positions,
TOP_K=self.topk_tokens,
COMPRESS_RATIO=self.compress_ratio,
PADDED_TOP_K=triton.next_power_of_2(self.topk_tokens),
num_warps=8,
)
return self.topk_indices_buffer
def wq_b_and_q_quant():
# ReplicatedLinear returns (output, bias); bias is None.
q, _ = self.wq_b(qr)
+28
View File
@@ -593,6 +593,20 @@ class MediaConnector:
return image_embedding_io.load_base64("", data)
async def fetch_image_embedding_async(
self,
data: str,
) -> torch.Tensor:
"""
Asynchronously load image embedding from a URL.
"""
image_embedding_io = ImageEmbeddingMediaIO()
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
global_thread_pool, image_embedding_io.load_base64, "", data
)
def fetch_audio_embedding(
self,
data: str,
@@ -603,3 +617,17 @@ class MediaConnector:
audio_embedding_io = AudioEmbeddingMediaIO()
return audio_embedding_io.load_base64("", data)
async def fetch_audio_embedding_async(
self,
data: str,
) -> torch.Tensor:
"""
Asynchronously load audio embedding from a URL.
"""
audio_embedding_io = AudioEmbeddingMediaIO()
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
global_thread_pool, audio_embedding_io.load_base64, "", data
)
+3 -3
View File
@@ -145,7 +145,7 @@ class SchedulerInterface(ABC):
self,
request_ids: str | Iterable[str] | None,
finished_status: "RequestStatus",
) -> list[tuple[str, int]]:
) -> "list[Request]":
"""Finish the requests in the scheduler's internal queue. If the request
is not in the queue, this method will do nothing for that request.
@@ -159,8 +159,8 @@ class SchedulerInterface(ABC):
finished_status: The finished status of the given requests.
Returns:
Tuple of (req_id, client_index) for requests that were aborted. Will not
include any that were already finished.
List of requests that were aborted. Will not include any that were
already finished.
"""
raise NotImplementedError
+27 -13
View File
@@ -60,7 +60,7 @@ from vllm.v1.outputs import DraftTokenIds, KVConnectorOutput, ModelRunnerOutput
from vllm.v1.request import Request, RequestStatus, StreamingUpdate
from vllm.v1.spec_decode.dynamic.utils import build_dynamic_sd_schedule_lookup
from vllm.v1.spec_decode.metrics import SpecDecodingStats
from vllm.v1.structured_output import StructuredOutputManager
from vllm.v1.structured_output import StructuredOutputGrammar, StructuredOutputManager
from vllm.v1.utils import record_function_or_nullcontext
logger = init_logger(__name__)
@@ -201,6 +201,10 @@ class Scheduler(SchedulerInterface):
self.finished_recving_kv_req_ids: set[str] = set()
self.failed_recving_kv_req_ids: set[str] = set()
# Grammar compilation failures to finish as per-request errors in
# update_from_output.
self.grammar_compile_error_reqs: set[str] = set()
# Encoder-related.
# Calculate encoder cache size if applicable
supports_mm_inputs = mm_registry.supports_multimodal_inputs(
@@ -1712,11 +1716,13 @@ class Scheduler(SchedulerInterface):
request.status = RequestStatus.FINISHED_STOPPED
stopped = True
if new_token_ids and self.structured_output_manager.should_advance(request):
if new_token_ids and self.structured_output_manager.should_advance(
request, new_token_ids=new_token_ids
):
struct_output_request = request.structured_output_request
assert struct_output_request is not None
grammar = struct_output_request.grammar
assert grammar is not None
assert isinstance(grammar, StructuredOutputGrammar)
# new_token_ids can be a mixed block of reasoning content, then
# the reasoning end marker, then the start of the grammar content.
# Trim the reasoning content so the grammar only sees grammar content.
@@ -1846,10 +1852,16 @@ class Scheduler(SchedulerInterface):
# This is a rare case and unlikely to impact performance.
self.waiting.remove_requests(stopped_preempted_reqs)
error_req_ids = set(self.grammar_compile_error_reqs)
self.grammar_compile_error_reqs.clear()
if failed_kv_load_req_ids and not self.recompute_kv_load_failures:
requests = [self.requests[req_id] for req_id in failed_kv_load_req_ids]
self.finish_requests(failed_kv_load_req_ids, RequestStatus.FINISHED_ERROR)
for request in requests:
error_req_ids.update(failed_kv_load_req_ids)
if error_req_ids:
error_reqs = self.finish_requests(
error_req_ids, RequestStatus.FINISHED_ERROR
)
for request in error_reqs:
outputs[request.client_index].append(
EngineCoreOutput(
request_id=request.request_id,
@@ -2079,8 +2091,7 @@ class Scheduler(SchedulerInterface):
# Filter out spec tokens which do not adhere to the grammar.
if self.structured_output_manager.should_advance(request):
metadata = request.structured_output_request
assert metadata is not None and metadata.grammar is not None
spec_token_ids = metadata.grammar.validate_tokens(spec_token_ids)
spec_token_ids = metadata.grammar.validate_tokens(spec_token_ids) # type: ignore[union-attr]
# Pad to original number of spec tokens.
num_invalid_tokens = orig_num_spec_tokens - len(spec_token_ids)
if num_invalid_tokens:
@@ -2121,7 +2132,7 @@ class Scheduler(SchedulerInterface):
def finish_requests(
self, request_ids: str | Iterable[str] | None, finished_status: RequestStatus
) -> list[tuple[str, int]]:
) -> list[Request]:
"""Handles the finish signal from outside the scheduler.
For example, the API server can abort a request when the client
@@ -2130,8 +2141,8 @@ class Scheduler(SchedulerInterface):
If request_ids is None, all requests will be finished.
Returns:
Tuple of (req_id, client_index) for requests that were aborted. Will not
include any that were already finished.
List of requests that were aborted. Will not include any that were
already finished.
"""
assert RequestStatus.is_finished(finished_status)
if isinstance(request_ids, str):
@@ -2180,7 +2191,7 @@ class Scheduler(SchedulerInterface):
request.status = finished_status
self._free_request(request, delay_free_blocks=delay_free_blocks)
return [(r.request_id, r.client_index) for r in valid_requests]
return valid_requests
def _free_request(
self, request: Request, delay_free_blocks: bool = False
@@ -2580,7 +2591,10 @@ class Scheduler(SchedulerInterface):
if request.status == RequestStatus.WAITING_FOR_STRUCTURED_OUTPUT_GRAMMAR:
structured_output_req = request.structured_output_request
if not (structured_output_req and structured_output_req.grammar):
if not structured_output_req or structured_output_req.grammar is None:
return False
if isinstance(structured_output_req.grammar, Exception):
self.grammar_compile_error_reqs.add(request.request_id)
return False
request.status = RequestStatus.WAITING
return True
+3 -3
View File
@@ -1830,13 +1830,13 @@ class EngineCoreProc(EngineCore):
) -> None:
self._send_finish_outputs_to_client(req_ids, client_index, FinishReason.ERROR)
def _send_abort_outputs(self, aborted_reqs: list[tuple[str, int]]) -> None:
def _send_abort_outputs(self, aborted_reqs: list[Request]) -> None:
# TODO(nick) this will be moved inside the scheduler
if aborted_reqs:
# Map client_index to list of request_ids that belong to that client.
by_client = defaultdict[int, set[str]](set)
for req_id, client_index in aborted_reqs:
by_client[client_index].add(req_id)
for request in aborted_reqs:
by_client[request.client_index].add(request.request_id)
for client_index, req_ids in by_client.items():
self._send_abort_outputs_to_client(list(req_ids), client_index)
+28 -10
View File
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import sys
from abc import ABC, abstractmethod
import tokenizers
@@ -320,10 +321,19 @@ def check_stop_strings(
Where stop_string is the matched stop string and offset is the
length to which output_text should be truncated, or -1 for no
truncation.
When several stop strings match within the newly generated text (for
example when speculative decoding appends multiple tokens in a single
step), the stop string that completes earliest in the text is selected,
so the result matches appending one token at a time. Ties are broken by
stop-list order.
"""
if not new_char_count or not stop:
return None
best_stop_str: str | None = None
best_stop_index = 0
best_end = sys.maxsize
for stop_str in stop:
stop_string_len = len(stop_str)
# Avoid searching already-searched text.
@@ -331,14 +341,22 @@ def check_stop_strings(
if stop_index == -1:
continue
if include_in_output:
# Truncate to end of stop string.
stop_index += stop_string_len
if stop_index >= len(output_text):
# No truncation required.
return stop_str, -1
# Prefer the stop string that completes earliest in the text.
end = stop_index + stop_string_len
if end < best_end:
best_stop_str = stop_str
best_stop_index = stop_index
best_end = end
# Truncate the output text to either the beginning
# or end of the stop string.
return stop_str, stop_index
return None
if best_stop_str is None:
return None
if include_in_output:
# Truncate to end of stop string.
if best_end >= len(output_text):
# No truncation required.
return best_stop_str, -1
return best_stop_str, best_end
# Truncate the output text to the beginning of the stop string.
return best_stop_str, best_stop_index
+27
View File
@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from abc import ABC, abstractmethod
from collections.abc import Sequence
from copy import copy
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, NamedTuple, TypeAlias
@@ -90,6 +91,32 @@ class LogprobsTensors(NamedTuple):
self.selected_token_ranks[mask],
)
@staticmethod
def cat(
tensors: Sequence["LogprobsTensors"],
cu_num_generated_tokens: list[int] | None = None,
) -> "LogprobsTensors":
"""Concatenate flattened logprob tensors."""
assert tensors
assert cu_num_generated_tokens is not None or all(
tensor.cu_num_generated_tokens is None for tensor in tensors
)
if len(tensors) == 1:
tensor = tensors[0]
if cu_num_generated_tokens is None:
return tensor
return tensor._replace(cu_num_generated_tokens=cu_num_generated_tokens)
return LogprobsTensors(
logprob_token_ids=torch.cat(
[tensor.logprob_token_ids for tensor in tensors]
),
logprobs=torch.cat([tensor.logprobs for tensor in tensors]),
selected_token_ranks=torch.cat(
[tensor.selected_token_ranks for tensor in tensors]
),
cu_num_generated_tokens=cu_num_generated_tokens,
)
@staticmethod
def empty_cpu(
num_positions: int, num_tokens_per_position: int
+8 -12
View File
@@ -7,7 +7,7 @@ import torch.nn as nn
from vllm import envs
from vllm._aiter_ops import rocm_aiter_ops
from vllm.config.model import LogprobsMode
from vllm.config.model import PROCESSED_LOGPROBS_MODES, LogprobsMode
from vllm.logger import init_logger
from vllm.platforms import CpuArchEnum, current_platform
from vllm.triton_utils import HAS_TRITON
@@ -87,7 +87,7 @@ class TopKTopPSampler(nn.Module):
# FlashInfer doesn't expose post-top-k/top-p logits/logprobs,
# so it can't be used when the configured mode requires them.
can_use_flashinfer = (
logprobs_mode not in ("processed_logits", "processed_logprobs")
logprobs_mode not in PROCESSED_LOGPROBS_MODES
and flashinfer_sampler_supported()
)
self.forward = (
@@ -108,7 +108,7 @@ class TopKTopPSampler(nn.Module):
else:
self.forward = self.forward_native
elif (
logprobs_mode not in ("processed_logits", "processed_logprobs")
logprobs_mode not in PROCESSED_LOGPROBS_MODES
and rocm_aiter_ops.is_enabled()
):
self.aiter_ops = None
@@ -165,7 +165,7 @@ class TopKTopPSampler(nn.Module):
return self.forward_native(logits, generators, k, p)
if self.use_fp64_gumbel:
return self.forward_native(logits, generators, k, p)
assert self.logprobs_mode not in ("processed_logits", "processed_logprobs"), (
assert self.logprobs_mode not in PROCESSED_LOGPROBS_MODES, (
"FlashInfer does not support returning logits/logprobs"
)
# flashinfer sampling functions expect contiguous logits.
@@ -236,10 +236,9 @@ class TopKTopPSampler(nn.Module):
return self.forward_native(logits, generators, k, p)
if self.use_fp64_gumbel:
return self.forward_native(logits, generators, k, p)
assert self.logprobs_mode not in (
"processed_logits",
"processed_logprobs",
), "aiter sampler does not support returning logits/logprobs."
assert self.logprobs_mode not in PROCESSED_LOGPROBS_MODES, (
"aiter sampler does not support returning logits/logprobs."
)
if self.aiter_ops is None and not self._init_aiter_ops():
return self.forward_native(logits, generators, k, p)
return self.aiter_sample(logits, k, p, generators), None
@@ -300,10 +299,7 @@ class TopKTopPSampler(nn.Module):
logits.shape[0], dtype=torch.int64, device=logits.device
)
logits_to_return = None
if (
self.logprobs_mode == "processed_logits"
or self.logprobs_mode == "processed_logprobs"
):
if self.logprobs_mode in PROCESSED_LOGPROBS_MODES:
logits_to_return = torch.empty_like(logits)
assert len(generators) != logits.shape[0], (
+2 -4
View File
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING
import torch
import torch.nn as nn
from vllm.config.model import PROCESSED_LOGPROBS_MODES
from vllm.logger import init_logger
from vllm.triton_utils import tl, triton
from vllm.v1.outputs import LogprobsLists, LogprobsTensors, SamplerOutput
@@ -67,10 +68,7 @@ class RejectionSampler(nn.Module):
self.sampler = sampler
self.use_fp64_gumbel = getattr(sampler, "use_fp64_gumbel", False)
logprobs_mode = self.sampler.logprobs_mode
self.is_processed_logprobs_mode = logprobs_mode in (
"processed_logprobs",
"processed_logits",
)
self.is_processed_logprobs_mode = logprobs_mode in PROCESSED_LOGPROBS_MODES
self.is_logits_logprobs_mode = logprobs_mode in (
"raw_logits",
"processed_logits",
+57 -45
View File
@@ -15,7 +15,6 @@ from vllm.v1.structured_output.backend_guidance import GuidanceBackend
from vllm.v1.structured_output.backend_types import (
StructuredOutputBackend,
StructuredOutputGrammar,
StructuredOutputOptions,
)
from vllm.v1.structured_output.backend_xgrammar import XgrammarBackend
@@ -29,7 +28,6 @@ if TYPE_CHECKING:
else:
torch = LazyLoader("torch", globals(), "torch")
logger = init_logger(__name__)
@@ -164,24 +162,33 @@ class StructuredOutputManager:
else:
raise ValueError(f"Unsupported structured output backend: {backend}")
grammar: Future[StructuredOutputGrammar] | StructuredOutputGrammar
if self._use_async_grammar_compilation:
grammar = self.executor.submit(self._create_grammar, request)
else:
grammar = self._create_grammar(request) # type: ignore[assignment]
request.structured_output_request.grammar = grammar # type: ignore[assignment]
try:
grammar = self._create_grammar(request)
except Exception as e:
grammar = Future()
grammar.set_exception(e)
request.structured_output_request.grammar = grammar
def _create_grammar(self, request: "Request") -> StructuredOutputGrammar:
key = request.structured_output_request.structured_output_key # type: ignore[union-attr]
struct_request = request.structured_output_request
assert struct_request is not None
# Note that the request was validated in the engine core client,
# so at this point we know it is a supported type of request.
#
# TODO: we still need to handle xgrammar compilation failures,
# though it should be unlikely as we test that up front as well.
request_type, grammar_spec = key
assert self.backend is not None
return self.backend.compile_grammar(request_type, grammar_spec)
# so at this point we know it is a supported type of request. Grammar
# compilation may still fail; the Future carries that error to the
# scheduler so it can fail only this request.
try:
request_type, grammar_spec = struct_request.structured_output_key
assert self.backend is not None
return self.backend.compile_grammar(request_type, grammar_spec)
except Exception:
logger.exception(
"Failed to compile grammar for request %s", request.request_id
)
raise
def _fill_bitmasks(
self, batch: Iterable[tuple[StructuredOutputGrammar, int, bool]]
@@ -244,8 +251,9 @@ class StructuredOutputManager:
structured_output_request = request.structured_output_request
if TYPE_CHECKING:
assert structured_output_request is not None
assert structured_output_request.grammar is not None
grammar = structured_output_request.grammar
if TYPE_CHECKING:
assert isinstance(grammar, StructuredOutputGrammar)
apply_bitmask = self.should_fill_bitmask(request)
batch.append((grammar, cumulative_index, apply_bitmask))
@@ -268,8 +276,9 @@ class StructuredOutputManager:
if TYPE_CHECKING:
assert structured_output_request is not None
assert structured_output_request.grammar is not None
grammar = structured_output_request.grammar
if TYPE_CHECKING:
assert isinstance(grammar, StructuredOutputGrammar)
apply_bitmask = self.should_fill_bitmask(request)
reasoner = self._get_reasoner(request)
@@ -368,7 +377,11 @@ class StructuredOutputManager:
return request.structured_output_request.reasoning_ended
return True
def should_advance(self, request: "Request") -> bool:
def should_advance(
self,
request: "Request",
new_token_ids: list[int] | None = None,
) -> bool:
if not request.use_structured_output:
return False
@@ -391,37 +404,36 @@ class StructuredOutputManager:
if structured_req.reasoning_ended:
return True
# Check if reasoning ends in *this* step
delta_from = request.num_computed_tokens - request.num_output_placeholders
# Check if reasoning ends in *this* step.
# When the caller passes new_token_ids (the tokens that were just
# appended this step), use it directly as the delta window. The
# placeholder-derived fallback assumes num_output_placeholders ==
# len(new_token_ids), which breaks under async scheduling + spec
# decode when some drafts are rejected (#43388): the placeholder
# count remains > 0 after the step and the computed delta window
# starts past the reasoning-end marker.
all_token_ids = request.all_token_ids
start = (
delta_from if delta_from >= 0 else max(len(all_token_ids) + delta_from, 0)
)
if reasoner.is_reasoning_end_streaming(
all_token_ids, itertools.islice(all_token_ids, start, None)
):
if new_token_ids:
# The tokens were already appended this step, so the step window
# starts exactly len(new_token_ids) from the end.
start = len(all_token_ids) - len(new_token_ids)
delta_ids: Iterable[int] = new_token_ids
else:
delta_from = request.num_computed_tokens - request.num_output_placeholders
start = (
delta_from
if delta_from >= 0
else max(len(all_token_ids) + delta_from, 0)
)
delta_ids = itertools.islice(all_token_ids, start, None)
if reasoner.is_reasoning_end_streaming(all_token_ids, delta_ids):
structured_req.reasoning_ended = True
# Reasoning just ended this step. Defer FSM advance until the next
# pass (see reasoning_ended check above) for JSON/regex/choice/grammar:
# advancing on the closing boundary token can accept tokens that still
# belong to the reasoning stream. Structural tags are the only safe
# same-step exception: they model phased output (e.g. thinking tag ->
# answer tag), and speculative decoding must run grammar.validate_tokens
# on draft tokens produced immediately after that transition.
if (
self.vllm_config.speculative_config is not None
and structured_req.structured_output_key[0]
== StructuredOutputOptions.STRUCTURAL_TAG
):
# The scheduler will advance the grammar with this step's
# tokens right away, but the step still contains reasoning
# content up to and including the end marker. Record where
# it ends so trim_reasoning_for_advance() can drop it.
structured_req.reasoning_end_token_index = (
self._find_reasoning_end_index(reasoner, all_token_ids, start)
)
return True
# Record the boundary so the scheduler can exclude reasoning tokens.
end_index = self._find_reasoning_end_index(reasoner, all_token_ids, start)
structured_req.reasoning_end_token_index = end_index
return True
return False
+9 -6
View File
@@ -21,7 +21,9 @@ if TYPE_CHECKING:
@dataclasses.dataclass
class StructuredOutputRequest:
params: StructuredOutputsParams
_grammar: Future[StructuredOutputGrammar] | StructuredOutputGrammar | None = None
_grammar: (
Future[StructuredOutputGrammar] | StructuredOutputGrammar | Exception | None
) = None
reasoning_ended: bool | None = None
# Absolute index into the request's all_token_ids of the last reasoning
# token (the reasoning-end marker). Tokens at or before this index are
@@ -52,6 +54,8 @@ class StructuredOutputRequest:
self._grammar = self._grammar.result(timeout=0.0001)
except TimeoutError:
return False
except Exception as e:
self._grammar = e
return True
@property
@@ -59,11 +63,10 @@ class StructuredOutputRequest:
return self._check_grammar_completion()
@property
def grammar(self) -> StructuredOutputGrammar | None:
completed = self._check_grammar_completion()
return (
cast(StructuredOutputGrammar | None, self._grammar) if completed else None
)
def grammar(self) -> StructuredOutputGrammar | Exception | None:
if not self._check_grammar_completion():
return None
return cast(StructuredOutputGrammar | Exception | None, self._grammar)
@grammar.setter
def grammar(
+17 -34
View File
@@ -498,7 +498,10 @@ class ModelCudaGraphManager(CudaGraphManager):
block_tables,
attn_groups,
kv_cache_config,
full_cudagraph=desc.cg_mode == CUDAGraphMode.FULL,
skip_attn=(
desc.cg_mode == CUDAGraphMode.PIECEWISE
and not self.use_breakable_cg
),
)
# Capture with dummy rows marked as padding.
@@ -507,6 +510,7 @@ class ModelCudaGraphManager(CudaGraphManager):
def forward_fn(cg_mode: CUDAGraphMode) -> None:
batch_descriptor = None
if cg_mode == CUDAGraphMode.PIECEWISE:
assert (attn_metadata is not None) == self.use_breakable_cg
batch_descriptor = BatchDescriptor(
num_tokens=num_tokens,
has_lora=has_lora,
@@ -589,7 +593,7 @@ def prepare_inputs_to_capture(
block_tables: BlockTables,
attn_groups: list[list[AttentionGroup]],
kv_cache_config: KVCacheConfig,
full_cudagraph: bool,
skip_attn: bool = False,
) -> AttentionState:
input_batch = InputBatch.make_dummy(num_reqs, num_tokens, input_buffers)
input_block_tables = block_tables.get_dummy_block_tables(num_reqs)
@@ -610,36 +614,15 @@ def prepare_inputs_to_capture(
)
input_batch.dcp_local_seq_lens = input_buffers.dcp_local_seq_lens[:num_reqs]
# NOTE(woosuk): Attention metadata is required not just by standard attention
# kernels, but also by specialized attention-like operations (e.g., Inkling's sconv,
# DSV4 compressor), which maintain their own states and require special metadata
# such as block tables.
# During CUDA graph capture:
# - For FULL CUDA graphs: We set for_capture=True so that both attention and
# attention-like ops produce capturable metadata compatible with CUDA graphs.
# - For PIECEWISE CUDA graphs: We still build attention metadata, but set
# for_capture=False. This is because:
# * Attention-like ops (such as sconv or DSV4 compressor) may not be used as
# breakpoints in PIECEWISE CUDA graphs, so we must generate their attention
# metadata so they can execute and be captured during graph capture.
# * Standard attention ops that are treated as breakpoints will be executed
# eagerly at capture time (not included in the graph itself), and for these,
# setting for_capture=False is essential. Some attention backends
# (like linear attention) cannot generate capturable metadata for prefill,
# so for_capture=False ensures they execute without issue.
# * We assume that attention-like operations intended for capture will still
# produce capturable metadata, even when for_capture=False. While this
# assumption is brittle, it currently works in practice.
# In summary: We always generate attention metadata for both FULL and PIECEWISE
# CUDA graphs, setting for_capture=True for FULL graphs, and for_capture=False
# for PIECEWISE graphs, to ensure correct execution and capture.
attn_metadata = model_state.prepare_attn(
input_batch,
CUDAGraphMode.NONE,
input_block_tables,
slot_mappings,
attn_groups,
kv_cache_config,
for_capture=full_cudagraph,
)
attn_metadata = None
if not skip_attn:
attn_metadata = model_state.prepare_attn(
input_batch,
CUDAGraphMode.NONE,
input_block_tables,
slot_mappings,
attn_groups,
kv_cache_config,
for_capture=True,
)
return AttentionState(attn_metadata, slot_mappings_by_layer)
+1 -3
View File
@@ -51,7 +51,7 @@ from vllm.sequence import IntermediateTensors
from vllm.tasks import SupportedTask
from vllm.utils.math_utils import cdiv
from vllm.utils.mem_utils import DeviceMemoryProfiler, format_gib
from vllm.utils.torch_utils import PIN_MEMORY, STR_DTYPE_TO_TORCH_DTYPE
from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE
from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
from vllm.v1.kv_cache_interface import KVCacheConfig, MambaSpec
from vllm.v1.outputs import DraftTokenIds, ModelRunnerOutput
@@ -521,12 +521,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
"""Build KV-block zeroing metadata; invoked from gpu_worker."""
self.kv_block_zeroer = KVBlockZeroer(
self.device,
pin_memory=PIN_MEMORY,
attn_groups_iter=(g for groups in self.attn_groups for g in groups),
kernel_block_sizes=self.kernel_block_sizes,
cache_dtype=self.cache_config.cache_dtype,
static_forward_context=self.compilation_config.static_forward_context,
max_concurrency=self.vllm_config.max_concurrent_batches,
)
@torch.inference_mode()
+1 -9
View File
@@ -132,15 +132,7 @@ class PromptLogprobsWorker:
if prompt_logprobs_list:
# Merge the in-progress logprobs.
logprobs = LogprobsTensors(
logprob_token_ids=torch.cat(
[x.logprob_token_ids for x in prompt_logprobs_list]
),
logprobs=torch.cat([x.logprobs for x in prompt_logprobs_list]),
selected_token_ranks=torch.cat(
[x.selected_token_ranks for x in prompt_logprobs_list]
),
)
logprobs = LogprobsTensors.cat(prompt_logprobs_list)
prompt_logprobs_list.clear()
if logprobs is None:
+3 -6
View File
@@ -5,7 +5,7 @@ import numpy as np
import torch
import vllm.envs as envs
from vllm.config.model import LogprobsMode
from vllm.config.model import PROCESSED_LOGPROBS_MODES, LogprobsMode
from vllm.sampling_params import SamplingParams
from vllm.v1.sample.ops.topk_topp_sampler import (
apply_top_k_top_p,
@@ -100,7 +100,7 @@ class Sampler:
)
if return_logprobs:
if self.logprobs_mode in ("processed_logprobs", "processed_logits"):
if self.logprobs_mode in PROCESSED_LOGPROBS_MODES:
logits = processed_logits
expanded_logits = logits.shape[0] != idx_mapping_np.shape[0]
cu_num_logits = cu_num_logits_np.tolist() if expanded_logits else None
@@ -221,10 +221,7 @@ class Sampler:
# any greedy requests or per-request seeds, or if post-processed
# logprobs need to be returned for any requests.
(top_k is None and top_p is None)
or (
return_logprobs
and self.logprobs_mode in ("processed_logprobs", "processed_logits")
)
or (return_logprobs and self.logprobs_mode in PROCESSED_LOGPROBS_MODES)
or self.sampling_states.any_greedy(idx_mapping_np)
or self.sampling_states.any_explicit_seed(idx_mapping_np)
)
@@ -56,7 +56,10 @@ class SpeculatorCudaGraphManager(CudaGraphManager):
block_tables,
attn_groups,
kv_cache_config,
full_cudagraph=desc.cg_mode == CUDAGraphMode.FULL,
skip_attn=(
desc.cg_mode == CUDAGraphMode.PIECEWISE
and not self.use_breakable_cg
),
)
return lambda cg_mode: forward_fn(
@@ -1,8 +1,12 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections.abc import Iterator
import numpy as np
import torch
from vllm.config import SpeculativeConfig
from vllm.config.model import PROCESSED_LOGPROBS_MODES
from vllm.triton_utils import tl, triton
from vllm.v1.outputs import LogprobsTensors
from vllm.v1.spec_decode.utils import unconditional_to_conditional_rates
@@ -19,6 +23,29 @@ from vllm.v1.worker.gpu.spec_decode.rejection_sampler_utils import (
rejection_sample,
)
# Cap on the FP32 target-logits buffer materialized by apply_sampling_params.
# TODO(mgoin): Chunking is a workaround. The rejection kernels already upcast
# per vocab block on load and apply ops like temperature and gumbel, so folding
# sampling-param application into those kernels would remove this buffer and
# its traffic entirely.
MAX_CHUNK_BYTES = 2**30 # 1GB
_FP32_BYTES = 4
def _iter_request_chunks(
cu_num_logits: np.ndarray, max_chunk_logits: int
) -> Iterator[tuple[int, int]]:
"""Yield maximally packed request ranges without splitting requests."""
assert max_chunk_logits > 0
num_reqs = cu_num_logits.size - 1
start = 0
while start < num_reqs:
max_logit = int(cu_num_logits[start]) + max_chunk_logits
end = int(np.searchsorted(cu_num_logits, max_logit, side="right") - 1)
end = min(num_reqs, max(start + 1, end))
yield start, end
start = end
@triton.jit
def _flatten_sampled_kernel(
@@ -66,18 +93,17 @@ class RejectionSampler:
def _get_logprobs_tensors(
self,
input_batch: InputBatch,
sampled: torch.Tensor,
num_sampled: torch.Tensor,
logits: torch.Tensor,
cu_num_logits: torch.Tensor,
cu_num_logits_np: np.ndarray,
max_num_logprobs: int,
) -> LogprobsTensors | None:
max_num_logprobs = self.sampler.sampling_states.max_num_logprobs(
input_batch.idx_mapping_np
)
if max_num_logprobs == NO_LOGPROBS:
return None
num_reqs = input_batch.cu_num_logits.shape[0] - 1
num_reqs = cu_num_logits.shape[0] - 1
num_logits = logits.shape[0]
flat_sampled = torch.zeros(
num_logits, dtype=sampled.dtype, device=sampled.device
@@ -87,19 +113,122 @@ class RejectionSampler:
sampled,
sampled.stride(0),
num_sampled,
input_batch.cu_num_logits,
cu_num_logits,
num_warps=1,
)
expanded_logits = num_logits != input_batch.idx_mapping.shape[0]
expanded_logits = num_logits != num_reqs
return compute_topk_scores(
logits,
max_num_logprobs,
flat_sampled,
input_batch.cu_num_logits_np.tolist() if expanded_logits else None,
cu_num_logits_np.tolist() if expanded_logits else None,
logits_mode=self.sampler.logprobs_mode
in ("raw_logits", "processed_logits"),
)
def _verify(
self,
logits: torch.Tensor,
draft_logits: torch.Tensor | None,
draft_sampled: torch.Tensor,
pos: torch.Tensor,
cu_num_logits: torch.Tensor,
idx_mapping: torch.Tensor,
idx_mapping_np: np.ndarray,
expanded_idx_mapping: torch.Tensor,
expanded_local_pos: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
processed_logits = self.sampler.apply_sampling_params(
logits,
expanded_idx_mapping,
idx_mapping_np,
pos,
draft_sampled,
expanded_local_pos,
)
sampled, num_sampled = rejection_sample(
processed_logits,
draft_logits,
draft_sampled,
cu_num_logits,
pos,
idx_mapping,
expanded_idx_mapping,
expanded_local_pos,
self.sampler.sampling_states.temperature.gpu,
self.sampler.sampling_states.seeds.gpu,
self.num_speculative_steps,
self.synthetic_conditional_rates,
use_fp64=self.sampler.use_fp64_gumbel,
use_block_verification=self.use_block_verification,
)
return processed_logits, sampled, num_sampled
def _verify_in_chunks(
self,
logits: torch.Tensor,
input_batch: InputBatch,
draft_logits: torch.Tensor | None,
draft_sampled: torch.Tensor,
pos: torch.Tensor,
max_chunk_logits: int,
max_num_logprobs: int,
) -> tuple[torch.Tensor, torch.Tensor, LogprobsTensors | None]:
cu_num_logits_np = input_batch.cu_num_logits_np
use_processed_logits = self.sampler.logprobs_mode in PROCESSED_LOGPROBS_MODES
sampled_chunks: list[torch.Tensor] = []
num_sampled_chunks: list[torch.Tensor] = []
logprobs_chunks: list[LogprobsTensors] = []
for start, end in _iter_request_chunks(cu_num_logits_np, max_chunk_logits):
lo = int(cu_num_logits_np[start])
hi = int(cu_num_logits_np[end])
chunk_cu_num_logits_np = cu_num_logits_np[start : end + 1] - lo
chunk_cu_num_logits = input_batch.cu_num_logits[start : end + 1] - lo
# draft_logits uses persistent request-state indices and stays global.
processed_logits, sampled, num_sampled = self._verify(
logits[lo:hi],
draft_logits,
draft_sampled[lo:hi],
pos[lo:hi],
chunk_cu_num_logits,
input_batch.idx_mapping[start:end],
input_batch.idx_mapping_np[start:end],
input_batch.expanded_idx_mapping[lo:hi],
input_batch.expanded_local_pos[lo:hi],
)
chunk_logprobs = self._get_logprobs_tensors(
sampled,
num_sampled,
processed_logits if use_processed_logits else logits[lo:hi],
chunk_cu_num_logits,
chunk_cu_num_logits_np,
max_num_logprobs,
)
if chunk_logprobs is not None:
logprobs_chunks.append(chunk_logprobs)
del processed_logits
sampled_chunks.append(sampled)
num_sampled_chunks.append(num_sampled)
if len(sampled_chunks) == 1:
logprobs_tensors = logprobs_chunks[0] if logprobs_chunks else None
return sampled_chunks[0], num_sampled_chunks[0], logprobs_tensors
logprobs_tensors = None
if logprobs_chunks:
expanded_logits = logits.shape[0] != input_batch.num_reqs
logprobs_tensors = LogprobsTensors.cat(
logprobs_chunks,
cu_num_generated_tokens=(
cu_num_logits_np.tolist() if expanded_logits else None
),
)
sampled = torch.cat(sampled_chunks)
num_sampled = torch.cat(num_sampled_chunks)
return sampled, num_sampled, logprobs_tensors
def __call__(
self,
logits: torch.Tensor,
@@ -112,37 +241,19 @@ class RejectionSampler:
draft_sampled = input_batch.input_ids[input_batch.logits_indices]
pos = input_batch.positions[input_batch.logits_indices]
processed_logits = self.sampler.apply_sampling_params(
logits,
input_batch.expanded_idx_mapping,
input_batch.idx_mapping_np,
pos,
draft_sampled,
input_batch.expanded_local_pos,
max_num_logprobs = self.sampler.sampling_states.max_num_logprobs(
input_batch.idx_mapping_np
)
sampled, num_sampled = rejection_sample(
processed_logits,
max_chunk_logits = max(1, MAX_CHUNK_BYTES // (logits.shape[1] * _FP32_BYTES))
sampled, num_sampled, logprobs_tensors = self._verify_in_chunks(
logits,
input_batch,
draft_logits,
draft_sampled,
input_batch.cu_num_logits,
pos,
input_batch.idx_mapping,
input_batch.expanded_idx_mapping,
input_batch.expanded_local_pos,
self.sampler.sampling_states.temperature.gpu,
self.sampler.sampling_states.seeds.gpu,
self.num_speculative_steps,
self.synthetic_conditional_rates,
use_fp64=self.sampler.use_fp64_gumbel,
use_block_verification=self.use_block_verification,
)
logprobs_tensors = self._get_logprobs_tensors(
input_batch,
sampled,
num_sampled,
processed_logits
if self.sampler.logprobs_mode in ("processed_logprobs", "processed_logits")
else logits,
max_chunk_logits,
max_num_logprobs,
)
num_sampled, num_rejected = get_num_sampled_and_rejected(
@@ -888,6 +888,10 @@ def rejection_sample(
use_fp64: bool = False,
use_block_verification: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]:
assert target_logits.ndim == 2 and target_logits.stride(-1) == 1
assert draft_logits is None or (
draft_logits.ndim == 3 and draft_logits.stride(-1) == 1
)
num_reqs = cu_num_logits.shape[0] - 1
num_logits, vocab_size = target_logits.shape
draft_logits_stride_0 = 0
+2 -6
View File
@@ -37,6 +37,7 @@ from vllm.config import (
update_config,
)
from vllm.config.cache import CacheConfig
from vllm.config.model import PROCESSED_LOGPROBS_MODES
from vllm.distributed.ec_transfer import get_ec_transfer, has_ec_transfer
from vllm.distributed.eplb.eplb_state import EplbState
from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_group
@@ -1134,13 +1135,11 @@ class GPUModelRunner(
"""
self._kv_block_zeroer = KVBlockZeroer(
self.device,
pin_memory=PIN_MEMORY,
attn_groups_iter=self._kv_cache_spec_attn_group_iterator(),
kernel_block_sizes=self._kernel_block_sizes,
cache_dtype=self.cache_config.cache_dtype,
runner_only_attn_layers=self.runner_only_attn_layers,
static_forward_context=self.compilation_config.static_forward_context,
max_concurrency=self.vllm_config.max_concurrent_batches,
)
def _zero_block_ids(self, block_ids: list[int]) -> None:
@@ -6217,10 +6216,7 @@ class GPUModelRunner(
# memory during profile_run.
# No .clone() of logits: warmup output is discarded, so any in-place
# mutation by forward_native does not affect correctness.
if self.sampler.logprobs_mode not in (
"processed_logits",
"processed_logprobs",
):
if self.sampler.logprobs_mode not in PROCESSED_LOGPROBS_MODES:
self.sampler(
logits=logits,
sampling_metadata=replace(
+1 -43
View File
@@ -91,13 +91,11 @@ class KVBlockZeroer:
def __init__(
self,
device: torch.device,
pin_memory: bool,
attn_groups_iter: Iterable["AttentionGroup"],
kernel_block_sizes: list[int],
cache_dtype: str,
static_forward_context: dict[str, Any],
runner_only_attn_layers: set[str] | None = None,
max_concurrency: int = 1,
) -> None:
"""Precompute the absolute-address table for the Triton zeroing kernel.
@@ -112,15 +110,7 @@ class KVBlockZeroer:
Only AttentionSpec layers are processed; Mamba layers are skipped.
"""
self.device = device
self.pin_memory = pin_memory
if max_concurrency < 1:
raise ValueError("max_concurrency must be at least 1")
self.max_concurrency = max_concurrency
self._meta: tuple[torch.Tensor, int, int, int] | None = None
self._id_cap: int = 0
self._ids_pinned: list[torch.Tensor] = []
self._ids_gpu: list[torch.Tensor] = []
self._id_buffer_index = 0
if runner_only_attn_layers is None:
runner_only_attn_layers = set()
@@ -182,8 +172,6 @@ class KVBlockZeroer:
return
blk_size = min(largest_power_of_2_divisor(page_size_el), 1024)
self._id_cap = 8192
self._allocate_id_buffers()
self._meta = (
torch.tensor(seg_addrs, dtype=torch.uint64, device=self.device),
page_size_el,
@@ -191,43 +179,13 @@ class KVBlockZeroer:
len(seg_addrs),
)
def _allocate_id_buffers(self) -> None:
self._ids_pinned = [
torch.empty(
self._id_cap,
dtype=torch.int64,
pin_memory=self.pin_memory,
)
for _ in range(self.max_concurrency)
]
self._ids_gpu = [
torch.empty(self._id_cap, dtype=torch.int64, device=self.device)
for _ in range(self.max_concurrency)
]
self._id_buffer_index = 0
def zero_block_ids(self, block_ids: list[int]) -> None:
"""Zero the KV cache memory for the given block IDs."""
if not block_ids or self._meta is None:
return
seg_addrs, page_size_el, blk_size, n_segs = self._meta
n_blocks = len(block_ids)
if n_blocks > self._id_cap:
# The old pinned buffers may still be the source of an in-flight
# nonblocking copy. Growing is rare, so we don't mind the sync overhead
torch.accelerator.synchronize()
self._id_cap = n_blocks * 2
self._allocate_id_buffers()
# The H2D copy is nonblocking, so its pinned source must not be mutated
# while this batch is in flight. Rotate through as many buffers as concurrent
# in-flight batches, to avoid collisions.
buffer_index = self._id_buffer_index
self._id_buffer_index = (buffer_index + 1) % self.max_concurrency
ids_pinned = self._ids_pinned[buffer_index]
ids_pinned[:n_blocks].numpy()[:] = block_ids
idx = self._ids_gpu[buffer_index][:n_blocks]
idx.copy_(ids_pinned[:n_blocks], non_blocking=True)
idx = async_tensor_h2d(block_ids, device=self.device, dtype=torch.int64)
grid = (n_blocks * n_segs * (page_size_el // blk_size),)
_zero_kv_blocks_kernel[grid](
seg_addrs,