Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d98b72bd3 | ||
|
|
75ccdf3145 | ||
|
|
c6fe94b4d5 | ||
|
|
46f01a50ac | ||
|
|
f00efc5265 | ||
|
|
b0cb1da1bd | ||
|
|
0e36e3bbd1 | ||
|
|
494845e79f | ||
|
|
0416dab275 | ||
|
|
c8db00b16c | ||
|
|
80c7683923 | ||
|
|
638d6e9757 | ||
|
|
1ad84fea86 | ||
|
|
12213c6795 | ||
|
|
10c75477b0 | ||
|
|
ac36a7a1e7 | ||
|
|
521aa80f71 | ||
|
|
a76df87db8 | ||
|
|
a4904ba903 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+13
-2
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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], (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user