Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .buildkite/scripts/hardware_ci/run-xpu-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ docker run \
python3 examples/offline_inference/basic/generate.py --model ibm-research/PowerMoE-3b --block-size 64 --enforce-eager -tp 2
python3 examples/offline_inference/basic/generate.py --model ibm-research/PowerMoE-3b --block-size 64 --enforce-eager -tp 2 --enable-expert-parallel
cd tests
pytest -v -s v1/core --ignore=v1/core/test_reset_prefix_cache_e2e.py
pytest -v -s v1/core --ignore=v1/core/test_reset_prefix_cache_e2e.py --ignore=v1/core/test_scheduler_e2e.py
pytest -v -s v1/engine
pytest -v -s v1/sample --ignore=v1/sample/test_logprobs.py --ignore=v1/sample/test_logprobs_e2e.py
pytest -v -s v1/worker --ignore=v1/worker/test_gpu_model_runner.py
Expand Down
13 changes: 8 additions & 5 deletions .buildkite/scripts/upload-release-wheels-pypi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,20 @@ mkdir -p $DIST_DIR
# include only wheels for the release version, ignore all files with "dev" or "rc" in the name (without excluding 'aarch64')
aws s3 cp --recursive --exclude "*" --include "vllm-${PURE_VERSION}*.whl" --exclude "*dev*" --exclude "*rc[0-9]*" "$S3_COMMIT_PREFIX" $DIST_DIR
echo "Wheels copied to local directory"
# generate source tarball
git archive --format=tar.gz --output="$DIST_DIR/vllm-${PURE_VERSION}.tar.gz" "$BUILDKITE_COMMIT"
# generate source distribution using setup.py
python setup.py sdist --dist-dir=$DIST_DIR
ls -la $DIST_DIR

SDIST_FILE=$(find $DIST_DIR -name "vllm*.tar.gz")
echo "Found sdist: $SDIST_FILE"

# upload wheels to PyPI (only default variant, i.e. files without '+' in the name)
PYPI_WHEEL_FILES=$(find $DIST_DIR -name "vllm-${PURE_VERSION}*.whl" -not -name "*+*")
if [[ -z "$PYPI_WHEEL_FILES" ]]; then
echo "No default variant wheels found, quitting..."
exit 1
fi

python3 -m twine check "$PYPI_WHEEL_FILES"
python3 -m twine upload --non-interactive --verbose "$PYPI_WHEEL_FILES"
echo "Wheels uploaded to PyPI"
python3 -m twine check "$PYPI_WHEEL_FILES" "$SDIST_FILE"
python3 -m twine upload --non-interactive --verbose "$PYPI_WHEEL_FILES" "$SDIST_FILE"
echo "Wheels and source distribution uploaded to PyPI"
2 changes: 1 addition & 1 deletion requirements/common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ tokenizers >= 0.21.1 # Required for fast incremental detokenization.
protobuf >= 5.29.6, !=6.30.*, !=6.31.*, !=6.32.*, !=6.33.0.*, !=6.33.1.*, !=6.33.2.*, !=6.33.3.*, !=6.33.4.* # Required by LlamaTokenizer, gRPC. CVE-2026-0994
fastapi[standard] >= 0.115.0 # Required by FastAPI's form models in the OpenAI API server's audio transcriptions endpoint.
aiohttp >= 3.13.3
openai >= 1.99.1 # For Responses API with reasoning content
openai >= 1.99.1, < 2.25.0 # For Responses API with reasoning content
pydantic >= 2.12.0
prometheus_client >= 0.18.0
pillow # Required for image processing
Expand Down
3 changes: 3 additions & 0 deletions requirements/cuda.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ torchaudio==2.10.0
torchvision==0.25.0 # Required for phi3v processor. See https://github.com/pytorch/vision?tab=readme-ov-file#installation for corresponding version
# FlashInfer should be updated together with the Dockerfile
flashinfer-python==0.6.4
# Cap nvidia-cudnn-frontend (transitive dep of flashinfer) due to
# breaking changes in 1.19.0
nvidia-cudnn-frontend>=1.13.0,<1.19.0

# QuACK and Cutlass DSL for FA4 (cute-DSL implementation)
nvidia-cutlass-dsl>=4.4.0.dev1
Expand Down
2 changes: 1 addition & 1 deletion requirements/xpu.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ torch==2.10.0+xpu
torchaudio
torchvision

vllm_xpu_kernels @ https://github.com/vllm-project/vllm-xpu-kernels/releases/download/v0.1.2/vllm_xpu_kernels-0.1.2-cp312-cp312-linux_x86_64.whl
vllm_xpu_kernels @ https://github.com/vllm-project/vllm-xpu-kernels/releases/download/v0.1.3/vllm_xpu_kernels-0.1.3-cp38-abi3-linux_x86_64.whl
172 changes: 172 additions & 0 deletions tests/reasoning/test_nemotron_v3_reasoning_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

from typing import TypedDict

import pytest
import regex as re

from tests.reasoning.utils import run_reasoning_extraction
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.reasoning import ReasoningParser, ReasoningParserManager

parser_name = "nemotron_v3"


class ReasoningCase(TypedDict):
output: str
reasoning: str | None
content: str | None


class FakeNemotronTokenizer:
def __init__(self):
self._vocab = {
"<think>": 1,
"</think>": 2,
}
self._pattern = re.compile(r"(<think>|</think>)")

def get_vocab(self) -> dict[str, int]:
return self._vocab

def tokenize(self, text: str) -> list[str]:
tokens: list[str] = []
for part in self._pattern.split(text):
if part:
tokens.append(part)
return tokens

def convert_tokens_to_string(self, tokens: list[str]) -> str:
return "".join(tokens)


@pytest.fixture
def tokenizer():
return FakeNemotronTokenizer()


@pytest.mark.parametrize(
"streaming,param_dict",
[
pytest.param(
False,
{
"output": "This is a reasoning section</think>This is the rest",
"reasoning": "This is a reasoning section",
"content": "This is the rest",
},
id="without_start_token",
),
pytest.param(
True,
{
"output": "This is a reasoning section</think>This is the rest",
"reasoning": "This is a reasoning section",
"content": "This is the rest",
},
id="without_start_token_streaming",
),
pytest.param(
False,
{
"output": "<think>This is a reasoning section</think>This is the rest",
"reasoning": "This is a reasoning section",
"content": "This is the rest",
},
id="with_start_token",
),
pytest.param(
True,
{
"output": "<think>This is a reasoning section</think>This is the rest",
"reasoning": "This is a reasoning section",
"content": "This is the rest",
},
id="with_start_token_streaming",
),
],
)
def test_nemotron_v3_reasoning(
tokenizer: FakeNemotronTokenizer,
streaming: bool,
param_dict: ReasoningCase,
):
output = tokenizer.tokenize(param_dict["output"])
model_output = [tokenizer.convert_tokens_to_string([token]) for token in output]
parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
tokenizer
)

reasoning, content = run_reasoning_extraction(
parser, model_output, streaming=streaming
)

assert reasoning == param_dict["reasoning"]
assert content == param_dict["content"]


def test_nemotron_v3_without_thinking_returns_content(
tokenizer: FakeNemotronTokenizer,
):
parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
parser = parser_cls(tokenizer)
request = ChatCompletionRequest(
model="test-model",
messages=[],
chat_template_kwargs={"enable_thinking": False},
)

reasoning, content = run_reasoning_extraction(
parser,
["This is plain content"],
request=request,
streaming=False,
)

assert reasoning is None
assert content == "This is plain content"


def test_nemotron_v3_force_nonempty_content_returns_content(
tokenizer: FakeNemotronTokenizer,
):
parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
parser = parser_cls(tokenizer)
request = ChatCompletionRequest(
model="test-model",
messages=[],
chat_template_kwargs={"force_nonempty_content": True},
)

reasoning, content = run_reasoning_extraction(
parser,
["<think>This is plain content"],
request=request,
streaming=False,
)

assert reasoning is None
assert content == "This is plain content"


def test_nemotron_v3_with_thinking_keeps_truncated_reasoning(
tokenizer: FakeNemotronTokenizer,
):
parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
parser = parser_cls(tokenizer)
request = ChatCompletionRequest(
model="test-model",
messages=[],
chat_template_kwargs={"enable_thinking": True},
)

reasoning, content = run_reasoning_extraction(
parser,
["This is truncated reasoning"],
request=request,
streaming=False,
)

assert reasoning == "This is truncated reasoning"
assert content is None
12 changes: 11 additions & 1 deletion tests/tokenizers_/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ def test_tokenizer_like_protocol():
_assert_tokenizer_like(tokenizer)

tokenizer = get_tokenizer(
"mistralai/Mistral-7B-Instruct-v0.3", tokenizer_mode="mistral"
"mistralai/Mistral-7B-Instruct-v0.3",
tokenizer_mode="mistral",
)
assert isinstance(tokenizer, MistralTokenizer)
_assert_tokenizer_like(tokenizer)
Expand All @@ -40,11 +41,20 @@ def test_tokenizer_like_protocol():

tokenizer = get_tokenizer("deepseek-ai/DeepSeek-V3", tokenizer_mode="deepseek_v32")
assert isinstance(tokenizer, HfTokenizer)

# Verify it's a fast tokenizer (required for FastIncrementalDetokenizer)
assert isinstance(tokenizer, PreTrainedTokenizerFast)
assert "DSV32" in tokenizer.__class__.__name__
_assert_tokenizer_like(tokenizer)

tokenizer = get_tokenizer(
"Qwen/Qwen-VL",
tokenizer_mode="qwen_vl",
trust_remote_code=True,
)
assert isinstance(tokenizer, HfTokenizer)
assert "WithoutImagePad" in tokenizer.__class__.__name__


@pytest.mark.parametrize("tokenizer_name", ["facebook/opt-125m", "gpt2"])
def test_tokenizer_revision(tokenizer_name: str):
Expand Down
1 change: 1 addition & 0 deletions vllm/benchmarks/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,7 @@ def add_cli_args(parser: argparse.ArgumentParser):
- "slow" will always use the slow tokenizer.\n
- "mistral" will always use the tokenizer from `mistral_common`.\n
- "deepseek_v32" will always use the tokenizer from `deepseek_v32`.\n
- "qwen_vl" will always use the tokenizer from `qwen_vl`.\n
- Other custom values can be supported via plugins.""",
)
parser.add_argument("--use-beam-search", action="store_true")
Expand Down
1 change: 1 addition & 0 deletions vllm/config/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class ModelConfig:
- "slow" will always use the slow tokenizer.\n
- "mistral" will always use the tokenizer from `mistral_common`.\n
- "deepseek_v32" will always use the tokenizer from `deepseek_v32`.\n
- "qwen_vl" will always use the tokenizer from `qwen_vl`.\n
- Other custom values can be supported via plugins."""
trust_remote_code: bool = False
"""Trust remote code (e.g., from HuggingFace) when downloading the model
Expand Down
25 changes: 13 additions & 12 deletions vllm/config/speculative.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,16 +299,16 @@ def hf_config_override(hf_config: PretrainedConfig) -> PretrainedConfig:
{"n_predict": n_predict, "architectures": ["ExaoneMoeMTP"]}
)

if hf_config.model_type in ("qwen3_5", "qwen3_5_moe"):
is_moe = hf_config.model_type == "qwen3_5_moe"
hf_config.model_type = "qwen3_5_mtp"
n_predict = getattr(hf_config, "mtp_num_hidden_layers", None)
hf_config.update(
{
"n_predict": n_predict,
"architectures": ["Qwen3_5MoeMTP" if is_moe else "Qwen3_5MTP"],
}
)
# if hf_config.model_type in ("qwen3_5", "qwen3_5_moe"):
# is_moe = hf_config.model_type == "qwen3_5_moe"
# hf_config.model_type = "qwen3_5_mtp"
# n_predict = getattr(hf_config, "mtp_num_hidden_layers", None)
# hf_config.update(
# {
# "n_predict": n_predict,
# "architectures": ["Qwen3_5MoeMTP" if is_moe else "Qwen3_5MTP"],
# }
# )
if hf_config.model_type == "longcat_flash":
hf_config.model_type = "longcat_flash_mtp"
n_predict = getattr(hf_config, "num_nextn_predict_layers", 1)
Expand Down Expand Up @@ -347,7 +347,7 @@ def __post_init__(self):
"method `%s` is deprecated and replaced with mtp.", self.method
)
self.method = "mtp"

if self.model is None and self.num_speculative_tokens is not None:
if self.method == "mtp":
if self.target_model_config is None:
Expand Down Expand Up @@ -465,7 +465,7 @@ def __post_init__(self):
)

# Automatically detect the method
if self.method in ("eagle", "eagle3"):
if self.method in ("eagle", "eagle3", 'medusa'):
pass
# examples:
# yuhuili/EAGLE-LLaMA3-Instruct-8B
Expand Down Expand Up @@ -589,6 +589,7 @@ def __post_init__(self):
self.target_parallel_config, self.draft_tensor_parallel_size
)
)
self.method = "medusa"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

self.method = "medusa" is placed at indentation level 8 (same as the top-level if/elif/else chain), so it executes unconditionally at the end of __post_init__, overriding whichever speculative decoding method was correctly detected (eagle, eagle3, mtp, mlp_speculator, etc.). This silently bypasses all method-specific validation and forces incorrect draft model execution.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Remove the stale/misindented line self.method = "medusa" at line 592. The medusa method is already correctly assigned conditionally at line 479 (elif self.draft_model_config.hf_config.model_type == "medusa": self.method = "medusa"). This leftover line executes unconditionally and overrides whichever speculative decoding method was correctly detected. Simply delete line 592 so that only return self remains.

⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.

Suggested change
self.method = "medusa"
return self

return self

def _validate_suffix_decoding(self):
Expand Down
12 changes: 1 addition & 11 deletions vllm/model_executor/layers/fused_moe/experts/trtllm_fp8_moe.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ def __init__(
):
super().__init__(moe_config, quant_config)

if moe_config.moe_parallel_config.use_ep and quant_config.is_per_tensor:
raise NotImplementedError(
"EP parallelism is not supported with TRTLLM"
"per-tensor FP8 quantization."
)

self.routing_method_type = moe_config.routing_method
self.topk = moe_config.experts_per_token
self.intermediate_size_per_partition = (
Expand Down Expand Up @@ -182,9 +176,6 @@ def _apply_per_block(
assert not apply_router_weight_on_input
assert activation == MoEActivation.SILU

if e_score_correction_bias is not None:
e_score_correction_bias = e_score_correction_bias.to(hidden_states.dtype)

if self.routing_method_type == RoutingMethodType.DeepSeekV3:
router_logits = router_logits.to(torch.float32)

Expand Down Expand Up @@ -240,12 +231,11 @@ def _apply_per_tensor(
) -> torch.Tensor:
# Delay import for non-CUDA.
import flashinfer
from flashinfer.fused_moe.core import ActivationType

# Confirm supported activation function.
assert activation in [MoEActivation.SILU, MoEActivation.RELU2_NO_MUL]

activation_type = ActivationType(activation_to_flashinfer_int(activation))
activation_type = activation_to_flashinfer_int(activation)

# Confirm Llama-4 routing is proper.
if self.routing_method_type == RoutingMethodType.Llama4:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,5 @@ def apply(
routed_scaling_factor=routed_scaling_factor,
routing_method_type=self.routing_method_type,
do_finalize=True,
activation_type=activation_to_flashinfer_int(activation),
)[0]
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ def _supports_current_device() -> bool:

@staticmethod
def _supports_no_act_and_mul() -> bool:
return False
return True

@staticmethod
def _supports_quant_scheme(
Expand Down
Loading