0
tests/e2e/pull_request/__init__.py
Normal file
0
tests/e2e/pull_request/__init__.py
Normal file
0
tests/e2e/pull_request/four_card/_310p/__init__.py
Normal file
0
tests/e2e/pull_request/four_card/_310p/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
|
||||
def test_qwen3_dense_tp2_fp16():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-8B",
|
||||
tensor_parallel_size=2,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
max_model_len=16384,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
|
||||
def test_qwen3_dense_tp4_w8a8():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"vllm-ascend/Qwen3-32B-W8A8",
|
||||
tensor_parallel_size=4,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
quantization="ascend",
|
||||
max_model_len=16384,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
@@ -0,0 +1,74 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
|
||||
def test_qwen3_moe_tp4_fp16():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-30B-A3B",
|
||||
tensor_parallel_size=4,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
max_model_len=16384,
|
||||
max_num_batched_tokens=2048,
|
||||
max_num_seqs=256,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
|
||||
def test_qwen3_moe_tp2_w8a8():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"vllm-ascend/Qwen3-30B-A3B-W8A8",
|
||||
tensor_parallel_size=2,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
quantization="ascend",
|
||||
max_model_len=16384,
|
||||
max_num_batched_tokens=2048,
|
||||
max_num_seqs=256,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
|
||||
@pytest.mark.skip("Probabilistic failure, need fix")
|
||||
def test_qwen3_5_moe_tp4_fp16():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3.5-35B-A3B",
|
||||
tensor_parallel_size=4,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
max_model_len=16384,
|
||||
max_num_batched_tokens=2048,
|
||||
max_num_seqs=256,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
34
tests/e2e/pull_request/four_card/_310p/test_vl_model_310p.py
Normal file
34
tests/e2e/pull_request/four_card/_310p/test_vl_model_310p.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from tests.e2e.conftest import wait_until_npu_memory_free
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
full_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
sys.path.insert(0, full_dir)
|
||||
|
||||
# ruff: noqa: E402
|
||||
from tests.e2e.pull_request.utils_310p import run_vl_model_test
|
||||
|
||||
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.7)
|
||||
def test_qwen3_vl_8b_tp2_fp16():
|
||||
"""Qwen3-VL-8B dual-card FP16 test"""
|
||||
run_vl_model_test(model_name="Qwen/Qwen3-VL-8B-Instruct", tensor_parallel_size=2, max_tokens=5)
|
||||
0
tests/e2e/pull_request/four_card/__init__.py
Normal file
0
tests/e2e/pull_request/four_card/__init__.py
Normal file
@@ -0,0 +1,493 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
"""PCP/DCP long-sequence accuracy guards.
|
||||
|
||||
Run `pytest tests/e2e/pull_request/four_card/context_parallel/test_accuracy.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from vllm import SamplingParams
|
||||
|
||||
from tests.e2e.conftest import DPVllmRunner, VllmRunner, wait_until_npu_memory_free
|
||||
|
||||
DEEPSEEK_V2_LITE = "vllm-ascend/DeepSeek-V2-Lite-W8A8"
|
||||
DEEPSEEK_MTP = "wemaster/deepseek_mtp_main_random_bf16"
|
||||
MAX_NUM_SEQS = 4
|
||||
E2E_ROOT = Path(__file__).resolve().parents[3]
|
||||
QWEN_IMAGE_PATH = E2E_ROOT / "prompts" / "qwen.png"
|
||||
|
||||
FULL_DECODE_GRAPH = {
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
"cudagraph_capture_sizes": [MAX_NUM_SEQS],
|
||||
}
|
||||
|
||||
|
||||
COMMON_PROMPTS = [
|
||||
"The capital of France is",
|
||||
"Hello, my name is Tom, I am",
|
||||
"The president of United States is",
|
||||
]
|
||||
|
||||
DSV2_PROMPTS = [
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
]
|
||||
|
||||
DSV2_PCP_GOLDEN = [
|
||||
"The president of the United States is a man who is not only a liar, but",
|
||||
"The capital of France is Paris.\nThe currency of France is the Euro",
|
||||
]
|
||||
|
||||
DSV2_DCP_GOLDEN = [
|
||||
"The president of the United States is a man who is not only a liar, but",
|
||||
"The capital of France is Paris.\nThe currency of France is the Euro",
|
||||
]
|
||||
|
||||
QWEN3_GOLDEN = [
|
||||
"The capital of France is Paris. The capital of",
|
||||
"Hello, my name is Tom, I am 12 years old",
|
||||
"The president of United States is the head of state and",
|
||||
]
|
||||
|
||||
QWEN3_NEXT_GOLDEN = [
|
||||
"The capital of France is Paris. The capital of",
|
||||
"Hello, my name is Tom, I am 12 years old",
|
||||
"The president of United States is the head of state and",
|
||||
]
|
||||
|
||||
DSV3_2_GOLDEN = [
|
||||
"The capital of France isbearerhaloce梗",
|
||||
"Hello, my name is Tom, I am" + "ERIC slicpacelike Chop",
|
||||
"The president of United States is平行astra unbehetroni",
|
||||
]
|
||||
|
||||
DSV3_2_GOLDEN_BACKUPS = (
|
||||
[
|
||||
"The capital of France isbearerhaloce梗",
|
||||
"Hello, my name is Tom, I am" + "ERIC slicpacelike Chop",
|
||||
"The president of United States isoint054 Rund959arki",
|
||||
],
|
||||
[
|
||||
"The capital of France isbearerdenomorthal",
|
||||
"Hello, my name is Tom, I am" + "ERIC slicpacelike Chop",
|
||||
"The president of United States is平行astra unbehetroni",
|
||||
],
|
||||
[
|
||||
"The capital of France isbearerdenomorthal",
|
||||
"Hello, my name is Tom, I am" + "ERIC slicpacelike Chop",
|
||||
"The president of United States isoint054 Rund959arki",
|
||||
],
|
||||
)
|
||||
|
||||
DSV3_2_DCP_GOLDEN = [
|
||||
"The capital of France isoint054 Rund959arki",
|
||||
"Hello, my name is Tom, I am" + "ERIC slicpacelike挂",
|
||||
"The president of United States isoint054 Rund959arki",
|
||||
]
|
||||
|
||||
DEEPSEEK_MTP3_GOLDEN = [
|
||||
"The capital of France is Salmonella团团 elsewhereッγκ",
|
||||
"Hello, my name is Tom, I amEiSlowukt Analysis sprouts",
|
||||
"The president of United States is Salmonella团团 elsewhereッγκ",
|
||||
]
|
||||
|
||||
DEEPSEEK_V4_PROMPTS = [
|
||||
"Hello, my name is",
|
||||
"What is the meaning of life?",
|
||||
]
|
||||
|
||||
DEEPSEEK_V4_GOLDEN = ["Hello, my name is {name} and I", 'What is the meaning of life?",\n "What is']
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AccuracyCase:
|
||||
name: str
|
||||
model: str
|
||||
prompts: Sequence[str]
|
||||
expected_outputs: Sequence[str] | Sequence[Sequence[str]]
|
||||
max_tokens: int
|
||||
runner_kwargs: dict[str, Any]
|
||||
|
||||
|
||||
def _run_accuracy_case(case: AccuracyCase) -> None:
|
||||
runner_cls = DPVllmRunner if case.runner_kwargs.get("data_parallel_size", 1) > 1 else VllmRunner
|
||||
with runner_cls(case.model, **case.runner_kwargs) as runner:
|
||||
outputs = runner.generate_greedy(list(case.prompts), case.max_tokens)
|
||||
|
||||
if isinstance(case.expected_outputs[0], str):
|
||||
expected_outputs = cast(Sequence[str], case.expected_outputs)
|
||||
match_outputs_with_goldens(outputs, expected_outputs)
|
||||
else:
|
||||
# If multiple expected output sets are provided, the output is considered correct if it matches any of the sets.
|
||||
multi_expected_outputs = cast(Sequence[Sequence[str]], case.expected_outputs)
|
||||
tries = []
|
||||
for expected in multi_expected_outputs:
|
||||
try:
|
||||
match_outputs_with_goldens(outputs, expected)
|
||||
except AssertionError as exc:
|
||||
tries.append(f"Output did not match expected set:\n{exc}")
|
||||
else:
|
||||
break
|
||||
if len(tries) == len(multi_expected_outputs):
|
||||
failure_details = "\n\n".join(tries)
|
||||
raise AssertionError(f"Output did not match any of the expected output sets:\n{failure_details}")
|
||||
|
||||
|
||||
def match_outputs_with_goldens(outputs: list[tuple[list[int], str]], goldens: Sequence[str]) -> None:
|
||||
"""Helper function to compare output with golden output, ignoring whitespace differences."""
|
||||
outputs_str: Sequence[str] = [output[1] for output in outputs]
|
||||
assert len(outputs_str) == len(goldens)
|
||||
for index, (output, golden) in enumerate(zip(outputs_str, goldens)):
|
||||
assert isinstance(output, str) and isinstance(golden, str), "Both output and golden must be strings"
|
||||
assert output and golden, "Output and golden should not be empty"
|
||||
assert output.strip() == golden.strip()
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"HCCL_BUFFSIZE": "768",
|
||||
"VLLM_WORKER_MULTIPROC_METHOD": "spawn",
|
||||
"OMP_NUM_THREADS": "1",
|
||||
"OMP_PROC_BIND": "false",
|
||||
"PYTORCH_NPU_ALLOC_CONF": "expandable_segments:True",
|
||||
},
|
||||
)
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.8)
|
||||
def test_qwen3_vl_multimodal_pcp_accuracy_guard() -> None:
|
||||
image = Image.open(QWEN_IMAGE_PATH).convert("RGB")
|
||||
single_image_prompt = (
|
||||
"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n"
|
||||
"<|im_start|>user\n"
|
||||
"<|vision_start|><|image_pad|><|vision_end|>"
|
||||
"Describe this image in detail.<|im_end|>\n"
|
||||
"<|im_start|>assistant\n"
|
||||
)
|
||||
multi_image_prompt = (
|
||||
"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n"
|
||||
"<|im_start|>user\n"
|
||||
"<|vision_start|><|image_pad|><|vision_end|>"
|
||||
"<|vision_start|><|image_pad|><|vision_end|>"
|
||||
"Compare these two images and describe similarities.<|im_end|>\n"
|
||||
"<|im_start|>assistant\n"
|
||||
)
|
||||
inputs = [
|
||||
{
|
||||
"prompt": single_image_prompt,
|
||||
"multi_modal_data": {"image": image},
|
||||
},
|
||||
{
|
||||
"prompt": multi_image_prompt,
|
||||
"multi_modal_data": {"image": [image, image.copy()]},
|
||||
},
|
||||
]
|
||||
sampling_params = SamplingParams(max_tokens=16, temperature=0.0)
|
||||
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-VL-8B-Instruct",
|
||||
enforce_eager=False,
|
||||
max_model_len=4096,
|
||||
tensor_parallel_size=2,
|
||||
prefill_context_parallel_size=2,
|
||||
decode_context_parallel_size=1,
|
||||
max_num_batched_tokens=1024,
|
||||
block_size=128,
|
||||
limit_mm_per_prompt={"image": 2},
|
||||
mm_processor_kwargs={
|
||||
"min_pixels": 28 * 28,
|
||||
"max_pixels": 1280 * 28 * 28,
|
||||
"fps": 1,
|
||||
},
|
||||
compilation_config={
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
"cudagraph_capture_sizes": [MAX_NUM_SEQS],
|
||||
},
|
||||
) as runner:
|
||||
outputs = runner.model.generate(inputs, sampling_params=sampling_params)
|
||||
|
||||
assert len(outputs) == len(inputs)
|
||||
for output in outputs:
|
||||
assert output.outputs and output.outputs[0].text.strip()
|
||||
|
||||
|
||||
DSV2_COMMON_KWARGS: dict[str, Any] = {
|
||||
"max_model_len": 1024,
|
||||
"max_num_seqs": MAX_NUM_SEQS,
|
||||
"max_num_batched_tokens": 1024,
|
||||
"enable_expert_parallel": True,
|
||||
"enable_chunked_prefill": True,
|
||||
"enable_prefix_caching": True,
|
||||
"block_size": 128,
|
||||
"quantization": "ascend",
|
||||
"compilation_config": FULL_DECODE_GRAPH,
|
||||
"additional_config": {"enable_flashcomm1": True},
|
||||
}
|
||||
|
||||
DSV2_PARALLEL_CASES = [
|
||||
AccuracyCase(
|
||||
name="dsv2_pcp_dcp_full_features",
|
||||
model=DEEPSEEK_V2_LITE,
|
||||
prompts=DSV2_PROMPTS,
|
||||
expected_outputs=DSV2_PCP_GOLDEN,
|
||||
max_tokens=10,
|
||||
runner_kwargs={
|
||||
**DSV2_COMMON_KWARGS,
|
||||
"tensor_parallel_size": 2,
|
||||
"prefill_context_parallel_size": 2,
|
||||
"decode_context_parallel_size": 2,
|
||||
"cp_kv_cache_interleave_size": 128,
|
||||
"long_prefill_token_threshold": 4,
|
||||
},
|
||||
),
|
||||
AccuracyCase(
|
||||
name="dsv2_pcp_only_full_features",
|
||||
model=DEEPSEEK_V2_LITE,
|
||||
prompts=DSV2_PROMPTS,
|
||||
expected_outputs=DSV2_PCP_GOLDEN,
|
||||
max_tokens=10,
|
||||
runner_kwargs={
|
||||
**DSV2_COMMON_KWARGS,
|
||||
"tensor_parallel_size": 2,
|
||||
"prefill_context_parallel_size": 2,
|
||||
"decode_context_parallel_size": 1,
|
||||
"cp_kv_cache_interleave_size": 128,
|
||||
"long_prefill_token_threshold": 4,
|
||||
},
|
||||
),
|
||||
AccuracyCase(
|
||||
name="dsv2_dcp_only_full_features",
|
||||
model=DEEPSEEK_V2_LITE,
|
||||
prompts=DSV2_PROMPTS,
|
||||
expected_outputs=DSV2_DCP_GOLDEN,
|
||||
max_tokens=10,
|
||||
runner_kwargs={
|
||||
**DSV2_COMMON_KWARGS,
|
||||
"tensor_parallel_size": 2,
|
||||
"prefill_context_parallel_size": 1,
|
||||
"decode_context_parallel_size": 2,
|
||||
"long_prefill_token_threshold": 64,
|
||||
"compilation_config": {
|
||||
**FULL_DECODE_GRAPH,
|
||||
"pass_config": {"enable_sp": True},
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
FULL_FEATURE_MODEL_CASES = [
|
||||
AccuracyCase(
|
||||
name="qwen3_pcp_dcp_full_features",
|
||||
model="vllm-ascend/Qwen3-30B-A3B-W8A8",
|
||||
prompts=COMMON_PROMPTS,
|
||||
expected_outputs=QWEN3_GOLDEN,
|
||||
max_tokens=5,
|
||||
runner_kwargs={
|
||||
"max_model_len": 1024,
|
||||
"max_num_seqs": MAX_NUM_SEQS,
|
||||
"max_num_batched_tokens": 1024,
|
||||
"tensor_parallel_size": 2,
|
||||
"prefill_context_parallel_size": 2,
|
||||
"decode_context_parallel_size": 1,
|
||||
"enable_expert_parallel": True,
|
||||
"enable_chunked_prefill": True,
|
||||
"enable_prefix_caching": True,
|
||||
"block_size": 128,
|
||||
"quantization": "ascend",
|
||||
"long_prefill_token_threshold": 4,
|
||||
"compilation_config": FULL_DECODE_GRAPH,
|
||||
"additional_config": {"enable_flashcomm1": True},
|
||||
},
|
||||
),
|
||||
AccuracyCase(
|
||||
name="qwen3_next_pcp_dcp_full_features",
|
||||
model="Qwen/Qwen3-Next-80B-A3B-Instruct",
|
||||
prompts=COMMON_PROMPTS,
|
||||
expected_outputs=QWEN3_NEXT_GOLDEN,
|
||||
max_tokens=5,
|
||||
runner_kwargs={
|
||||
"enforce_eager": True,
|
||||
"max_model_len": 1024,
|
||||
"tensor_parallel_size": 2,
|
||||
"prefill_context_parallel_size": 2,
|
||||
"decode_context_parallel_size": 1,
|
||||
"max_num_batched_tokens": 1024,
|
||||
"enable_expert_parallel": True,
|
||||
# TODO(qcs): We should set `long_prefill_token_threshold` to 4
|
||||
# when chunked prefill with PCP is stable.
|
||||
"long_prefill_token_threshold": 128,
|
||||
"gpu_memory_utilization": 0.8,
|
||||
"block_size": 128,
|
||||
# FlashComm1 is disabled for qwen3_next until the PCP decode path is fixed.
|
||||
"additional_config": {"enable_flashcomm1": False},
|
||||
},
|
||||
),
|
||||
AccuracyCase(
|
||||
name="dsv3_2_pcp_dcp_full_features",
|
||||
model="vllm-ascend/DeepSeek-V3.2-W8A8-Pruning",
|
||||
prompts=COMMON_PROMPTS,
|
||||
# TODO(qcs): Remove multi-expected_outputs after the first request output is stable.
|
||||
expected_outputs=(DSV3_2_GOLDEN, *DSV3_2_GOLDEN_BACKUPS),
|
||||
max_tokens=5,
|
||||
runner_kwargs={
|
||||
"max_model_len": 1024,
|
||||
"max_num_seqs": MAX_NUM_SEQS,
|
||||
"max_num_batched_tokens": 1024,
|
||||
"tensor_parallel_size": 2,
|
||||
"prefill_context_parallel_size": 2,
|
||||
"decode_context_parallel_size": 2,
|
||||
"enable_expert_parallel": True,
|
||||
"enable_chunked_prefill": True,
|
||||
"enable_prefix_caching": True,
|
||||
"gpu_memory_utilization": 0.2,
|
||||
"cp_kv_cache_interleave_size": 128,
|
||||
"block_size": 128,
|
||||
"quantization": "ascend",
|
||||
# TODO(qcs): We should set `long_prefill_token_threshold` to 4
|
||||
# when chunked prefill with PCP is stable.
|
||||
"long_prefill_token_threshold": 128,
|
||||
"compilation_config": FULL_DECODE_GRAPH,
|
||||
"additional_config": {"enable_flashcomm1": True},
|
||||
# graph_mode is disabled for dsv32 until the PCP gatherv3 out of index issue is fixed.
|
||||
"enforce_eager": True,
|
||||
},
|
||||
),
|
||||
AccuracyCase(
|
||||
name="dsv3_2_sfa_dcp_replicated_indexer",
|
||||
model="vllm-ascend/DeepSeek-V3.2-W8A8-Pruning",
|
||||
prompts=COMMON_PROMPTS,
|
||||
expected_outputs=DSV3_2_DCP_GOLDEN,
|
||||
max_tokens=5,
|
||||
runner_kwargs={
|
||||
"max_model_len": 1024,
|
||||
"max_num_seqs": MAX_NUM_SEQS,
|
||||
"max_num_batched_tokens": 1024,
|
||||
"data_parallel_size": 2,
|
||||
"tensor_parallel_size": 2,
|
||||
"prefill_context_parallel_size": 1,
|
||||
"decode_context_parallel_size": 2,
|
||||
"enable_expert_parallel": True,
|
||||
"enable_chunked_prefill": True,
|
||||
"enable_prefix_caching": True,
|
||||
"gpu_memory_utilization": 0.4,
|
||||
"cp_kv_cache_interleave_size": 1,
|
||||
"block_size": 128,
|
||||
"quantization": "ascend",
|
||||
"long_prefill_token_threshold": 128,
|
||||
"compilation_config": FULL_DECODE_GRAPH,
|
||||
"additional_config": {
|
||||
"enable_flashcomm1": True,
|
||||
"enable_dsa_cp": True,
|
||||
"enable_sparse_sfa_c8": True,
|
||||
"enable_sparse_li_c8": True,
|
||||
},
|
||||
"speculative_config": {
|
||||
"method": "mtp",
|
||||
"num_speculative_tokens": 3,
|
||||
},
|
||||
},
|
||||
),
|
||||
pytest.param(
|
||||
AccuracyCase(
|
||||
name="deepseek_mtp3_pcp_dcp_full_features",
|
||||
model=DEEPSEEK_MTP,
|
||||
prompts=COMMON_PROMPTS,
|
||||
expected_outputs=DEEPSEEK_MTP3_GOLDEN,
|
||||
max_tokens=5,
|
||||
runner_kwargs={
|
||||
"max_model_len": 1024,
|
||||
"max_num_seqs": MAX_NUM_SEQS,
|
||||
"max_num_batched_tokens": 1024,
|
||||
"tensor_parallel_size": 2,
|
||||
"prefill_context_parallel_size": 2,
|
||||
"decode_context_parallel_size": 2,
|
||||
"enable_expert_parallel": True,
|
||||
"enable_chunked_prefill": True,
|
||||
"enable_prefix_caching": True,
|
||||
"block_size": 128,
|
||||
"long_prefill_token_threshold": 4,
|
||||
"speculative_config": {
|
||||
"method": "mtp",
|
||||
"num_speculative_tokens": 3,
|
||||
},
|
||||
"compilation_config": FULL_DECODE_GRAPH,
|
||||
"additional_config": {"enable_flashcomm1": True},
|
||||
},
|
||||
),
|
||||
marks=pytest.mark.skip(reason="Temporarily skip MTP with PCP/DCP until the token layout issue is fixed."),
|
||||
),
|
||||
AccuracyCase(
|
||||
name="deepseek_v4_w4a8_dsa_cp_full_features",
|
||||
model="gdydems/DeepSeek-V4-Flash-w4a8-mtp",
|
||||
prompts=DEEPSEEK_V4_PROMPTS,
|
||||
expected_outputs=DEEPSEEK_V4_GOLDEN,
|
||||
max_tokens=5,
|
||||
runner_kwargs={
|
||||
"max_model_len": 8192,
|
||||
"max_num_seqs": 16,
|
||||
"max_num_batched_tokens": 4096,
|
||||
"dtype": "auto",
|
||||
"tensor_parallel_size": 4,
|
||||
"prefill_context_parallel_size": 1,
|
||||
"decode_context_parallel_size": 1,
|
||||
"enable_expert_parallel": True,
|
||||
"gpu_memory_utilization": 0.9,
|
||||
"quantization": "ascend",
|
||||
"tokenizer_mode": "deepseek_v4",
|
||||
"block_size": 128,
|
||||
"compilation_config": {
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
},
|
||||
"additional_config": {
|
||||
"enable_flashcomm1": True,
|
||||
"enable_dsa_cp": True,
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"HCCL_BUFFSIZE": "768",
|
||||
},
|
||||
)
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.8)
|
||||
@pytest.mark.parametrize("case", DSV2_PARALLEL_CASES, ids=lambda case: case.name)
|
||||
def test_dsv2_lite_parallel_config_accuracy(case: AccuracyCase) -> None:
|
||||
_run_accuracy_case(case)
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"HCCL_BUFFSIZE": "768",
|
||||
"PYTORCH_NPU_ALLOC_CONF": "expandable_segments:True",
|
||||
},
|
||||
)
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.8)
|
||||
@pytest.mark.parametrize("case", FULL_FEATURE_MODEL_CASES, ids=lambda case: case.name)
|
||||
def test_models_pcp_dcp_full_feature_accuracy(case: AccuracyCase) -> None:
|
||||
_run_accuracy_case(case)
|
||||
@@ -0,0 +1,76 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
"""DeepSeek-V2-Lite prefix-cache CP guard.
|
||||
|
||||
Run `pytest tests/e2e/pull_request/four_card/context_parallel/test_prefix_caching_cp.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
# TODO(qcs): We should use Qwen3.5 for this test when it's fixed and available,
|
||||
# so that we can test the hybrid kv cache with both full attention and linear attention.
|
||||
MODEL = "vllm-ascend/DeepSeek-V2-Lite-W8A8"
|
||||
MAX_NUM_SEQS = 2
|
||||
THREAD_ENV = {
|
||||
"OMP_NUM_THREADS": "1",
|
||||
"MKL_NUM_THREADS": "1",
|
||||
"OPENBLAS_NUM_THREADS": "1",
|
||||
"NUMEXPR_NUM_THREADS": "1",
|
||||
}
|
||||
|
||||
DSV2_LITE_PREFIX_PROMPT = (
|
||||
"You are reading a compact synthetic operations ledger. "
|
||||
"Use only the rows below when answering the final question.\n"
|
||||
+ "\n".join(
|
||||
f"Row {i}: route R{i:03d} moves cargo from zone {i % 11} to zone {(i * 7) % 13}; priority is {i % 5}."
|
||||
for i in range(64)
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
INPUT_PROMPTS = [
|
||||
DSV2_LITE_PREFIX_PROMPT + "Question: What route is listed in row 17? Answer briefly.",
|
||||
DSV2_LITE_PREFIX_PROMPT + "Question: What priority is listed in row 42? Answer briefly.",
|
||||
]
|
||||
|
||||
|
||||
@patch.dict(os.environ, THREAD_ENV)
|
||||
def test_dsv2_lite_prefix_cache_with_pcp() -> None:
|
||||
with VllmRunner(
|
||||
MODEL,
|
||||
block_size=128,
|
||||
max_model_len=2048,
|
||||
max_num_seqs=MAX_NUM_SEQS,
|
||||
max_num_batched_tokens=2048,
|
||||
tensor_parallel_size=2,
|
||||
prefill_context_parallel_size=2,
|
||||
decode_context_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
enable_expert_parallel=True,
|
||||
enable_prefix_caching=True,
|
||||
quantization="ascend",
|
||||
) as vllm_model:
|
||||
prefix_cache_outputs = vllm_model.generate_greedy(INPUT_PROMPTS, 8)
|
||||
|
||||
assert len(prefix_cache_outputs) == len(INPUT_PROMPTS)
|
||||
for output_ids, output_text in prefix_cache_outputs:
|
||||
assert output_ids
|
||||
assert output_text
|
||||
@@ -0,0 +1,85 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
"""Compare the short outputs of HF and vLLM when using greedy sampling.
|
||||
|
||||
Run `pytest tests/e2e/pull_request/four_card/spec_decode/test_mtp_qwen3_next.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from vllm.config import CompilationConfig
|
||||
from vllm.v1.metrics.reader import Counter, Vector
|
||||
|
||||
from tests.e2e.conftest import VllmRunner, cleanup_dist_env_and_memory
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
MODELS = ["Qwen/Qwen3-Next-80B-A3B-Instruct"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", MODELS)
|
||||
def test_qwen3_next_mtp_acceptance_tp4(model_name):
|
||||
golden = [0.85, 0.46, 0.19]
|
||||
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
max_tokens = 1024
|
||||
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
tensor_parallel_size=4,
|
||||
max_model_len=4096,
|
||||
gpu_memory_utilization=0.8,
|
||||
distributed_executor_backend="mp",
|
||||
disable_log_stats=False,
|
||||
speculative_config={
|
||||
"method": "qwen3_next_mtp",
|
||||
"num_speculative_tokens": 3,
|
||||
},
|
||||
compilation_config=CompilationConfig(cudagraph_mode="FULL_DECODE_ONLY", cudagraph_capture_sizes=[20]),
|
||||
) as spec_vllm_model:
|
||||
_ = spec_vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
metrics = spec_vllm_model.model.get_metrics()
|
||||
|
||||
num_drafts = 0
|
||||
num_accepted_tokens_per_pos = [0] * 3
|
||||
for metric in metrics:
|
||||
if metric.name == "vllm:spec_decode_num_drafts":
|
||||
assert isinstance(metric, Counter)
|
||||
num_drafts += metric.value
|
||||
elif metric.name == "vllm:spec_decode_num_accepted_tokens_per_pos":
|
||||
assert isinstance(metric, Vector)
|
||||
for pos in range(len(metric.values)):
|
||||
num_accepted_tokens_per_pos[pos] += metric.values[pos]
|
||||
|
||||
acceptance_per_pos = [num_accepted_tokens / num_drafts for num_accepted_tokens in num_accepted_tokens_per_pos]
|
||||
|
||||
match = all((a >= b) or (b - a < 0.06) for a, b in zip(acceptance_per_pos, golden))
|
||||
if not match:
|
||||
print(f"acceptance_per_pos: {acceptance_per_pos}")
|
||||
print(f"golden: {golden}")
|
||||
|
||||
assert match
|
||||
cleanup_dist_env_and_memory()
|
||||
49
tests/e2e/pull_request/four_card/test_data_parallel_tp2.py
Normal file
49
tests/e2e/pull_request/four_card/test_data_parallel_tp2.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import wait_until_npu_memory_free
|
||||
|
||||
MODELS = ["Qwen/Qwen3-30B-A3B"]
|
||||
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||
DATA_PARALLEL_SCRIPT = REPO_ROOT / "examples" / "offline_data_parallel.py"
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="broken, fix me")
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [32])
|
||||
@patch.dict(os.environ, {"ASCEND_RT_VISIBLE_DEVICES": "0,1,2,3"})
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.7)
|
||||
def test_qwen3_inference_dp2_tp2(model, max_tokens):
|
||||
env = os.environ.copy()
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(DATA_PARALLEL_SCRIPT),
|
||||
"--model",
|
||||
model,
|
||||
"--dp-size",
|
||||
"2",
|
||||
"--tp-size",
|
||||
"2",
|
||||
"--node-size",
|
||||
"1",
|
||||
"--node-rank",
|
||||
"0",
|
||||
"--trust-remote-code",
|
||||
]
|
||||
|
||||
print(f"Running subprocess: {' '.join(cmd)}")
|
||||
proc = subprocess.run(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=600)
|
||||
output = proc.stdout.decode(errors="ignore")
|
||||
|
||||
print(output)
|
||||
|
||||
assert "DP rank 0 needs to process" in output
|
||||
assert "DP rank 1 needs to process" in output
|
||||
assert "Generated text:" in output
|
||||
assert proc.returncode == 0
|
||||
@@ -0,0 +1,164 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
from tests.e2e.conftest import DisaggPDProxy, RemotePDServer, VllmRunner, wait_until_npu_memory_free
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1"})
|
||||
@wait_until_npu_memory_free()
|
||||
def test_moe_w8a8_tp_pp_ep_full_decode_only():
|
||||
"""Verify W8A8 MoE generation with TP, PP, EP, and full decode only."""
|
||||
model = "vllm-ascend/DeepSeek-V3.2-W8A8-Pruning"
|
||||
prompts = ["Hello, my name is"]
|
||||
|
||||
with VllmRunner(
|
||||
model,
|
||||
enable_expert_parallel=True,
|
||||
quantization="ascend",
|
||||
max_model_len=1024,
|
||||
tensor_parallel_size=2,
|
||||
pipeline_parallel_size=2,
|
||||
gpu_memory_utilization=0.8,
|
||||
compilation_config={"cudagraph_capture_sizes": [2, 4, 6, 8, 10, 12], "cudagraph_mode": "FULL_DECODE_ONLY"},
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.generate_greedy(prompts, max_tokens=500)
|
||||
|
||||
assert len(outputs) == len(prompts)
|
||||
assert len(outputs[0][1]) > len(prompts[0])
|
||||
|
||||
|
||||
@wait_until_npu_memory_free()
|
||||
def test_pd_disaggregation_w8a8_sfa_dsa_full_decode_only():
|
||||
"""Verify W8A8 1P1D PD disaggregation with full decode only."""
|
||||
prefiller_port = [get_open_port()]
|
||||
decoder_port = [get_open_port()]
|
||||
proxy_port = get_open_port()
|
||||
|
||||
ld_library_path = os.environ.get("LD_LIBRARY_PATH", "")
|
||||
env_dict = {
|
||||
"LD_LIBRARY_PATH": f"/usr/local/lib:{ld_library_path}",
|
||||
}
|
||||
|
||||
vllm_server_args = [
|
||||
[
|
||||
"--port",
|
||||
str(prefiller_port[0]),
|
||||
"--model",
|
||||
"vllm-ascend/DeepSeek-V3.2-W8A8-Pruning",
|
||||
"--trust-remote-code",
|
||||
"--enable-request-id-headers",
|
||||
"--no-enable-prefix-caching",
|
||||
"--enable-expert-parallel",
|
||||
"--quantization",
|
||||
"ascend",
|
||||
"--max-model-len",
|
||||
"1024",
|
||||
"--max-num-batched-tokens",
|
||||
"1024",
|
||||
"--max-num-seqs",
|
||||
"4",
|
||||
"--tensor-parallel-size",
|
||||
"2",
|
||||
"--gpu-memory-utilization",
|
||||
"0.9",
|
||||
"--kv-transfer-config",
|
||||
json.dumps(
|
||||
{
|
||||
"kv_connector": "MooncakeConnectorV1",
|
||||
"kv_role": "kv_producer",
|
||||
"kv_port": "30000",
|
||||
"kv_connector_extra_config": {
|
||||
"prefill": {"dp_size": 1, "tp_size": 2},
|
||||
"decode": {"dp_size": 1, "tp_size": 2},
|
||||
},
|
||||
}
|
||||
),
|
||||
"--enforce-eager",
|
||||
],
|
||||
[
|
||||
"--port",
|
||||
str(decoder_port[0]),
|
||||
"--model",
|
||||
"vllm-ascend/DeepSeek-V3.2-W8A8-Pruning",
|
||||
"--trust-remote-code",
|
||||
"--enable-request-id-headers",
|
||||
"--no-enable-prefix-caching",
|
||||
"--enable-expert-parallel",
|
||||
"--quantization",
|
||||
"ascend",
|
||||
"--max-model-len",
|
||||
"1024",
|
||||
"--max-num-batched-tokens",
|
||||
"1024",
|
||||
"--max-num-seqs",
|
||||
"4",
|
||||
"--tensor-parallel-size",
|
||||
"2",
|
||||
"--gpu-memory-utilization",
|
||||
"0.9",
|
||||
"--kv-transfer-config",
|
||||
json.dumps(
|
||||
{
|
||||
"kv_connector": "MooncakeConnectorV1",
|
||||
"kv_role": "kv_consumer",
|
||||
"kv_port": "30200",
|
||||
"kv_connector_extra_config": {
|
||||
"prefill": {"dp_size": 1, "tp_size": 2},
|
||||
"decode": {"dp_size": 1, "tp_size": 2},
|
||||
},
|
||||
}
|
||||
),
|
||||
"--compilation-config",
|
||||
json.dumps(
|
||||
{
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
"cudagraph_capture_sizes": [1, 2, 4, 8],
|
||||
}
|
||||
),
|
||||
],
|
||||
]
|
||||
|
||||
with (
|
||||
RemotePDServer(vllm_server_args, env_dict=env_dict),
|
||||
DisaggPDProxy(
|
||||
port=proxy_port,
|
||||
prefiller_ports=prefiller_port,
|
||||
decoder_ports=decoder_port,
|
||||
) as proxy,
|
||||
):
|
||||
response = requests.post(
|
||||
proxy.url_for("v1", "completions"),
|
||||
json={
|
||||
"model": "vllm-ascend/DeepSeek-V3.2-W8A8-Pruning",
|
||||
"prompt": "Hello, my name is",
|
||||
"max_tokens": 5,
|
||||
"temperature": 0.0,
|
||||
},
|
||||
timeout=600,
|
||||
)
|
||||
response.raise_for_status()
|
||||
output = response.json()
|
||||
|
||||
assert output["choices"][0]["text"]
|
||||
117
tests/e2e/pull_request/four_card/test_deepseek_v4.py
Normal file
117
tests/e2e/pull_request/four_card/test_deepseek_v4.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.e2e.conftest import VllmRunner, wait_until_npu_memory_free
|
||||
|
||||
os.environ["PYTORCH_NPU_ALLOC_CONF"] = "expandable_segments:True"
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1",
|
||||
},
|
||||
)
|
||||
@wait_until_npu_memory_free()
|
||||
def test_deepseek_v4_w4a8_tp4_basic_greedy():
|
||||
"""Verify DeepSeek V4 W4A8 basic greedy generation with TP4 and EP."""
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
"What is the meaning of life?",
|
||||
]
|
||||
max_tokens = 5
|
||||
|
||||
with VllmRunner(
|
||||
"gdydems/DeepSeek-V4-Flash-w4a8-mtp",
|
||||
max_model_len=8192,
|
||||
max_num_seqs=16,
|
||||
max_num_batched_tokens=4096,
|
||||
dtype="auto",
|
||||
tensor_parallel_size=4,
|
||||
enable_expert_parallel=True,
|
||||
gpu_memory_utilization=0.9,
|
||||
quantization="ascend",
|
||||
tokenizer_mode="deepseek_v4",
|
||||
block_size=128,
|
||||
compilation_config={
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
},
|
||||
speculative_config={"num_speculative_tokens": 1, "method": "mtp"},
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
expected_token_ids = [
|
||||
[19923, 14, 1026, 2329, 344, 680, 2852, 95, 305, 342],
|
||||
[3085, 344, 270, 5281, 294, 1988, 33, 3955, 361, 582, 3085, 344],
|
||||
]
|
||||
assert len(outputs) == len(example_prompts)
|
||||
for i, (output_ids, output_str) in enumerate(outputs):
|
||||
assert len(output_str) > 0
|
||||
assert len(output_ids) > 0
|
||||
assert output_ids == expected_token_ids[i]
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1",
|
||||
},
|
||||
)
|
||||
@wait_until_npu_memory_free()
|
||||
def test_deepseek_v4_w4a8_tp4_index_cache_freq4():
|
||||
"""IndexCache freq=4 must produce non-empty greedy outputs identical in
|
||||
shape to the baseline test, verifying skip_topk/topk_indices_buffer
|
||||
plumbing (DSAModules → AscendDSAImpl) is wired correctly across both
|
||||
serial and dual-stream paths.
|
||||
"""
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
"The capital of France is",
|
||||
"What is the meaning of life?",
|
||||
]
|
||||
max_tokens = 5
|
||||
|
||||
with VllmRunner(
|
||||
"gdydems/DeepSeek-V4-Flash-w4a8-mtp",
|
||||
max_model_len=8192,
|
||||
max_num_seqs=16,
|
||||
max_num_batched_tokens=4096,
|
||||
dtype="auto",
|
||||
tensor_parallel_size=4,
|
||||
enable_expert_parallel=True,
|
||||
gpu_memory_utilization=0.9,
|
||||
quantization="ascend",
|
||||
tokenizer_mode="deepseek_v4",
|
||||
block_size=128,
|
||||
compilation_config={
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
},
|
||||
hf_overrides={
|
||||
"use_index_cache": True,
|
||||
"index_topk_freq": 4,
|
||||
},
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
assert len(outputs) == len(example_prompts)
|
||||
for output_ids, output_str in outputs:
|
||||
assert len(output_str) > 0
|
||||
assert len(output_ids) > 0
|
||||
673
tests/e2e/pull_request/four_card/test_graph_mode.py
Normal file
673
tests/e2e/pull_request/four_card/test_graph_mode.py
Normal file
@@ -0,0 +1,673 @@
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import contextlib
|
||||
import multiprocessing
|
||||
import os
|
||||
import queue
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm import LLM, SamplingParams
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
from tests.e2e.conftest import cleanup_dist_env_and_memory, wait_until_npu_memory_free
|
||||
from tests.e2e.pull_request.utils import PROMPTS_LONG, PROMPTS_SHORT
|
||||
|
||||
QWEN3 = "Qwen/Qwen3-0.6B"
|
||||
DEEPSEEK_V2_LITE = "vllm-ascend/DeepSeek-V2-Lite-W8A8"
|
||||
|
||||
QWEN3_PROMPTS_SHORT_BASELINE = [
|
||||
{
|
||||
"token_ids": [444, 2210, 13, 358, 2776, 264, 220, 17, 17, 4666, 6284, 5458, 504, 5616, 13, 358],
|
||||
"logprobs": [
|
||||
-4.082355976104736,
|
||||
-1.8239692449569702,
|
||||
-0.7117747664451599,
|
||||
-0.19997872412204742,
|
||||
-0.9261042475700378,
|
||||
-1.156473994255066,
|
||||
-1.6240551471710205,
|
||||
-0.4462895393371582,
|
||||
-1.7280645370483398,
|
||||
-0.03872159123420715,
|
||||
-0.006691192742437124,
|
||||
-1.729214072227478,
|
||||
-1.4082962274551392,
|
||||
-1.2914694547653198,
|
||||
-0.30865585803985596,
|
||||
-0.19352123141288757,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [279, 1852, 438, 279, 4767, 315, 279, 3639, 19140, 13, 1096, 374, 1576, 279, 4767, 315],
|
||||
"logprobs": [
|
||||
-1.329056978225708,
|
||||
-1.9254618883132935,
|
||||
-0.16430772840976715,
|
||||
-0.07656584680080414,
|
||||
-0.20014727115631104,
|
||||
-0.009215084835886955,
|
||||
-0.6130291223526001,
|
||||
-0.7763887047767639,
|
||||
-0.7629368901252747,
|
||||
-0.7878186702728271,
|
||||
-1.5039337873458862,
|
||||
-0.34936416149139404,
|
||||
-1.1888322830200195,
|
||||
-0.7042690515518188,
|
||||
-0.7195678949356079,
|
||||
-0.5817108154296875,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [12095, 13, 576, 6722, 315, 15344, 374, 21718, 13, 576, 6722, 315, 17689, 374, 24081, 13],
|
||||
"logprobs": [
|
||||
-0.4164676070213318,
|
||||
-0.650071918964386,
|
||||
-1.2497835159301758,
|
||||
-0.41373029351234436,
|
||||
-0.10615493357181549,
|
||||
-1.7584394216537476,
|
||||
-0.014708652161061764,
|
||||
-0.09583187848329544,
|
||||
-0.05152897536754608,
|
||||
-0.5075417757034302,
|
||||
-0.04731765389442444,
|
||||
-0.004497650545090437,
|
||||
-0.734763503074646,
|
||||
-0.006638852413743734,
|
||||
-0.03197822347283363,
|
||||
-0.02921150252223015,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [537, 1101, 264, 29016, 8645, 714, 264, 27155, 17991, 315, 1246, 582, 3887, 11, 975, 11],
|
||||
"logprobs": [
|
||||
-2.253798246383667,
|
||||
-0.6618977785110474,
|
||||
-1.1329351663589478,
|
||||
-1.5547151565551758,
|
||||
-2.502274751663208,
|
||||
-0.6954820156097412,
|
||||
-0.16636334359645844,
|
||||
-0.9172338247299194,
|
||||
-0.8822890520095825,
|
||||
-0.6346368789672852,
|
||||
-1.1293692588806152,
|
||||
-0.355191707611084,
|
||||
-0.23319871723651886,
|
||||
-0.3448694050312042,
|
||||
-0.015147067606449127,
|
||||
-0.023382289335131645,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
QWEN3_PROMPTS_LONG_BASELINE = [
|
||||
{
|
||||
"token_ids": [4710, 1249, 11625, 419, 3491, 11, 582, 1184, 311, 990, 279, 7506, 315, 328, 1543, 323],
|
||||
"logprobs": [
|
||||
-1.1807091236114502,
|
||||
-0.7548902034759521,
|
||||
-0.06596270948648453,
|
||||
-0.21140751242637634,
|
||||
-0.17375628650188446,
|
||||
-0.012642711400985718,
|
||||
-0.8002182841300964,
|
||||
-0.5415360331535339,
|
||||
-0.005715575534850359,
|
||||
-1.0228453874588013,
|
||||
-0.49259668588638306,
|
||||
-0.5897566080093384,
|
||||
-0.0001705739414319396,
|
||||
-0.008742272853851318,
|
||||
-0.0008293526479974389,
|
||||
-0.20622815191745758,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [4710, 1249, 11625, 419, 3491, 11, 582, 646, 990, 279, 2701, 5486, 25, 6771, 400, 47],
|
||||
"logprobs": [
|
||||
-0.6663188934326172,
|
||||
-0.31078171730041504,
|
||||
-0.1298152357339859,
|
||||
-0.17452840507030487,
|
||||
-0.14723201096057892,
|
||||
-0.011572140268981457,
|
||||
-0.5467657446861267,
|
||||
-1.1380974054336548,
|
||||
-0.8147172927856445,
|
||||
-0.2714247703552246,
|
||||
-1.2143880128860474,
|
||||
-0.84015291929245,
|
||||
-0.7829729318618774,
|
||||
-1.5653221607208252,
|
||||
-0.631846010684967,
|
||||
-1.7602534294128418,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [4710, 1249, 11625, 419, 3491, 11, 582, 646, 990, 279, 2701, 5486, 25, 6771, 400, 1124],
|
||||
"logprobs": [
|
||||
-0.979120671749115,
|
||||
-0.74333256483078,
|
||||
-0.22203724086284637,
|
||||
-0.31942218542099,
|
||||
-0.2570137679576874,
|
||||
-0.012989195063710213,
|
||||
-0.939915657043457,
|
||||
-1.038073182106018,
|
||||
-0.4173763394355774,
|
||||
-0.17641115188598633,
|
||||
-1.119675874710083,
|
||||
-1.2035267353057861,
|
||||
-0.7646281719207764,
|
||||
-1.5680660009384155,
|
||||
-0.5041356086730957,
|
||||
-1.0269478559494019,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
DEEPSEEK_V2_LITE_PROMPTS_SHORT_BASELINE = [
|
||||
{
|
||||
"token_ids": [185, 40, 608, 245, 207, 17, 15, 1012, 1712, 12608, 11, 285, 304, 463, 803, 14079],
|
||||
"logprobs": [
|
||||
-4.621323108673096,
|
||||
-3.077335834503174,
|
||||
-1.3346226215362549,
|
||||
-0.9667544364929199,
|
||||
-2.628643274307251,
|
||||
-1.0353751182556152,
|
||||
-1.9108309745788574,
|
||||
-0.9746326208114624,
|
||||
-0.07258293032646179,
|
||||
-2.375293016433716,
|
||||
-1.7306381464004517,
|
||||
-1.7152436971664429,
|
||||
-0.5231161713600159,
|
||||
-1.2002876996994019,
|
||||
-0.945522665977478,
|
||||
-2.263141393661499,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [245, 668, 779, 317, 441, 889, 245, 69524, 11, 548, 245, 42357, 11, 245, 39925, 11],
|
||||
"logprobs": [
|
||||
-1.9601335525512695,
|
||||
-2.9202699661254883,
|
||||
-0.935673713684082,
|
||||
-1.9938912391662598,
|
||||
-2.5300979614257812,
|
||||
-2.1406760215759277,
|
||||
-2.01202130317688,
|
||||
-2.9169065952301025,
|
||||
-0.5751016736030579,
|
||||
-0.5648833513259888,
|
||||
-1.2851604223251343,
|
||||
-2.817136764526367,
|
||||
-0.826416015625,
|
||||
-0.8048403263092041,
|
||||
-1.2173839807510376,
|
||||
-0.32657456398010254,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [8913, 13, 185, 549, 19305, 280, 7239, 317, 254, 28071, 13, 185, 549, 13829, 13451, 279],
|
||||
"logprobs": [
|
||||
-0.39279282093048096,
|
||||
-0.8086707592010498,
|
||||
-0.7125738859176636,
|
||||
-1.6490240097045898,
|
||||
-1.9644602537155151,
|
||||
-0.4904576539993286,
|
||||
-0.0832064300775528,
|
||||
-0.00726190535351634,
|
||||
-0.38277116417884827,
|
||||
-0.2940319776535034,
|
||||
-0.7399694323539734,
|
||||
-0.07564151287078857,
|
||||
-0.7905924320220947,
|
||||
-1.8350766897201538,
|
||||
-0.4076140522956848,
|
||||
-0.20184002816677094,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [6464, 11, 285, 359, 487, 82, 889, 1872, 276, 752, 34993, 13, 1733, 20838, 11106, 276],
|
||||
"logprobs": [
|
||||
-2.4984869956970215,
|
||||
-0.7851194739341736,
|
||||
-0.6003906726837158,
|
||||
-1.4917504787445068,
|
||||
-0.9962607026100159,
|
||||
-0.004179196432232857,
|
||||
-1.5068310499191284,
|
||||
-0.4656505584716797,
|
||||
-0.0027805021964013577,
|
||||
-0.11925199627876282,
|
||||
-0.35579147934913635,
|
||||
-0.35492807626724243,
|
||||
-1.605405330657959,
|
||||
-1.38535737991333,
|
||||
-0.930091142654419,
|
||||
-0.004122450482100248,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
DEEPSEEK_V2_LITE_PROMPTS_LONG_BASELINE = [
|
||||
{
|
||||
"token_ids": [185, 185, 1679, 26430, 279, 16145, 285, 8204, 185, 185, 13483, 9890, 16982, 457, 17693, 829],
|
||||
"logprobs": [
|
||||
-0.24134111404418945,
|
||||
-0.06949862092733383,
|
||||
-2.196256637573242,
|
||||
-0.3235849142074585,
|
||||
-5.745722592109814e-05,
|
||||
-7.152554530875932e-07,
|
||||
-9.381330892210826e-05,
|
||||
-3.4570632578834193e-06,
|
||||
-0.0006878394051454961,
|
||||
-6.079655122448457e-06,
|
||||
-7.152554530875932e-07,
|
||||
-8.356221951544285e-05,
|
||||
-1.8358061424805783e-05,
|
||||
-1.2636104656849056e-05,
|
||||
-1.5497195136049413e-06,
|
||||
-2.0146166207268834e-05,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [185, 185, 1679, 26430, 279, 16145, 285, 8204, 185, 185, 13483, 9890, 16982, 457, 17693, 829],
|
||||
"logprobs": [
|
||||
-0.27419501543045044,
|
||||
-0.08238636702299118,
|
||||
-2.0612075328826904,
|
||||
-1.0054221153259277,
|
||||
-4.947062916471623e-05,
|
||||
-4.768370445162873e-07,
|
||||
-2.1457441107486375e-05,
|
||||
-3.933898824470816e-06,
|
||||
-0.00018630675913300365,
|
||||
-1.1920920996999484e-06,
|
||||
-4.172316494077677e-06,
|
||||
-0.00010048838157672435,
|
||||
-1.0847986231965479e-05,
|
||||
-1.1920928244535389e-07,
|
||||
-1.1920920996999484e-06,
|
||||
-1.4185804502631072e-05,
|
||||
],
|
||||
},
|
||||
{
|
||||
"token_ids": [185, 185, 1679, 26430, 279, 16145, 285, 8204, 185, 185, 13483, 9890, 16982, 457, 17693, 829],
|
||||
"logprobs": [
|
||||
-0.14973750710487366,
|
||||
-0.09690935909748077,
|
||||
-1.8772021532058716,
|
||||
-1.6962311267852783,
|
||||
-0.0001429217227268964,
|
||||
-8.344646289515367e-07,
|
||||
-7.617183291586116e-05,
|
||||
-3.814689989667386e-06,
|
||||
-0.0010556369088590145,
|
||||
-6.318072337307967e-06,
|
||||
-1.1920920996999484e-06,
|
||||
-0.00035553809721022844,
|
||||
-2.729855441430118e-05,
|
||||
-2.3841855067985307e-07,
|
||||
-1.4305104514278355e-06,
|
||||
-4.410733708937187e-06,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
CASE_QWEN_ACLGRAPH = {
|
||||
"model": QWEN3,
|
||||
"quantization": None,
|
||||
"prompts": {"short": PROMPTS_SHORT, "long": PROMPTS_LONG},
|
||||
"compilation_config": {"max_cudagraph_capture_size": 24, "cudagraph_mode": "FULL"},
|
||||
"tensor_parallel_size": 4,
|
||||
"data_parallel_size": 1,
|
||||
"enable_expert_parallel": False,
|
||||
"golden_answers": {"short": QWEN3_PROMPTS_SHORT_BASELINE, "long": QWEN3_PROMPTS_LONG_BASELINE},
|
||||
# TODO: it increases after profile graph memory is disabled, invetigate later
|
||||
"baseline_capture_mem": 0.30,
|
||||
"capture_mem_tolerance": 1.3,
|
||||
}
|
||||
|
||||
CASE_DS_ACLGRAPH = {
|
||||
"model": DEEPSEEK_V2_LITE,
|
||||
"quantization": "ascend",
|
||||
"prompts": {"short": PROMPTS_SHORT, "long": PROMPTS_LONG},
|
||||
"compilation_config": {"max_cudagraph_capture_size": 24, "cudagraph_mode": "FULL_AND_PIECEWISE"},
|
||||
"tensor_parallel_size": 2,
|
||||
"data_parallel_size": 2,
|
||||
"enable_expert_parallel": True,
|
||||
"golden_answers": {
|
||||
"short": DEEPSEEK_V2_LITE_PROMPTS_SHORT_BASELINE,
|
||||
"long": DEEPSEEK_V2_LITE_PROMPTS_LONG_BASELINE,
|
||||
},
|
||||
"baseline_capture_mem": 0.68,
|
||||
"capture_mem_tolerance": 1.5,
|
||||
}
|
||||
|
||||
CASE_DS_ACLGRAPH_ENPU = {
|
||||
**CASE_DS_ACLGRAPH,
|
||||
"env_vars": {"ENPU_ENABLE": "true"},
|
||||
}
|
||||
|
||||
# inherit from tests/e2e/pull_request/utils.py::compare_logprobs
|
||||
ATOL = 0.0689
|
||||
|
||||
_SAMPLING_PARAMS = SamplingParams(
|
||||
max_tokens=16,
|
||||
temperature=0.0,
|
||||
top_p=1.0,
|
||||
top_k=0,
|
||||
logprobs=20,
|
||||
)
|
||||
|
||||
|
||||
def _install_spies(metrics: dict[str, Any] | None):
|
||||
"""Installs thread-safe spies on NPU methods to track invocation counts."""
|
||||
if metrics is None:
|
||||
return contextlib.nullcontext()
|
||||
|
||||
from vllm_ascend.worker.model_runner_v1 import NPUModelRunner
|
||||
|
||||
def make_spy(cls, method_name, capture_mem):
|
||||
original = getattr(cls, method_name)
|
||||
|
||||
def spy(self, *args, **kwargs):
|
||||
mem_before = torch.npu.mem_get_info()[0] # free memory
|
||||
result = original(self, *args, **kwargs)
|
||||
mem_after = torch.npu.mem_get_info()[0]
|
||||
with capture_mem["call_counts"].get_lock():
|
||||
capture_mem["call_counts"].value += 1
|
||||
capture_mem["mem_before_capture"].value += mem_before
|
||||
capture_mem["mem_after_capture"].value += mem_after
|
||||
return result
|
||||
|
||||
return spy
|
||||
|
||||
stack = contextlib.ExitStack()
|
||||
hooks = [
|
||||
(NPUModelRunner, "capture_model", metrics["capture_mem"]),
|
||||
]
|
||||
|
||||
for cls, method, metric in hooks:
|
||||
stack.enter_context(patch.object(cls, method, make_spy(cls, method, metric)))
|
||||
|
||||
return stack
|
||||
|
||||
|
||||
def _check_prefill_token(baseline, comp_ids, comp_logprobs, prompt_idx: int, atol: float) -> None:
|
||||
"""Token 0 is produced by the prefill pass; both models see identical input,
|
||||
so the chosen token *must* be the same and its logprob must match within atol."""
|
||||
base_token_id = baseline["token_ids"][0]
|
||||
comp_token_id = comp_ids[0]
|
||||
assert base_token_id == comp_token_id, (
|
||||
f"Prefill token mismatch at prompt {prompt_idx}: baseline={base_token_id}, compiled={comp_token_id}"
|
||||
)
|
||||
base_logprob = baseline["logprobs"][0]
|
||||
comp_logprob = comp_logprobs[0][comp_token_id]
|
||||
assert abs(base_logprob - comp_logprob) <= atol, (
|
||||
f"Prefill logprob mismatch at prompt {prompt_idx}: "
|
||||
f"baseline={base_logprob:.4f}, compiled={comp_logprob:.4f}, "
|
||||
f"diff={abs(base_logprob - comp_logprob):.4f} > atol={atol}"
|
||||
)
|
||||
|
||||
|
||||
def _check_decode_token(baseline, comp_ids, comp_logprobs, token_idx: int, prompt_idx: int, decode_atol: float) -> None:
|
||||
"""Tokens 1-2 come from decode passes. When the two models pick different
|
||||
tokens the context has already diverged, so we cannot compare logprobs of
|
||||
the chosen tokens directly. Instead we do a cross-lookup: find the
|
||||
baseline's chosen token inside compiled's top-K distribution (and vice
|
||||
versa) and assert that the assigned log-probability is close. This
|
||||
confirms that the compiled model's distribution is numerically consistent
|
||||
with the baseline's even when the argmax differs by a tiny margin.
|
||||
"""
|
||||
base_token_id = baseline["token_ids"][token_idx]
|
||||
comp_token_id = comp_ids[token_idx]
|
||||
base_logprob = baseline["logprobs"][token_idx]
|
||||
comp_topk = comp_logprobs[token_idx]
|
||||
if base_token_id == comp_token_id:
|
||||
# Happy path: same token, direct logprob comparison.
|
||||
diff = abs(base_logprob - comp_topk[comp_token_id])
|
||||
assert diff <= decode_atol, (
|
||||
f"Decode logprob mismatch at prompt {prompt_idx}, token {token_idx}: "
|
||||
f"baseline={base_logprob:.4f}, "
|
||||
f"compiled={comp_topk[comp_token_id]:.4f}, "
|
||||
f"diff={diff:.4f} > decode_atol={decode_atol}"
|
||||
)
|
||||
return
|
||||
|
||||
# Tokens differ – cross-lookup in each model's top-K distribution.
|
||||
comp_logprob = comp_topk[comp_token_id]
|
||||
# Check: what log-probability did compiled assign to baseline's token?
|
||||
assert base_token_id in comp_topk, (
|
||||
f"Decode token mismatch at prompt {prompt_idx}, token {token_idx}: "
|
||||
f"baseline chose token {base_token_id} (logprob={base_logprob:.4f}) but "
|
||||
f"compiled chose token {comp_token_id} (logprob={comp_logprob:.4f}) and "
|
||||
f"baseline's token does not appear in compiled's top-{_SAMPLING_PARAMS.logprobs} distribution"
|
||||
)
|
||||
comp_logprob_of_base_token = comp_topk[base_token_id]
|
||||
diff = abs(base_logprob - comp_logprob_of_base_token)
|
||||
assert diff <= decode_atol, (
|
||||
f"Decode distribution mismatch at prompt {prompt_idx}, token {token_idx}: "
|
||||
f"baseline chose token {base_token_id} with logprob={base_logprob:.4f}; "
|
||||
f"compiled assigned logprob={comp_logprob_of_base_token:.4f} to that token, "
|
||||
f"diff={diff:.4f} > decode_atol={decode_atol} "
|
||||
f"(compiled chose token {comp_token_id} with logprob={comp_logprob:.4f})"
|
||||
)
|
||||
|
||||
|
||||
def _run_worker_process(
|
||||
rank: int,
|
||||
local_rank: int,
|
||||
world_size: int,
|
||||
cur_case: dict,
|
||||
master_ip: str,
|
||||
master_port: int,
|
||||
result_queue: multiprocessing.Queue,
|
||||
metrics: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Main entry point for the worker process."""
|
||||
os.environ.update(
|
||||
{
|
||||
"VLLM_DP_RANK": str(rank),
|
||||
"VLLM_DP_RANK_LOCAL": str(local_rank),
|
||||
"VLLM_DP_SIZE": str(world_size),
|
||||
"VLLM_DP_MASTER_IP": master_ip,
|
||||
"VLLM_DP_MASTER_PORT": str(master_port),
|
||||
}
|
||||
)
|
||||
|
||||
for key, value in cur_case.get("env_vars", {}).items():
|
||||
os.environ[key] = str(value)
|
||||
|
||||
# Apply hooks and run inference
|
||||
with _install_spies(metrics):
|
||||
short_prompts = cur_case["prompts"]["short"]
|
||||
chunk_size = len(short_prompts) // world_size
|
||||
short_start_idx = rank * chunk_size
|
||||
short_end_idx = short_start_idx + chunk_size if rank < world_size - 1 else len(short_prompts)
|
||||
local_short_prompts = short_prompts[short_start_idx:short_end_idx]
|
||||
|
||||
long_prompts = cur_case["prompts"]["long"]
|
||||
chunk_size = len(long_prompts) // world_size
|
||||
long_start_idx = rank * chunk_size
|
||||
long_end_idx = long_start_idx + chunk_size if rank < world_size - 1 else len(long_prompts)
|
||||
local_long_prompts = long_prompts[long_start_idx:long_end_idx]
|
||||
|
||||
llm = LLM(
|
||||
model=cur_case["model"],
|
||||
max_model_len=1024,
|
||||
compilation_config=cur_case["compilation_config"],
|
||||
quantization=cur_case["quantization"],
|
||||
tensor_parallel_size=cur_case["tensor_parallel_size"],
|
||||
enable_expert_parallel=cur_case["enable_expert_parallel"],
|
||||
trust_remote_code=True,
|
||||
)
|
||||
|
||||
compiled_outputs_short = llm.generate(local_short_prompts, _SAMPLING_PARAMS)
|
||||
compiled_outputs_long = llm.generate(local_long_prompts, _SAMPLING_PARAMS)
|
||||
|
||||
def extract_outputs(outputs):
|
||||
extracted = []
|
||||
for out in outputs:
|
||||
gen = out.outputs[0]
|
||||
extracted.append(
|
||||
{
|
||||
"text": gen.text,
|
||||
"token_ids": list(gen.token_ids),
|
||||
"logprobs": [
|
||||
{token_id: lp.logprob for token_id, lp in step_logprobs.items()}
|
||||
for step_logprobs in gen.logprobs
|
||||
]
|
||||
if gen.logprobs
|
||||
else None,
|
||||
}
|
||||
)
|
||||
return extracted
|
||||
|
||||
result_data = {
|
||||
"rank": rank,
|
||||
"short": {"prompt_idx": short_start_idx, "outputs": extract_outputs(compiled_outputs_short)},
|
||||
"long": {"prompt_idx": long_start_idx, "outputs": extract_outputs(compiled_outputs_long)},
|
||||
}
|
||||
result_queue.put(result_data)
|
||||
|
||||
|
||||
def _exit():
|
||||
from vllm_ascend.ascend_config import clear_ascend_config
|
||||
|
||||
clear_ascend_config()
|
||||
cleanup_dist_env_and_memory()
|
||||
|
||||
|
||||
def check_accuracy(baselines, result, atol, decode_atol):
|
||||
for idx, comp_out in enumerate(result["outputs"]):
|
||||
prompt_idx = result["prompt_idx"] + idx
|
||||
baseline = baselines[prompt_idx]
|
||||
comp_ids = comp_out["token_ids"]
|
||||
comp_logprobs = comp_out["logprobs"]
|
||||
|
||||
assert comp_logprobs is not None, f"logprobs not returned for prompt {prompt_idx}"
|
||||
assert len(baseline["token_ids"]) == len(comp_ids) == _SAMPLING_PARAMS.max_tokens, (
|
||||
f"Expected {_SAMPLING_PARAMS.max_tokens} tokens for prompt {prompt_idx}, "
|
||||
f"got baseline={len(baseline['token_ids'])}, compiled={len(comp_ids)}"
|
||||
)
|
||||
_check_prefill_token(baseline, comp_ids, comp_logprobs, prompt_idx, atol)
|
||||
for token_idx in range(1, _SAMPLING_PARAMS.max_tokens):
|
||||
_check_decode_token(baseline, comp_ids, comp_logprobs, token_idx, prompt_idx, decode_atol)
|
||||
|
||||
|
||||
def check_capture_mem(capture_mem, baseline_capture_mem=0.2, capture_mem_tolerance=1.3):
|
||||
assert capture_mem["call_counts"].value != 0, (
|
||||
f"capture_model was not called during test. capture_called({capture_mem['call_counts'].value})"
|
||||
)
|
||||
|
||||
print("capture_called =", capture_mem["call_counts"].value)
|
||||
print("capture_mem_before =", capture_mem["mem_before_capture"].value)
|
||||
print("capture_mem_after =", capture_mem["mem_after_capture"].value)
|
||||
|
||||
mem_used_by_capture = (
|
||||
capture_mem["mem_before_capture"].value - capture_mem["mem_after_capture"].value
|
||||
) / capture_mem["call_counts"].value
|
||||
# Empirical observation: capturing ACL graphs for Qwen3-0.6B uses ~0.20 GiB of NPU memory.
|
||||
# DeepSeek-V2-Lite-W8A8 uses ~0.68 GiB of NPU memory
|
||||
# a 1.3x tolerance is applied to account for runtime variance.
|
||||
max_capture_mem_gib = baseline_capture_mem * capture_mem_tolerance
|
||||
max_mem_expected = max_capture_mem_gib * (1024**3)
|
||||
assert mem_used_by_capture < max_mem_expected, (
|
||||
f"capture_model used more memory than expected. "
|
||||
f"Used: {mem_used_by_capture / (1024**3):.2f} GiB, "
|
||||
f"Expected: < {max_capture_mem_gib:.2f} GiB"
|
||||
)
|
||||
|
||||
|
||||
@wait_until_npu_memory_free(0.7)
|
||||
@pytest.mark.parametrize("cur_case", [CASE_QWEN_ACLGRAPH, CASE_DS_ACLGRAPH, CASE_DS_ACLGRAPH_ENPU])
|
||||
def test_aclgraph(cur_case: dict, monkeypatch: pytest.MonkeyPatch):
|
||||
# Counter doesn't work in default "spawn" mode
|
||||
metrics = None
|
||||
if "DeepSeek-V2-Lite-W8A8" in cur_case["model"]:
|
||||
# TODO(shihan-lin168): remove this env after set_device issue is resolved
|
||||
monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
||||
|
||||
else:
|
||||
monkeypatch.setenv("OMP_NUM_THREADS", "1")
|
||||
monkeypatch.delenv("VLLM_WORKER_MULTIPROC_METHOD", raising=False)
|
||||
metrics = {
|
||||
"capture_mem": {
|
||||
# Monitor key metrics of mem
|
||||
"call_counts": multiprocessing.Value("i", 0),
|
||||
"mem_before_capture": multiprocessing.Value("q", 0),
|
||||
"mem_after_capture": multiprocessing.Value("q", 0),
|
||||
},
|
||||
}
|
||||
|
||||
port = get_open_port()
|
||||
|
||||
# Create a queue to collect results from both processes
|
||||
result_queue: multiprocessing.Queue[dict] = multiprocessing.Queue()
|
||||
|
||||
# Launch workers
|
||||
workers = []
|
||||
for rank in range(cur_case["data_parallel_size"]):
|
||||
p = multiprocessing.Process(
|
||||
target=_run_worker_process,
|
||||
args=(rank, rank, cur_case["data_parallel_size"], cur_case, "127.0.0.1", port, result_queue, metrics),
|
||||
)
|
||||
p.start()
|
||||
workers.append(p)
|
||||
|
||||
all_dp_results = []
|
||||
|
||||
# get results
|
||||
for _ in range(cur_case["data_parallel_size"]):
|
||||
try:
|
||||
result = result_queue.get(timeout=180)
|
||||
all_dp_results.append(result)
|
||||
except queue.Empty:
|
||||
print("Error: Timeout waiting for worker results. A worker might have crashed.")
|
||||
break
|
||||
|
||||
# Supervision loop
|
||||
for p in workers:
|
||||
p.join(timeout=30)
|
||||
if p.exitcode != 0:
|
||||
for k in workers:
|
||||
if k.is_alive():
|
||||
k.kill()
|
||||
p.join(timeout=5)
|
||||
raise RuntimeError(f"Worker {p.pid} failed with exit code {p.exitcode}")
|
||||
_exit()
|
||||
assert len(all_dp_results) == cur_case["data_parallel_size"], f"Expected 2 results, got {len(all_dp_results)}"
|
||||
|
||||
# check graph memory
|
||||
if metrics is not None:
|
||||
check_capture_mem(metrics["capture_mem"], cur_case["baseline_capture_mem"], cur_case["capture_mem_tolerance"])
|
||||
|
||||
# check accuracy
|
||||
decode_atol = 2 * ATOL
|
||||
for result in all_dp_results:
|
||||
check_accuracy(cur_case["golden_answers"]["short"], result["short"], ATOL, decode_atol)
|
||||
check_accuracy(cur_case["golden_answers"]["long"], result["long"], ATOL, decode_atol)
|
||||
146
tests/e2e/pull_request/four_card/test_pipeline_parallel.py
Normal file
146
tests/e2e/pull_request/four_card/test_pipeline_parallel.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import DPVllmRunner, VllmRunner, wait_until_npu_memory_free
|
||||
from tests.e2e.model_utils import check_outputs_equal
|
||||
|
||||
DS3 = "deepseek-ai/DeepSeek-V2-Lite-Chat"
|
||||
MODELS = [
|
||||
DS3,
|
||||
]
|
||||
MOE_MODELS = [
|
||||
DS3,
|
||||
]
|
||||
|
||||
DATA_PARALLELS = [2]
|
||||
TENSOR_PARALLELS = [1]
|
||||
PIPELINE_PARALLELS = [2]
|
||||
DIST_EXECUTOR_BACKEND = ["mp", "ray"]
|
||||
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The future of AI is",
|
||||
]
|
||||
GOLDEN = [
|
||||
(
|
||||
[
|
||||
17464,
|
||||
11,
|
||||
601,
|
||||
1210,
|
||||
317,
|
||||
459,
|
||||
6946,
|
||||
29,
|
||||
32,
|
||||
1568,
|
||||
32092,
|
||||
535,
|
||||
6946,
|
||||
29,
|
||||
285,
|
||||
304,
|
||||
6,
|
||||
76,
|
||||
245,
|
||||
459,
|
||||
6946,
|
||||
],
|
||||
"Hello, my name is <strong>Alessandro</strong> and I'm a <strong",
|
||||
),
|
||||
(
|
||||
[
|
||||
549,
|
||||
3680,
|
||||
280,
|
||||
20838,
|
||||
317,
|
||||
6464,
|
||||
11,
|
||||
285,
|
||||
359,
|
||||
487,
|
||||
82,
|
||||
1872,
|
||||
276,
|
||||
330,
|
||||
245,
|
||||
2624,
|
||||
12,
|
||||
73309,
|
||||
279,
|
||||
254,
|
||||
1843,
|
||||
],
|
||||
"The future of AI is bright, and it’s going to be a game-changer in the world",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("tp_size", TENSOR_PARALLELS)
|
||||
@pytest.mark.parametrize("pp_size", PIPELINE_PARALLELS)
|
||||
@pytest.mark.parametrize("distributed_executor_backend", DIST_EXECUTOR_BACKEND)
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.6)
|
||||
def test_models_pp2_tp2(model: str, tp_size: int, pp_size: int, distributed_executor_backend: str) -> None:
|
||||
with VllmRunner(
|
||||
model,
|
||||
tensor_parallel_size=tp_size,
|
||||
pipeline_parallel_size=pp_size,
|
||||
compilation_config={
|
||||
"cudagraph_mode": "PIECEWISE",
|
||||
"cudagraph_capture_sizes": [1, 2, 4],
|
||||
},
|
||||
distributed_executor_backend=distributed_executor_backend,
|
||||
gpu_memory_utilization=0.7,
|
||||
enable_expert_parallel=model in MOE_MODELS,
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.generate_greedy(prompts, 16)
|
||||
check_outputs_equal(
|
||||
outputs_0_lst=outputs,
|
||||
outputs_1_lst=GOLDEN,
|
||||
name_0=f"{model}-tp{tp_size}pp{pp_size}",
|
||||
name_1="GOLDEN",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("dp_size", DATA_PARALLELS)
|
||||
@pytest.mark.parametrize("pp_size", PIPELINE_PARALLELS)
|
||||
@pytest.mark.parametrize("distributed_executor_backend", DIST_EXECUTOR_BACKEND)
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.6)
|
||||
def test_models_pp2_dp2(model: str, dp_size: int, pp_size: int, distributed_executor_backend: str) -> None:
|
||||
with DPVllmRunner(
|
||||
model,
|
||||
data_parallel_size=dp_size,
|
||||
pipeline_parallel_size=pp_size,
|
||||
compilation_config={
|
||||
"cudagraph_mode": "PIECEWISE",
|
||||
"cudagraph_capture_sizes": [1, 2, 4],
|
||||
},
|
||||
distributed_executor_backend=distributed_executor_backend,
|
||||
gpu_memory_utilization=0.7,
|
||||
enable_expert_parallel=model in MOE_MODELS,
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.generate_greedy(prompts, 16)
|
||||
check_outputs_equal(
|
||||
outputs_0_lst=outputs,
|
||||
outputs_1_lst=GOLDEN,
|
||||
name_0=f"{model}-dp{dp_size}pp{pp_size}",
|
||||
name_1="GOLDEN",
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Performance guard for profiling-based dynamic chunk sizing (PP scenario).
|
||||
|
||||
Measures Time-To-First-Token (TTFT) on 64k-token prefill requests with
|
||||
profiling_chunk_config enabled. The test runs against
|
||||
DeepSeek-V2-Lite-Chat served with PP=2, TP=2 (4 NPU cards total).
|
||||
|
||||
Test flow:
|
||||
1. Create an LLM engine with profiling_chunk_config enabled.
|
||||
2. Run NUM_WARMUP sequential requests (64k tokens, max_tokens=1) to warm
|
||||
up both the NPU and the profiling predictor.
|
||||
3. Run NUM_TEST sequential requests, recording TTFT for each.
|
||||
4. Assert that the median TTFT does not exceed BASELINE_TTFT_S seconds.
|
||||
"""
|
||||
|
||||
import os
|
||||
import statistics
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
MODEL = "Qwen/Qwen3-30B-A3B"
|
||||
|
||||
# ~64k tokens
|
||||
_WORD = "hello "
|
||||
INPUT_64K_TOKENS = _WORD * (384_000 // len(_WORD))
|
||||
|
||||
NUM_WARMUP = 5
|
||||
NUM_TEST = 5
|
||||
|
||||
# NOTE: Any changes to this baseline must be approved by team members.
|
||||
# Measured on Qwen3-30B-A3B, PP=2, TP=2, 64k prefill, profiling_chunk enabled.
|
||||
BASELINE_TTFT_S = 5.2
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"VLLM_WORKER_MULTIPROC_METHOD": "spawn",
|
||||
"VLLM_ALLOW_LONG_MAX_MODEL_LEN": "1",
|
||||
},
|
||||
)
|
||||
def test_profiling_chunk_ttft_performance() -> None:
|
||||
with VllmRunner(
|
||||
MODEL,
|
||||
max_model_len=70000,
|
||||
tensor_parallel_size=2,
|
||||
pipeline_parallel_size=2,
|
||||
block_size=128,
|
||||
enable_expert_parallel=True,
|
||||
enable_prefix_caching=False,
|
||||
gpu_memory_utilization=0.9,
|
||||
max_num_batched_tokens=12288,
|
||||
distributed_executor_backend="mp",
|
||||
enforce_eager=True,
|
||||
async_scheduling=False,
|
||||
additional_config={
|
||||
"profiling_chunk_config": {"enabled": True, "smooth_factor": 0.9},
|
||||
"enable_cpu_binding": False,
|
||||
"enable_flashcomm1": True,
|
||||
},
|
||||
hf_overrides={
|
||||
"rope_parameters": {
|
||||
"rope_type": "yarn",
|
||||
"rope_theta": 1000,
|
||||
"factor": 5,
|
||||
"original_max_position_embeddings": 262144,
|
||||
}
|
||||
},
|
||||
) as vllm_model:
|
||||
# With max_tokens=1, total latency ≈ prefill time ≈ TTFT
|
||||
prompts = [INPUT_64K_TOKENS]
|
||||
|
||||
# ── Warmup ──────────────────────────────────────────────────────────
|
||||
for _ in range(NUM_WARMUP):
|
||||
vllm_model.generate_greedy(prompts, max_tokens=1)
|
||||
|
||||
# ── Measurement ─────────────────────────────────────────────────────
|
||||
ttfts: list[float] = []
|
||||
for _ in range(NUM_TEST):
|
||||
start = time.perf_counter()
|
||||
vllm_model.generate_greedy(prompts, max_tokens=1)
|
||||
ttfts.append(time.perf_counter() - start)
|
||||
|
||||
median_ttft = statistics.median(ttfts)
|
||||
ttft_str = ", ".join(f"{t:.2f}s" for t in ttfts)
|
||||
print(
|
||||
f"\n[profiling_chunk perf] TTFT per request: [{ttft_str}]"
|
||||
f"\n[profiling_chunk perf] Median TTFT: {median_ttft:.2f}s "
|
||||
f"(baseline: {BASELINE_TTFT_S}s)"
|
||||
)
|
||||
|
||||
assert median_ttft <= BASELINE_TTFT_S, (
|
||||
f"TTFT performance regression: median TTFT {median_ttft:.2f}s "
|
||||
f"exceeds baseline {BASELINE_TTFT_S}s. "
|
||||
f"Individual TTFTs: [{ttft_str}]"
|
||||
)
|
||||
70
tests/e2e/pull_request/four_card/test_qwen3_5.py
Normal file
70
tests/e2e/pull_request/four_card/test_qwen3_5.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.e2e.conftest import DPVllmRunner, VllmRunner
|
||||
|
||||
|
||||
def test_qwen3_5_27b_distributed_mp_tp4():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
] * 4
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3.5-27B",
|
||||
tensor_parallel_size=4,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
max_model_len=4096,
|
||||
gpu_memory_utilization=0.90,
|
||||
distributed_executor_backend="mp",
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
del vllm_model
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1"})
|
||||
def test_qwen3_5_35b_distributed_mp_tp4_full_decode_only_mtp3_flashcomm():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
max_tokens = 20
|
||||
with DPVllmRunner(
|
||||
"Qwen/Qwen3.5-35B-A3B",
|
||||
data_parallel_size=2,
|
||||
tensor_parallel_size=2,
|
||||
enable_expert_parallel=True,
|
||||
max_model_len=4096,
|
||||
gpu_memory_utilization=0.90,
|
||||
distributed_executor_backend="mp",
|
||||
compilation_config={
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
"cudagraph_capture_sizes": [4, 8, 12, 16],
|
||||
},
|
||||
speculative_config={
|
||||
"method": "qwen3_5_mtp",
|
||||
"num_speculative_tokens": 3,
|
||||
},
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
del vllm_model
|
||||
80
tests/e2e/pull_request/four_card/test_qwen3_next.py
Normal file
80
tests/e2e/pull_request/four_card/test_qwen3_next.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
|
||||
def test_qwen3_next_distributed_mp_full_decode_only_tp4():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
] * 4
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-Next-80B-A3B-Instruct",
|
||||
tensor_parallel_size=4,
|
||||
max_model_len=4096,
|
||||
gpu_memory_utilization=0.8,
|
||||
distributed_executor_backend="mp",
|
||||
compilation_config={"cudagraph_mode": "FULL_DECODE_ONLY", "cudagraph_capture_sizes": [1, 2, 4]},
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
del vllm_model
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1"})
|
||||
@patch.dict(os.environ, {"HCCL_BUFFSIZE": "1024"})
|
||||
def test_qwen3_next_w8a8dynamic_distributed_mp_flash_comm_tp4():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
] * 4
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"vllm-ascend/Qwen3-Next-80B-A3B-Instruct-W8A8",
|
||||
tensor_parallel_size=4,
|
||||
max_model_len=4096,
|
||||
gpu_memory_utilization=0.7,
|
||||
distributed_executor_backend="mp",
|
||||
enable_expert_parallel=True,
|
||||
enforce_eager=True,
|
||||
quantization="ascend",
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
del vllm_model
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"HCCL_BUFFSIZE": "1024"})
|
||||
def test_qwen3_next_distributed_mp_graph_mode_tp4():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
] * 4
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-Next-80B-A3B-Instruct",
|
||||
tensor_parallel_size=4,
|
||||
max_model_len=4096,
|
||||
gpu_memory_utilization=0.8,
|
||||
distributed_executor_backend="mp",
|
||||
enable_expert_parallel=True,
|
||||
cudagraph_capture_sizes=[1, 2, 4],
|
||||
enforce_eager=False,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
del vllm_model
|
||||
0
tests/e2e/pull_request/one_card/_310p/__init__.py
Normal file
0
tests/e2e/pull_request/one_card/_310p/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import pytest
|
||||
import torch
|
||||
from modelscope import snapshot_download # type: ignore[import-untyped]
|
||||
from transformers import AutoModelForSequenceClassification
|
||||
|
||||
from tests.e2e.conftest import HfRunner, VllmRunner
|
||||
|
||||
|
||||
@pytest.mark.skip("Probabilistic failure, need fix")
|
||||
def test_qwen_pooling_classify_correctness() -> None:
|
||||
model_name = snapshot_download("Howeee/Qwen2.5-1.5B-apeach")
|
||||
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is what",
|
||||
]
|
||||
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
runner="pooling",
|
||||
max_model_len=1024,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
gpu_memory_utilization=0.6,
|
||||
) as vllm_runner:
|
||||
vllm_outputs = vllm_runner.classify(prompts)
|
||||
|
||||
with HfRunner(
|
||||
model_name,
|
||||
dtype="float16",
|
||||
model_kwargs={"attn_implementation": "eager"},
|
||||
auto_cls=AutoModelForSequenceClassification,
|
||||
) as hf_runner:
|
||||
hf_outputs = hf_runner.classify(prompts)
|
||||
|
||||
for hf_output, vllm_output in zip(hf_outputs, vllm_outputs):
|
||||
hf_output = torch.tensor(hf_output)
|
||||
vllm_output = torch.tensor(vllm_output)
|
||||
assert torch.allclose(hf_output, vllm_output, 1e-2)
|
||||
173
tests/e2e/pull_request/one_card/_310p/test_dense_model_310p.py
Normal file
173
tests/e2e/pull_request/one_card/_310p/test_dense_model_310p.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
|
||||
|
||||
from tests.e2e.conftest import VllmRunner, wait_until_npu_memory_free
|
||||
from tests.e2e.model_utils import check_outputs_equal
|
||||
|
||||
QWEN3_5_PREFIX_MAMBA_PROMPT = (
|
||||
"You are reading a compact synthetic operations ledger. "
|
||||
"Use only the rows below when answering the final question.\n"
|
||||
+ "\n".join(
|
||||
f"Row {i}: route R{i:03d} moves cargo from zone {i % 11} to zone {(i * 7) % 13}; priority is {i % 5}."
|
||||
for i in range(64)
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
QWEN3_5_PREFIX_MAMBA_PROMPTS = [
|
||||
QWEN3_5_PREFIX_MAMBA_PROMPT + "Question: What route is listed in row 17? Answer briefly.",
|
||||
QWEN3_5_PREFIX_MAMBA_PROMPT + "Question: What priority is listed in row 42? Answer briefly.",
|
||||
]
|
||||
|
||||
|
||||
def _generate_qwen3_5_prefix_mamba_outputs(enable_prefix_caching: bool) -> list[tuple[list[int], str]]:
|
||||
outputs: list[tuple[list[int], str]] = []
|
||||
|
||||
if enable_prefix_caching:
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3.5-4B",
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
max_model_len=2048,
|
||||
max_num_batched_tokens=2048,
|
||||
enable_prefix_caching=True,
|
||||
mamba_cache_mode="align",
|
||||
mamba_ssm_cache_dtype="float16",
|
||||
) as vllm_model:
|
||||
for prompt in QWEN3_5_PREFIX_MAMBA_PROMPTS:
|
||||
outputs.extend(vllm_model.generate_greedy([prompt], max_tokens=8))
|
||||
else:
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3.5-4B",
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
max_model_len=2048,
|
||||
max_num_batched_tokens=2048,
|
||||
enable_prefix_caching=False,
|
||||
mamba_ssm_cache_dtype="float16",
|
||||
) as vllm_model:
|
||||
for prompt in QWEN3_5_PREFIX_MAMBA_PROMPTS:
|
||||
outputs.extend(vllm_model.generate_greedy([prompt], max_tokens=8))
|
||||
return outputs
|
||||
|
||||
|
||||
def test_qwen3_dense_tp1_fp16():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-8B",
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
max_model_len=16384,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
|
||||
@wait_until_npu_memory_free(0.7)
|
||||
def test_qwen3_dense_tp1_fp16_aclgraph():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
] * 8
|
||||
max_tokens = 2
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-8B",
|
||||
tensor_parallel_size=1,
|
||||
dtype="float16",
|
||||
max_num_seqs=16,
|
||||
max_model_len=16384,
|
||||
gpu_memory_utilization=0.80,
|
||||
additional_config={"ascend_compilation_config": {"fuse_norm_quant": False}},
|
||||
compilation_config={
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
"cudagraph_capture_sizes": [1, 2, 4, 8, 16],
|
||||
},
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
|
||||
def test_qwen3_dense_tp1_w8a8():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"vllm-ascend/Qwen3-8B-W8A8",
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
quantization="ascend",
|
||||
max_model_len=16384,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
|
||||
def test_qwen3_5_dense_tp1_fp16():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3.5-4B",
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
max_model_len=16384,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
|
||||
@wait_until_npu_memory_free(0.7)
|
||||
def test_qwen3_5_dense_prefix_mamba_cache_tp1_fp16():
|
||||
prefix_cache_outputs = _generate_qwen3_5_prefix_mamba_outputs(enable_prefix_caching=True)
|
||||
no_prefix_cache_outputs = _generate_qwen3_5_prefix_mamba_outputs(enable_prefix_caching=False)
|
||||
|
||||
assert len(prefix_cache_outputs) == len(no_prefix_cache_outputs) == len(QWEN3_5_PREFIX_MAMBA_PROMPTS)
|
||||
check_outputs_equal(
|
||||
outputs_0_lst=no_prefix_cache_outputs,
|
||||
outputs_1_lst=prefix_cache_outputs,
|
||||
name_0="no_prefix_cache_outputs",
|
||||
name_1="prefix_cache_outputs",
|
||||
)
|
||||
|
||||
|
||||
@wait_until_npu_memory_free(0.7)
|
||||
def test_qwen3_5_dense_tp1_fp16_aclgraph():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
] * 8
|
||||
max_tokens = 2
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3.5-4B",
|
||||
tensor_parallel_size=1,
|
||||
dtype="float16",
|
||||
max_num_seqs=16,
|
||||
max_model_len=16384,
|
||||
gpu_memory_utilization=0.80,
|
||||
additional_config={"ascend_compilation_config": {"fuse_norm_quant": False}},
|
||||
compilation_config={
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
"cudagraph_capture_sizes": [1, 2, 4, 8, 16],
|
||||
},
|
||||
mamba_ssm_cache_dtype="float16",
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
106
tests/e2e/pull_request/one_card/_310p/test_embedding_310p.py
Normal file
106
tests/e2e/pull_request/one_card/_310p/test_embedding_310p.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
import pytest
|
||||
from modelscope import snapshot_download # type: ignore[import-untyped]
|
||||
|
||||
from tests.e2e.conftest import HfRunner, VllmRunner
|
||||
from tests.e2e.utils import check_embeddings_close
|
||||
|
||||
MODELS = [
|
||||
"Qwen/Qwen3-Embedding-0.6B", # lasttoken
|
||||
"intfloat/multilingual-e5-small", # mean_tokens
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
def test_embed_models_correctness(model: str):
|
||||
queries = ["What is the capital of China?", "Explain gravity"]
|
||||
|
||||
model_name = snapshot_download(model)
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
runner="pooling",
|
||||
max_model_len=512,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
gpu_memory_utilization=0.6,
|
||||
) as vllm_runner:
|
||||
vllm_outputs = vllm_runner.embed(queries)
|
||||
|
||||
with HfRunner(
|
||||
model_name,
|
||||
dtype="float16",
|
||||
is_sentence_transformer=True,
|
||||
) as hf_runner:
|
||||
hf_outputs = hf_runner.encode(queries)
|
||||
|
||||
check_embeddings_close(
|
||||
embeddings_0_lst=hf_outputs,
|
||||
embeddings_1_lst=vllm_outputs,
|
||||
name_0="hf",
|
||||
name_1="vllm",
|
||||
tol=1e-2,
|
||||
)
|
||||
|
||||
|
||||
def test_bge_m3_correctness():
|
||||
queries = ["What is the capital of China?", "Explain gravity"]
|
||||
|
||||
model_name = snapshot_download("BAAI/bge-m3")
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
runner="pooling",
|
||||
max_model_len=1024,
|
||||
dtype="float16",
|
||||
cudagraph_capture_sizes=[512, 1024],
|
||||
additional_config={"ascend_compilation_config": {"fuse_norm_quant": False}},
|
||||
) as vllm_aclgraph_runner:
|
||||
vllm_aclgraph_outputs = vllm_aclgraph_runner.embed(queries)
|
||||
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
runner="pooling",
|
||||
max_model_len=1024,
|
||||
dtype="float16",
|
||||
enforce_eager=True,
|
||||
) as vllm_runner:
|
||||
vllm_eager_outputs = vllm_runner.embed(queries)
|
||||
|
||||
with HfRunner(
|
||||
model_name,
|
||||
dtype="float16",
|
||||
is_sentence_transformer=True,
|
||||
) as hf_runner:
|
||||
hf_outputs = hf_runner.encode(queries)
|
||||
|
||||
check_embeddings_close(
|
||||
embeddings_0_lst=hf_outputs,
|
||||
embeddings_1_lst=vllm_eager_outputs,
|
||||
name_0="hf",
|
||||
name_1="vllm",
|
||||
tol=1e-2,
|
||||
)
|
||||
|
||||
check_embeddings_close(
|
||||
embeddings_0_lst=vllm_eager_outputs,
|
||||
embeddings_1_lst=vllm_aclgraph_outputs,
|
||||
name_0="eager",
|
||||
name_1="aclgraph",
|
||||
tol=1e-2,
|
||||
)
|
||||
87
tests/e2e/pull_request/one_card/_310p/test_scoring_310p.py
Normal file
87
tests/e2e/pull_request/one_card/_310p/test_scoring_310p.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import pytest
|
||||
from modelscope import snapshot_download # type: ignore[import-untyped]
|
||||
|
||||
from tests.e2e.conftest import HfRunner, VllmRunner
|
||||
|
||||
CROSS_ENCODER_MODELS = [
|
||||
"BAAI/bge-reranker-v2-m3", # Roberta
|
||||
]
|
||||
|
||||
|
||||
TEXTS_1 = [
|
||||
"What is the capital of France?",
|
||||
"What is the capital of Germany?",
|
||||
]
|
||||
|
||||
TEXTS_2 = [
|
||||
"The capital of France is Paris.",
|
||||
"The capital of Germany is Berlin.",
|
||||
]
|
||||
|
||||
DTYPE = "float16"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=CROSS_ENCODER_MODELS)
|
||||
def model_name(request):
|
||||
yield snapshot_download(request.param)
|
||||
|
||||
|
||||
def test_cross_encoder_score_1_to_1(model_name):
|
||||
text_pair = [TEXTS_1[0], TEXTS_2[0]]
|
||||
|
||||
with HfRunner(model_name, dtype=DTYPE, is_cross_encoder=True) as hf_model:
|
||||
hf_outputs = hf_model.predict([text_pair]).tolist()
|
||||
|
||||
with VllmRunner(
|
||||
model_name, runner="pooling", dtype=DTYPE, max_model_len=1024, enforce_eager=True, gpu_memory_utilization=0.6
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.score(text_pair[0], text_pair[1])
|
||||
|
||||
assert len(vllm_outputs) == 1
|
||||
assert len(hf_outputs) == 1
|
||||
|
||||
assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.01)
|
||||
|
||||
|
||||
def test_cross_encoder_score_1_to_N(model_name):
|
||||
text_pairs = [
|
||||
[TEXTS_1[0], TEXTS_2[0]],
|
||||
[TEXTS_1[0], TEXTS_2[1]],
|
||||
]
|
||||
|
||||
with HfRunner(model_name, dtype=DTYPE, is_cross_encoder=True) as hf_model:
|
||||
hf_outputs = hf_model.predict(text_pairs).tolist()
|
||||
|
||||
with VllmRunner(
|
||||
model_name, runner="pooling", dtype=DTYPE, max_model_len=1024, enforce_eager=True, gpu_memory_utilization=0.6
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.score(TEXTS_1[0], TEXTS_2)
|
||||
|
||||
assert len(vllm_outputs) == 2
|
||||
assert len(hf_outputs) == 2
|
||||
|
||||
assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.01)
|
||||
assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.01)
|
||||
|
||||
|
||||
def test_cross_encoder_score_N_to_N(model_name):
|
||||
text_pairs = [
|
||||
[TEXTS_1[0], TEXTS_2[0]],
|
||||
[TEXTS_1[1], TEXTS_2[1]],
|
||||
]
|
||||
|
||||
with HfRunner(model_name, dtype=DTYPE, is_cross_encoder=True) as hf_model:
|
||||
hf_outputs = hf_model.predict(text_pairs).tolist()
|
||||
|
||||
with VllmRunner(
|
||||
model_name, runner="pooling", dtype=DTYPE, max_model_len=1024, enforce_eager=True, gpu_memory_utilization=0.6
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.score(TEXTS_1, TEXTS_2)
|
||||
|
||||
assert len(vllm_outputs) == 2
|
||||
assert len(hf_outputs) == 2
|
||||
|
||||
assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.01)
|
||||
assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.01)
|
||||
@@ -0,0 +1,35 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
|
||||
def test_qwen3_5_mtp_tp1_eager():
|
||||
example_prompts = ["Hello, my name is"]
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3.5-4B",
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
dtype="float16",
|
||||
max_model_len=2048,
|
||||
mamba_ssm_cache_dtype="float16",
|
||||
speculative_config={
|
||||
"method": "qwen3_5_mtp",
|
||||
"num_speculative_tokens": 1,
|
||||
},
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens=8)
|
||||
34
tests/e2e/pull_request/one_card/_310p/test_vl_model_310p.py
Normal file
34
tests/e2e/pull_request/one_card/_310p/test_vl_model_310p.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from tests.e2e.conftest import wait_until_npu_memory_free
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
full_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
sys.path.insert(0, full_dir)
|
||||
|
||||
# ruff: noqa: E402
|
||||
from tests.e2e.pull_request.utils_310p import run_vl_model_test
|
||||
|
||||
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.7)
|
||||
def test_qwen3_vl_8b_tp1_fp16():
|
||||
"""Qwen3-VL-8B single-card FP16 test"""
|
||||
run_vl_model_test(model_name="Qwen/Qwen3-VL-8B-Instruct", tensor_parallel_size=1, max_tokens=5)
|
||||
0
tests/e2e/pull_request/one_card/__init__.py
Normal file
0
tests/e2e/pull_request/one_card/__init__.py
Normal file
0
tests/e2e/pull_request/one_card/compile/__init__.py
Normal file
0
tests/e2e/pull_request/one_card/compile/__init__.py
Normal file
127
tests/e2e/pull_request/one_card/compile/backend.py
Normal file
127
tests/e2e/pull_request/one_card/compile/backend.py
Normal file
@@ -0,0 +1,127 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from collections.abc import Callable, Sequence
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
import torch.fx as fx
|
||||
from torch._inductor.decomposition import select_decomp_table
|
||||
from vllm.compilation.passes.fx_utils import OpOverload
|
||||
from vllm.config import get_current_vllm_config
|
||||
|
||||
from vllm_ascend.compilation.compiler_interface import compile_fx
|
||||
|
||||
|
||||
class TestBackend:
|
||||
"""
|
||||
A custom compilation backend for testing operator fusion passes.
|
||||
It applies the AddRMSNormQuantFusionPass during graph compilation and
|
||||
records the FX graph before and after the transformation.
|
||||
"""
|
||||
|
||||
def __init__(self, custom_passes: list[Any] | None = None):
|
||||
vllm_config = get_current_vllm_config()
|
||||
compile_config = vllm_config.compilation_config
|
||||
self.inductor_config = compile_config.inductor_compile_config
|
||||
self.inductor_config["graph_fusion_manager"] = self.post_pass
|
||||
self.custom_passes = custom_passes
|
||||
|
||||
# Placeholders to store FX graphs for verification
|
||||
self.graph_pre_pass = None
|
||||
self.graph_post_pass = None
|
||||
|
||||
def post_pass(self, graph: fx.Graph, runtime_shape: int | None = None) -> fx.Graph:
|
||||
"""
|
||||
Apply custom graph transformation passes.
|
||||
"""
|
||||
self.graph_pre_pass = deepcopy(graph)
|
||||
if self.custom_passes is not None:
|
||||
for pass_ in self.custom_passes:
|
||||
pass_(graph)
|
||||
self.graph_post_pass = deepcopy(graph)
|
||||
return graph
|
||||
|
||||
def compile(
|
||||
self,
|
||||
graph: fx.GraphModule,
|
||||
example_inputs: list[Any],
|
||||
compiler_config: dict[str, Any],
|
||||
runtime_shape: int | None = None,
|
||||
key: str | None = None,
|
||||
) -> tuple[Callable | None, Any | None]:
|
||||
"""
|
||||
Compile the FX graph using vLLM's Ascend compiler interface.
|
||||
Wraps the post-pass logic into the inner_compile callback.
|
||||
"""
|
||||
|
||||
def compile_inner(graph, example_inputs):
|
||||
current_pass_manager = compiler_config["graph_fusion_manager"]
|
||||
return current_pass_manager(graph, runtime_shape)
|
||||
|
||||
decompositions = select_decomp_table()
|
||||
compiled_fn = compile_fx(
|
||||
graph=graph,
|
||||
example_inputs=example_inputs,
|
||||
inner_compile=compile_inner,
|
||||
decompositions=decompositions,
|
||||
)
|
||||
return compiled_fn, None
|
||||
|
||||
def __call__(self, gm: fx.GraphModule, example_inputs: list[Any] | None):
|
||||
"""
|
||||
Make the backend callable by torch.compile().
|
||||
Returns a compiled executable function.
|
||||
"""
|
||||
assert example_inputs is not None
|
||||
compiled_fn, _ = self.compile(
|
||||
gm,
|
||||
example_inputs,
|
||||
compiler_config={"graph_fusion_manager": self.post_pass},
|
||||
runtime_shape=None,
|
||||
key=None,
|
||||
)
|
||||
return compiled_fn
|
||||
|
||||
def find_nodes_by_target(self, graph: fx.GraphModule, target: OpOverload) -> list[fx.Node]:
|
||||
"""Helper to find all FX nodes that call a specific operator."""
|
||||
return [node for node in graph.graph.nodes if hasattr(node, "target") and node.target == target]
|
||||
|
||||
def op_count(self, op: OpOverload, before: bool = False) -> int:
|
||||
"""Return the number of nodes that call the given operator."""
|
||||
graph = self.graph_pre_pass if before else self.graph_post_pass
|
||||
return len(self.find_nodes_by_target(graph, op))
|
||||
|
||||
def check_before_ops(self, ops: Sequence[OpOverload], fully_replaced: bool = True):
|
||||
"""
|
||||
Verify that the original (unfused) operators exist before the pass
|
||||
and are fully removed afterward (if fully_replaced=True).
|
||||
"""
|
||||
for op in ops:
|
||||
num_pre = len(self.find_nodes_by_target(self.graph_pre_pass, op))
|
||||
num_post = len(self.find_nodes_by_target(self.graph_post_pass, op))
|
||||
print(f"Op {op}: pre={num_pre}, post={num_post}")
|
||||
|
||||
assert num_pre > 0, f"Op {op} not found in pre-pass graph"
|
||||
if fully_replaced:
|
||||
assert num_post == 0, f"Unexpected op {op} in post-pass graph: {num_post} nodes remain"
|
||||
|
||||
def check_after_ops(self, ops: Sequence[OpOverload]):
|
||||
"""Verify that the fused operator appears in the transformed graph."""
|
||||
for op in ops:
|
||||
num_post = len(self.find_nodes_by_target(self.graph_post_pass, op))
|
||||
print(f"Op {op}: post={num_post}")
|
||||
assert num_post > 0, f"Op {op} not found in post-pass graph"
|
||||
@@ -0,0 +1,309 @@
|
||||
import copy
|
||||
|
||||
import npugraph_ex as nge
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch_npu
|
||||
import vllm.config
|
||||
from vllm.config import ModelConfig, VllmConfig
|
||||
from vllm.distributed import ensure_model_parallel_initialized, init_distributed_environment
|
||||
from vllm.utils.system_utils import update_environment_variables
|
||||
|
||||
from vllm_ascend.ascend_forward_context import set_ascend_forward_context
|
||||
from vllm_ascend.compilation.passes.norm_quant_fusion_pass import (
|
||||
AddRMSNormQuantPattern,
|
||||
AddRMSNormQuantPatternWithBias,
|
||||
AddRMSNormQuantSPPattern,
|
||||
AddRMSNormQuantSPPatternWithBias,
|
||||
)
|
||||
from vllm_ascend.utils import enable_custom_op
|
||||
|
||||
|
||||
def find_op(gm, op_default):
|
||||
return any(node.op == "call_function" and node.target == op_default for node in gm.graph.nodes)
|
||||
|
||||
|
||||
def create_pattern_wrapper(assert_func):
|
||||
original_func = nge.npu_fx_compiler._optimize_fx
|
||||
|
||||
def wrapper(gm, example_inputs=None, config=None):
|
||||
ret = original_func(gm, example_inputs, config)
|
||||
graph_after = copy.deepcopy(gm)
|
||||
assert_func(graph_after)
|
||||
return ret
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class ModelWithoutBias(nn.Module):
|
||||
"""
|
||||
A minimal test model that simulates the pattern:
|
||||
AddRMSNorm → Quantization (without bias)
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size: int, dtype: torch.bfloat16, eps: float = 1e-6, device="npu"):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.eps = eps
|
||||
self.rms_norm_weight = nn.Parameter(torch.randn(hidden_size, dtype=dtype, device=device))
|
||||
self.quant_scale = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_scale_reciprocal = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_offset = torch.zeros(hidden_size, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
"""
|
||||
Forward pass:
|
||||
1. Perform npu_add_rms_norm
|
||||
2. Quantize the normalized output to int8
|
||||
Returns both quantized output and updated residual.
|
||||
"""
|
||||
residual = torch.zeros_like(x)
|
||||
|
||||
norm_output, _, new_residual = torch_npu.npu_add_rms_norm(x, residual, self.rms_norm_weight, self.eps)
|
||||
|
||||
quantized_output = torch.ops.vllm.quantize(
|
||||
norm_output, self.quant_scale, self.quant_scale_reciprocal, self.quant_offset
|
||||
)
|
||||
|
||||
return quantized_output, new_residual
|
||||
|
||||
|
||||
class ModelWithBias(nn.Module):
|
||||
"""
|
||||
A test model that simulates the pattern:
|
||||
AddRMSNorm → Add Bias → Quantization (with bias)
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size: int, dtype: torch.bfloat16, eps: float = 1e-6, device="npu"):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.eps = eps
|
||||
self.rms_norm_weight = nn.Parameter(torch.randn(hidden_size, dtype=dtype, device=device))
|
||||
self.bias = nn.Parameter(torch.randn(hidden_size, dtype=dtype, device=device))
|
||||
self.quant_scale = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_scale_reciprocal = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_offset = torch.zeros(hidden_size, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
"""
|
||||
Forward pass:
|
||||
1. Perform npu_add_rms_norm
|
||||
2. Add bias
|
||||
3. Quantize to int8
|
||||
Returns both quantized output and updated residual.
|
||||
"""
|
||||
residual = torch.zeros_like(x)
|
||||
|
||||
norm_output, _, new_residual = torch_npu.npu_add_rms_norm(x, residual, self.rms_norm_weight, self.eps)
|
||||
|
||||
# Add bias
|
||||
norm_output_with_bias = norm_output + self.bias
|
||||
|
||||
quantized_output = torch.ops.vllm.quantize(
|
||||
norm_output_with_bias, self.quant_scale, self.quant_scale_reciprocal, self.quant_offset
|
||||
)
|
||||
|
||||
return quantized_output, new_residual
|
||||
|
||||
|
||||
class ModelSPWithoutBias(nn.Module):
|
||||
"""
|
||||
A minimal test model that simulates the pattern:
|
||||
AddRMSNorm → maybe_allgather → Quantization (without bias)
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size: int, dtype: torch.bfloat16, eps: float = 1e-6, device="npu"):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.eps = eps
|
||||
self.rms_norm_weight = nn.Parameter(torch.randn(hidden_size, dtype=dtype, device=device))
|
||||
self.quant_scale = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_scale_reciprocal = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_offset = torch.zeros(hidden_size, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
"""
|
||||
Forward pass:
|
||||
1. Perform npu_add_rms_norm
|
||||
2. Perform a fake maybe_all_gather_and_maybe_unpad
|
||||
3. Quantize the normalized output to int8
|
||||
Returns both quantized output and updated residual.
|
||||
"""
|
||||
residual = torch.zeros_like(x)
|
||||
|
||||
norm_output, _, new_residual = torch_npu.npu_add_rms_norm(x, residual, self.rms_norm_weight, self.eps)
|
||||
|
||||
norm_output = torch.ops.vllm.maybe_all_gather_and_maybe_unpad(norm_output, True)
|
||||
|
||||
quantized_output = torch.ops.vllm.quantize(
|
||||
norm_output, self.quant_scale, self.quant_scale_reciprocal, self.quant_offset
|
||||
)
|
||||
|
||||
return quantized_output, new_residual
|
||||
|
||||
|
||||
class ModelSPWithBias(nn.Module):
|
||||
"""
|
||||
A minimal test model that simulates the pattern:
|
||||
AddRMSNorm → Add bias → maybe_allgather → Quantization (without bias)
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size: int, dtype: torch.bfloat16, eps: float = 1e-6, device="npu"):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.eps = eps
|
||||
self.rms_norm_weight = nn.Parameter(torch.randn(hidden_size, dtype=dtype, device=device))
|
||||
self.bias = nn.Parameter(torch.randn(hidden_size, dtype=dtype, device=device))
|
||||
self.quant_scale = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_scale_reciprocal = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_offset = torch.zeros(hidden_size, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
"""
|
||||
Forward pass:
|
||||
1. Perform npu_add_rms_norm
|
||||
2. Add bias
|
||||
3. Perform a fake maybe_all_gather_and_maybe_unpad
|
||||
4. Quantize the normalized output to int8
|
||||
Returns both quantized output and updated residual.
|
||||
"""
|
||||
residual = torch.zeros_like(x)
|
||||
|
||||
norm_output, _, new_residual = torch_npu.npu_add_rms_norm(x, residual, self.rms_norm_weight, self.eps)
|
||||
|
||||
# Add bias
|
||||
norm_output_with_bias = norm_output + self.bias
|
||||
|
||||
norm_output_with_bias = torch.ops.vllm.maybe_all_gather_and_maybe_unpad(norm_output_with_bias, True)
|
||||
|
||||
quantized_output = torch.ops.vllm.quantize(
|
||||
norm_output_with_bias, self.quant_scale, self.quant_scale_reciprocal, self.quant_offset
|
||||
)
|
||||
|
||||
return quantized_output, new_residual
|
||||
|
||||
|
||||
def assert_addrmsnorm_quant(after_gm, expect_fused=True, use_bias=False, sp_enable=False):
|
||||
check_rules = [
|
||||
(torch.ops.npu.npu_add_rms_norm_quant.default, expect_fused),
|
||||
(torch.ops.npu.npu_add_rms_norm.default, not expect_fused),
|
||||
(torch.ops.npu.npu_quantize.default, not expect_fused),
|
||||
]
|
||||
if use_bias:
|
||||
check_rules.append((torch.ops.aten.add.Tensor, not expect_fused))
|
||||
if sp_enable:
|
||||
check_rules.append((torch.ops.vllm.maybe_all_gather_and_maybe_unpad.default, expect_fused))
|
||||
for torch_op, expect_exist in check_rules:
|
||||
found = find_op(after_gm, torch_op)
|
||||
if expect_exist:
|
||||
assert found, f"Expected operator '{torch_op}' but not find"
|
||||
else:
|
||||
assert not found, f"Not expected operator '{torch_op}' but find"
|
||||
|
||||
|
||||
_registered_patterns = set()
|
||||
|
||||
|
||||
def register_pattern_safe(pattern_class, vllm_config, eps, pattern_key):
|
||||
global _registered_patterns
|
||||
if pattern_key in _registered_patterns:
|
||||
print(f"Pattern {pattern_key} already registered, skipping...")
|
||||
return None
|
||||
|
||||
pattern = pattern_class(vllm_config=vllm_config, eps=eps)
|
||||
try:
|
||||
# Import the required pass class
|
||||
from torch._inductor.pattern_matcher import PatternMatcherPass
|
||||
|
||||
pm_pass = PatternMatcherPass()
|
||||
pattern.register(pm_pass)
|
||||
_registered_patterns.add(pattern_key)
|
||||
print(f"Successfully registered pattern: {pattern_key}")
|
||||
except RuntimeError as e:
|
||||
if "Duplicate pattern" in str(e):
|
||||
print(f"Pattern {pattern_key} already exists (caught from RuntimeError), skipping...")
|
||||
_registered_patterns.add(pattern_key)
|
||||
else:
|
||||
raise e
|
||||
return pattern
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("hidden_size", [64])
|
||||
@pytest.mark.parametrize("num_tokens", [257])
|
||||
@pytest.mark.parametrize("eps", [1e-5])
|
||||
@pytest.mark.parametrize("use_bias", [False, True])
|
||||
@pytest.mark.parametrize("sp_enable", [False, True])
|
||||
def test_rmsnorm_quant_fusion(
|
||||
dtype: torch.dtype,
|
||||
hidden_size: int,
|
||||
num_tokens: int,
|
||||
eps: float,
|
||||
use_bias: bool,
|
||||
sp_enable: bool,
|
||||
):
|
||||
# Check if fusion operator is available
|
||||
if not hasattr(torch.ops.npu, "npu_add_rms_norm_quant"):
|
||||
pytest.skip("Fusion operator npu_add_rms_norm_quant not available, skipping test")
|
||||
|
||||
vllm_config = VllmConfig(model_config=ModelConfig(dtype=dtype))
|
||||
with vllm.config.set_current_vllm_config(vllm_config):
|
||||
update_environment_variables(
|
||||
{
|
||||
"RANK": "0",
|
||||
"LOCAL_RANK": "0",
|
||||
"WORLD_SIZE": "1",
|
||||
"MASTER_ADDR": "localhost",
|
||||
"MASTER_PORT": "12345",
|
||||
}
|
||||
)
|
||||
init_distributed_environment()
|
||||
ensure_model_parallel_initialized(1, 1)
|
||||
|
||||
with vllm.config.set_current_vllm_config(vllm_config), set_ascend_forward_context(None, vllm_config):
|
||||
if use_bias:
|
||||
# Skip test if custom ops are not available
|
||||
if not enable_custom_op():
|
||||
pytest.skip("Custom ops not available, skipping bias test")
|
||||
# Check if the bias operator exists
|
||||
if not hasattr(torch.ops._C_ascend, "npu_add_rms_norm_bias"):
|
||||
pytest.skip("Operator npu_add_rms_norm_bias not available, skipping bias test")
|
||||
if sp_enable:
|
||||
model = ModelSPWithBias(hidden_size, dtype, eps, device="npu")
|
||||
register_pattern_safe(
|
||||
AddRMSNormQuantSPPatternWithBias, vllm_config, eps, "GraphEXAddRMSNormQuantSPPatternWithBias"
|
||||
)
|
||||
else:
|
||||
model = ModelWithBias(hidden_size, dtype, eps, device="npu")
|
||||
register_pattern_safe(
|
||||
AddRMSNormQuantPatternWithBias, vllm_config, eps, "GraphEXAddRMSNormQuantPatternWithBias"
|
||||
)
|
||||
else:
|
||||
# The non-bias patterns currently use npu_add_rms_norm_bias in their pattern matching
|
||||
# so we need to skip if it's not available
|
||||
if not hasattr(torch.ops._C_ascend, "npu_add_rms_norm_bias"):
|
||||
pytest.skip("Operator npu_add_rms_norm_bias not available, skipping test")
|
||||
if sp_enable:
|
||||
model = ModelSPWithoutBias(hidden_size, dtype, eps, device="npu")
|
||||
register_pattern_safe(AddRMSNormQuantSPPattern, vllm_config, eps, "GraphEXAddRMSNormQuantSPPattern")
|
||||
else:
|
||||
model = ModelWithoutBias(hidden_size, dtype, eps, device="npu")
|
||||
register_pattern_safe(AddRMSNormQuantPattern, vllm_config, eps, "GraphEXAddRMSNormQuantPattern")
|
||||
|
||||
model = model.to("npu")
|
||||
x = torch.randn(num_tokens, hidden_size, device="npu", dtype=dtype, requires_grad=False)
|
||||
|
||||
with torch.no_grad():
|
||||
# Don't expect fusion since patterns are not properly integrated into the compilation pipeline
|
||||
# Just test that the model compiles and runs without errors
|
||||
compiled_model = torch.compile(model, backend="npugraph_ex", fullgraph=True, dynamic=True)
|
||||
compiled_out, compiled_res = compiled_model(x)
|
||||
|
||||
# Verify output shapes are correct
|
||||
assert compiled_out.shape == (num_tokens, hidden_size), (
|
||||
f"Expected shape {(num_tokens, hidden_size)}, got {compiled_out.shape}"
|
||||
)
|
||||
assert compiled_res.shape == (num_tokens, hidden_size), (
|
||||
f"Expected shape {(num_tokens, hidden_size)}, got {compiled_res.shape}"
|
||||
)
|
||||
@@ -0,0 +1,228 @@
|
||||
import copy
|
||||
|
||||
import npugraph_ex as nge
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import vllm.config
|
||||
from vllm.config import ModelConfig, VllmConfig
|
||||
from vllm.distributed import ensure_model_parallel_initialized, init_distributed_environment
|
||||
from vllm.utils.system_utils import update_environment_variables
|
||||
|
||||
from vllm_ascend.ascend_forward_context import set_ascend_forward_context
|
||||
from vllm_ascend.compilation.passes.qknorm_rope_fusion_pass import (
|
||||
QKNormRopeFusionPattern,
|
||||
QKNormRopeFusionPatternWithBias,
|
||||
)
|
||||
from vllm_ascend.ops.triton.triton_utils import init_device_properties_triton
|
||||
|
||||
MAX_POSITION_EMBEDDING = 262144
|
||||
|
||||
|
||||
def find_op(gm, op_default):
|
||||
return any(node.op == "call_function" and node.target == op_default for node in gm.graph.nodes)
|
||||
|
||||
|
||||
def create_pattern_wrapper(assert_func):
|
||||
original_func = nge.npu_fx_compiler._optimize_fx
|
||||
|
||||
def wrapper(gm, example_inputs=None, config=None):
|
||||
ret = original_func(gm, example_inputs, config)
|
||||
graph_after = copy.deepcopy(gm)
|
||||
assert_func(graph_after)
|
||||
return ret
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def init_triton():
|
||||
init_device_properties_triton()
|
||||
|
||||
|
||||
class ModelQKNormRopeWithoutBias(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
head_dim: int,
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
dtype: torch.dtype = torch.bfloat16,
|
||||
eps: float = 1e-6,
|
||||
device="npu",
|
||||
):
|
||||
super().__init__()
|
||||
self.head_dim = head_dim
|
||||
self.num_heads = num_heads
|
||||
self.num_kv_heads = num_kv_heads
|
||||
self.q_size = num_heads * head_dim
|
||||
self.kv_size = num_kv_heads * head_dim
|
||||
self.eps = eps
|
||||
|
||||
# RMSNorm weight per head (shared across heads of same type)
|
||||
self.q_weight = nn.Parameter(torch.randn(head_dim, dtype=dtype, device=device))
|
||||
self.k_weight = nn.Parameter(torch.randn(head_dim, dtype=dtype, device=device))
|
||||
|
||||
def forward(self, qkv, cos_sin_cache, positions):
|
||||
"""
|
||||
Args:
|
||||
qkv: [T, q_size + 2*kv_size]
|
||||
cos: [1, T, 1, head_dim]
|
||||
sin: [1, T, 1, head_dim]
|
||||
Returns:
|
||||
q_rope, k_rope, v
|
||||
"""
|
||||
# Split QKV
|
||||
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
|
||||
|
||||
# Q RMSNorm (per-head)
|
||||
q_by_head = q.view(*q.shape[:-1], self.num_heads, self.head_dim)
|
||||
q_norm_out, _ = torch.ops.npu.npu_rms_norm(q_by_head, self.q_weight, self.eps)
|
||||
|
||||
# K RMSNorm (per-head)
|
||||
k_by_head = k.view(*k.shape[:-1], self.num_kv_heads, self.head_dim)
|
||||
k_norm_out, _ = torch.ops.npu.npu_rms_norm(k_by_head, self.k_weight, self.eps)
|
||||
|
||||
# Reshape for RoPE: [T, num_heads, head_dim] -> [1, T, num_heads, head_dim]
|
||||
q_flat = q_norm_out.view(q.shape)
|
||||
k_flat = k_norm_out.view(k.shape)
|
||||
|
||||
# Apply RoPE
|
||||
q_rope, k_rope = torch.ops.vllm.npu_rotary_embedding(
|
||||
positions, q_flat, k_flat, cos_sin_cache, self.head_dim, self.head_dim, True
|
||||
)
|
||||
|
||||
return q_rope, k_rope, v
|
||||
|
||||
|
||||
class ModelQKNormRopeWithBias(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
head_dim: int,
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
dtype: torch.dtype = torch.bfloat16,
|
||||
eps: float = 1e-6,
|
||||
device="npu",
|
||||
):
|
||||
super().__init__()
|
||||
self.head_dim = head_dim
|
||||
self.num_heads = num_heads
|
||||
self.num_kv_heads = num_kv_heads
|
||||
self.q_size = num_heads * head_dim
|
||||
self.kv_size = num_kv_heads * head_dim
|
||||
self.eps = eps
|
||||
|
||||
self.q_weight = nn.Parameter(torch.randn(head_dim, dtype=dtype, device=device))
|
||||
self.k_weight = nn.Parameter(torch.randn(head_dim, dtype=dtype, device=device))
|
||||
self.q_bias = nn.Parameter(torch.randn(head_dim, dtype=dtype, device=device))
|
||||
self.k_bias = nn.Parameter(torch.randn(head_dim, dtype=dtype, device=device))
|
||||
|
||||
def forward(self, qkv, cos_sin_cache, positions):
|
||||
# Split QKV
|
||||
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
|
||||
|
||||
# Q RMSNorm + Bias
|
||||
q_by_head = q.view(*q.shape[:-1], self.num_heads, self.head_dim)
|
||||
q_norm_out, _ = torch.ops.npu.npu_rms_norm(q_by_head, self.q_weight, self.eps)
|
||||
q_normed = q_norm_out + self.q_bias
|
||||
|
||||
# K RMSNorm + Bias
|
||||
k_by_head = k.view(*k.shape[:-1], self.num_kv_heads, self.head_dim)
|
||||
k_norm_out, _ = torch.ops.npu.npu_rms_norm(k_by_head, self.k_weight, self.eps)
|
||||
k_normed = k_norm_out + self.k_bias
|
||||
|
||||
# Reshape for RoPE
|
||||
q_flat = q_normed.view(q.shape)
|
||||
k_flat = k_normed.view(k.shape)
|
||||
|
||||
# Apply RoPE
|
||||
q_rope, k_rope = torch.ops.vllm.npu_rotary_embedding(
|
||||
positions, q_flat, k_flat, cos_sin_cache, self.head_dim, self.head_dim, True
|
||||
)
|
||||
|
||||
return q_rope, k_rope, v
|
||||
|
||||
|
||||
def assert_qknorm_rope_fusion(after_gm, expect_fused=True, use_bias=False):
|
||||
check_rules = [
|
||||
(torch.ops.vllm.qkv_rmsnorm_rope.default, expect_fused),
|
||||
(torch.ops.npu.npu_rms_norm.default, not expect_fused),
|
||||
(torch.ops.vllm.npu_rotary_embedding.default, not expect_fused),
|
||||
]
|
||||
if use_bias:
|
||||
check_rules.append((torch.ops.aten.add.Tensor, not expect_fused))
|
||||
for torch_op, expect_exist in check_rules:
|
||||
found = find_op(after_gm, torch_op)
|
||||
if expect_exist:
|
||||
assert found, f"Expected operator '{torch_op}' but not find"
|
||||
else:
|
||||
assert not found, f"Not expected operator '{torch_op}' but find"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("hidden_size", [64])
|
||||
@pytest.mark.parametrize("num_tokens", [257])
|
||||
@pytest.mark.parametrize("eps", [1e-5])
|
||||
@pytest.mark.parametrize("use_bias", [False, True])
|
||||
def test_rmsnorm_quant_fusion(
|
||||
dtype: torch.dtype,
|
||||
hidden_size: int,
|
||||
num_tokens: int,
|
||||
eps: float,
|
||||
use_bias: bool,
|
||||
):
|
||||
vllm_config = VllmConfig(model_config=ModelConfig(dtype=dtype))
|
||||
with vllm.config.set_current_vllm_config(vllm_config):
|
||||
update_environment_variables(
|
||||
{
|
||||
"RANK": "0",
|
||||
"LOCAL_RANK": "0",
|
||||
"WORLD_SIZE": "1",
|
||||
"MASTER_ADDR": "localhost",
|
||||
"MASTER_PORT": "12345",
|
||||
}
|
||||
)
|
||||
init_distributed_environment()
|
||||
ensure_model_parallel_initialized(1, 1)
|
||||
num_heads = 16
|
||||
num_kv_heads = 8
|
||||
head_dim = 128
|
||||
with vllm.config.set_current_vllm_config(vllm_config), set_ascend_forward_context(None, vllm_config):
|
||||
fusion_pattern = None
|
||||
q_size = num_heads * head_dim
|
||||
kv_size = num_kv_heads * head_dim
|
||||
qkv_size = q_size + 2 * kv_size
|
||||
if use_bias:
|
||||
model = ModelQKNormRopeWithBias(head_dim, num_heads, num_kv_heads, dtype, eps, device="npu")
|
||||
fusion_pattern = QKNormRopeFusionPatternWithBias(
|
||||
vllm_config=vllm_config, head_dim=head_dim, num_heads=num_heads, num_kv_heads=num_kv_heads, eps=eps
|
||||
)
|
||||
else:
|
||||
model = ModelQKNormRopeWithoutBias(head_dim, num_heads, num_kv_heads, dtype, eps, device="npu")
|
||||
fusion_pattern = QKNormRopeFusionPattern(
|
||||
vllm_config=vllm_config, head_dim=head_dim, num_heads=num_heads, num_kv_heads=num_kv_heads, eps=eps
|
||||
)
|
||||
from torch._inductor.pattern_matcher import PatternMatcherPass
|
||||
|
||||
pm_pass = PatternMatcherPass()
|
||||
fusion_pattern.register(pm_pass)
|
||||
model = model.to("npu")
|
||||
seq_len = num_tokens
|
||||
qkv = torch.randn(seq_len, qkv_size, device="npu", dtype=dtype)
|
||||
cos_sin_cache = torch.from_numpy(np.random.uniform(0, 1, [MAX_POSITION_EMBEDDING, head_dim])).to(dtype).npu()
|
||||
positions = torch.randint(
|
||||
low=0, high=MAX_POSITION_EMBEDDING, size=(num_tokens,), dtype=torch.int64, device="npu"
|
||||
)
|
||||
|
||||
with torch.no_grad():
|
||||
original_optimize = nge.npu_fx_compiler._optimize_fx
|
||||
nge.npu_fx_compiler._optimize_fx = create_pattern_wrapper(
|
||||
lambda gm: assert_qknorm_rope_fusion(gm, expect_fused=True, use_bias=use_bias)
|
||||
)
|
||||
|
||||
compiled_model = torch.compile(model, backend="npugraph_ex", fullgraph=True, dynamic=True)
|
||||
|
||||
compiled_model(qkv, cos_sin_cache, positions)
|
||||
|
||||
nge.npu_fx_compiler._optimize_fx = original_optimize
|
||||
@@ -0,0 +1,298 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import vllm.config
|
||||
from vllm.compilation.passes.fx_utils import OpOverload
|
||||
from vllm.config import ModelConfig, VllmConfig
|
||||
from vllm.distributed import ensure_model_parallel_initialized, init_distributed_environment
|
||||
from vllm.utils.system_utils import update_environment_variables
|
||||
|
||||
import vllm_ascend.ops.register_custom_ops # noqa
|
||||
from vllm_ascend.ascend_forward_context import set_ascend_forward_context
|
||||
from vllm_ascend.compilation.passes.norm_quant_fusion_pass import AddRMSNormQuantFusionPass
|
||||
from vllm_ascend.utils import enable_custom_op
|
||||
|
||||
from .backend import TestBackend
|
||||
|
||||
# Cache backend to avoid duplicate pattern registration
|
||||
_backend_cache = None
|
||||
|
||||
|
||||
def get_or_create_backend(vllm_config):
|
||||
"""Get or create backend with fusion passes (cached to avoid duplicate pattern registration)."""
|
||||
global _backend_cache
|
||||
if _backend_cache is None:
|
||||
_backend_cache = TestBackend(custom_passes=[AddRMSNormQuantFusionPass(vllm_config=vllm_config)])
|
||||
return _backend_cache
|
||||
|
||||
|
||||
class TestModelWithoutBias(nn.Module):
|
||||
"""
|
||||
A minimal test model that simulates the pattern:
|
||||
AddRMSNorm → Quantization (without bias)
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size: int, dtype: torch.dtype, eps: float = 1e-6, device="npu"):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.eps = eps
|
||||
self.rms_norm_weight = nn.Parameter(torch.randn(hidden_size, device=device))
|
||||
self.quant_scale = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_scale_reciprocal = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_offset = torch.zeros(hidden_size, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
"""
|
||||
Forward pass:
|
||||
1. Perform npu_add_rms_norm
|
||||
2. Quantize the normalized output to int8
|
||||
Returns both quantized output and updated residual.
|
||||
"""
|
||||
residual = torch.zeros_like(x)
|
||||
|
||||
norm_output, _, new_residual = torch.ops._C_ascend.npu_add_rms_norm_bias(
|
||||
x, residual, self.rms_norm_weight, None, self.eps
|
||||
)
|
||||
|
||||
quantized_output = torch.ops.vllm.quantize(
|
||||
norm_output, self.quant_scale, self.quant_scale_reciprocal, self.quant_offset
|
||||
)
|
||||
|
||||
return quantized_output, new_residual
|
||||
|
||||
def ops_in_model_before(self) -> list[OpOverload]:
|
||||
"""Return the list of expected operators BEFORE fusion."""
|
||||
return [torch.ops._C_ascend.npu_add_rms_norm_bias.default, torch.ops.vllm.quantize.default]
|
||||
|
||||
def ops_in_model_after(self) -> list[OpOverload]:
|
||||
"""Return the list of expected operators AFTER successful fusion."""
|
||||
return [torch.ops.npu.npu_add_rms_norm_quant.default]
|
||||
|
||||
|
||||
class TestModelWithBias(nn.Module):
|
||||
"""
|
||||
A test model that simulates the pattern:
|
||||
AddRMSNorm → Add Bias → Quantization (with bias)
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size: int, dtype: torch.dtype, eps: float = 1e-6, device="npu"):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.eps = eps
|
||||
self.rms_norm_weight = nn.Parameter(torch.randn(hidden_size, device=device))
|
||||
self.bias = nn.Parameter(torch.randn(hidden_size, device=device))
|
||||
self.quant_scale = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_scale_reciprocal = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_offset = torch.zeros(hidden_size, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
"""
|
||||
Forward pass:
|
||||
1. Perform npu_add_rms_norm
|
||||
2. Add bias
|
||||
3. Quantize to int8
|
||||
Returns both quantized output and updated residual.
|
||||
"""
|
||||
residual = torch.zeros_like(x)
|
||||
|
||||
norm_output_with_bias, _, new_residual = torch.ops._C_ascend.npu_add_rms_norm_bias(
|
||||
x, residual, self.rms_norm_weight, self.bias, self.eps
|
||||
)
|
||||
|
||||
quantized_output = torch.ops.vllm.quantize(
|
||||
norm_output_with_bias, self.quant_scale, self.quant_scale_reciprocal, self.quant_offset
|
||||
)
|
||||
|
||||
return quantized_output, new_residual
|
||||
|
||||
def ops_in_model_before(self) -> list[OpOverload]:
|
||||
"""Return the list of expected operators BEFORE fusion."""
|
||||
return [torch.ops._C_ascend.npu_add_rms_norm_bias.default, torch.ops.vllm.quantize.default]
|
||||
|
||||
def ops_in_model_after(self) -> list[OpOverload]:
|
||||
"""Return the list of expected operators AFTER successful fusion."""
|
||||
return [torch.ops.npu.npu_add_rms_norm_quant.default]
|
||||
|
||||
|
||||
class TestModelSPWithoutBias(nn.Module):
|
||||
"""
|
||||
A minimal test model that simulates the pattern:
|
||||
AddRMSNorm → maybe_allgather → Quantization (without bias)
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size: int, dtype: torch.dtype, eps: float = 1e-6, device="npu"):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.eps = eps
|
||||
self.rms_norm_weight = nn.Parameter(torch.randn(hidden_size, device=device))
|
||||
self.quant_scale = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_scale_reciprocal = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_offset = torch.zeros(hidden_size, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
"""
|
||||
Forward pass:
|
||||
1. Perform npu_add_rms_norm
|
||||
2. Perform a fake maybe_all_gather_and_maybe_unpad
|
||||
3. Quantize the normalized output to int8
|
||||
Returns both quantized output and updated residual.
|
||||
"""
|
||||
residual = torch.zeros_like(x)
|
||||
|
||||
norm_output, _, new_residual = torch.ops._C_ascend.npu_add_rms_norm_bias(
|
||||
x, residual, self.rms_norm_weight, None, self.eps
|
||||
)
|
||||
|
||||
norm_output = torch.ops.vllm.maybe_all_gather_and_maybe_unpad(norm_output, True)
|
||||
|
||||
quantized_output = torch.ops.vllm.quantize(
|
||||
norm_output, self.quant_scale, self.quant_scale_reciprocal, self.quant_offset
|
||||
)
|
||||
|
||||
return quantized_output, new_residual
|
||||
|
||||
def ops_in_model_before(self) -> list[OpOverload]:
|
||||
"""Return the list of expected operators BEFORE fusion."""
|
||||
return [
|
||||
torch.ops._C_ascend.npu_add_rms_norm_bias.default,
|
||||
torch.ops.vllm.maybe_all_gather_and_maybe_unpad.default,
|
||||
torch.ops.vllm.quantize.default,
|
||||
]
|
||||
|
||||
def ops_in_model_after(self) -> list[OpOverload]:
|
||||
"""Return the list of expected operators AFTER successful fusion."""
|
||||
return [torch.ops.npu.npu_add_rms_norm_quant.default, torch.ops.vllm.maybe_all_gather_and_maybe_unpad.default]
|
||||
|
||||
|
||||
class TestModelSPWithBias(nn.Module):
|
||||
"""
|
||||
A minimal test model that simulates the pattern:
|
||||
AddRMSNorm → Add bias → maybe_allgather → Quantization (without bias)
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size: int, dtype: torch.dtype, eps: float = 1e-6, device="npu"):
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
self.eps = eps
|
||||
self.rms_norm_weight = nn.Parameter(torch.randn(hidden_size, device=device))
|
||||
self.bias = nn.Parameter(torch.randn(hidden_size, device=device))
|
||||
self.quant_scale = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_scale_reciprocal = torch.ones(hidden_size, dtype=dtype, device=device)
|
||||
self.quant_offset = torch.zeros(hidden_size, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
"""
|
||||
Forward pass:
|
||||
1. Perform npu_add_rms_norm
|
||||
2. Add bias
|
||||
3. Perform a fake maybe_all_gather_and_maybe_unpad
|
||||
4. Quantize the normalized output to int8
|
||||
Returns both quantized output and updated residual.
|
||||
"""
|
||||
residual = torch.zeros_like(x)
|
||||
|
||||
norm_output_with_bias, _, new_residual = torch.ops._C_ascend.npu_add_rms_norm_bias(
|
||||
x, residual, self.rms_norm_weight, self.bias, self.eps
|
||||
)
|
||||
|
||||
norm_output_with_bias = torch.ops.vllm.maybe_all_gather_and_maybe_unpad(norm_output_with_bias, True)
|
||||
|
||||
quantized_output = torch.ops.vllm.quantize(
|
||||
norm_output_with_bias, self.quant_scale, self.quant_scale_reciprocal, self.quant_offset
|
||||
)
|
||||
|
||||
return quantized_output, new_residual
|
||||
|
||||
def ops_in_model_before(self) -> list[OpOverload]:
|
||||
"""Return the list of expected operators BEFORE fusion."""
|
||||
return [
|
||||
torch.ops._C_ascend.npu_add_rms_norm_bias.default,
|
||||
torch.ops.vllm.maybe_all_gather_and_maybe_unpad.default,
|
||||
torch.ops.vllm.quantize.default,
|
||||
]
|
||||
|
||||
def ops_in_model_after(self) -> list[OpOverload]:
|
||||
"""Return the list of expected operators AFTER successful fusion."""
|
||||
return [torch.ops.npu.npu_add_rms_norm_quant.default, torch.ops.vllm.maybe_all_gather_and_maybe_unpad.default]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("hidden_size", [64])
|
||||
@pytest.mark.parametrize("num_tokens", [257])
|
||||
@pytest.mark.parametrize("eps", [1e-5, 1e-6])
|
||||
@pytest.mark.parametrize("use_bias", [False, True])
|
||||
@pytest.mark.parametrize("sp_enable", [False, True])
|
||||
def test_rmsnorm_quant_fusion(
|
||||
dtype: torch.dtype,
|
||||
hidden_size: int,
|
||||
num_tokens: int,
|
||||
eps: float,
|
||||
use_bias: bool,
|
||||
sp_enable: bool,
|
||||
):
|
||||
"""
|
||||
End-to-end test for AddRMSNorm+Quantize fusion.
|
||||
Compares: Operator presence/absence before and after graph transformation
|
||||
"""
|
||||
torch.set_default_dtype(dtype)
|
||||
torch.manual_seed(1)
|
||||
|
||||
vllm_config = VllmConfig(model_config=ModelConfig(dtype=dtype))
|
||||
|
||||
with vllm.config.set_current_vllm_config(vllm_config):
|
||||
update_environment_variables(
|
||||
{
|
||||
"RANK": "0",
|
||||
"LOCAL_RANK": "0",
|
||||
"WORLD_SIZE": "1",
|
||||
"MASTER_ADDR": "localhost",
|
||||
"MASTER_PORT": "12345",
|
||||
}
|
||||
)
|
||||
init_distributed_environment()
|
||||
ensure_model_parallel_initialized(1, 1)
|
||||
|
||||
with vllm.config.set_current_vllm_config(vllm_config), set_ascend_forward_context(None, vllm_config):
|
||||
backend = get_or_create_backend(vllm_config)
|
||||
if use_bias:
|
||||
if not enable_custom_op():
|
||||
return
|
||||
if sp_enable:
|
||||
model = TestModelSPWithBias(hidden_size, dtype, eps, device="npu")
|
||||
else:
|
||||
model = TestModelWithBias(hidden_size, dtype, eps, device="npu")
|
||||
else:
|
||||
if sp_enable:
|
||||
model = TestModelSPWithoutBias(hidden_size, dtype, eps, device="npu")
|
||||
else:
|
||||
model = TestModelWithoutBias(hidden_size, dtype, eps, device="npu")
|
||||
model = model.to("npu")
|
||||
|
||||
x = torch.rand(num_tokens, hidden_size, device="npu", dtype=dtype, requires_grad=False)
|
||||
|
||||
result_unfused = model(x)
|
||||
print("Unfused result:", [t.shape for t in result_unfused])
|
||||
model_fused = torch.compile(model, backend=backend)
|
||||
result_fused = model_fused(x)
|
||||
print("Fused result:", [t.shape for t in result_fused])
|
||||
|
||||
print("=== Checking operator fusion ===")
|
||||
backend.check_before_ops(model.ops_in_model_before(), fully_replaced=not sp_enable)
|
||||
backend.check_after_ops(model.ops_in_model_after())
|
||||
0
tests/e2e/pull_request/one_card/lora/__init__.py
Normal file
0
tests/e2e/pull_request/one_card/lora/__init__.py
Normal file
59
tests/e2e/pull_request/one_card/lora/test_ilama_lora.py
Normal file
59
tests/e2e/pull_request/one_card/lora/test_ilama_lora.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import vllm
|
||||
from vllm.lora.request import LoRARequest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
MODEL_PATH = "vllm-ascend/ilama-3.2-1B"
|
||||
|
||||
PROMPT_TEMPLATE = """I want you to act as a SQL terminal in front of an example database, you need only to return the sql command to me.Below is an instruction that describes a task, Write a response that appropriately completes the request.\n"\n##Instruction:\nconcert_singer contains tables such as stadium, singer, concert, singer_in_concert. Table stadium has columns such as Stadium_ID, Location, Name, Capacity, Highest, Lowest, Average. Stadium_ID is the primary key.\nTable singer has columns such as Singer_ID, Name, Country, Song_Name, Song_release_year, Age, Is_male. Singer_ID is the primary key.\nTable concert has columns such as concert_ID, concert_Name, Theme, Stadium_ID, Year. concert_ID is the primary key.\nTable singer_in_concert has columns such as concert_ID, Singer_ID. concert_ID is the primary key.\nThe Stadium_ID of concert is the foreign key of Stadium_ID of stadium.\nThe Singer_ID of singer_in_concert is the foreign key of Singer_ID of singer.\nThe concert_ID of singer_in_concert is the foreign key of concert_ID of concert.\n\n###Input:\n{query}\n\n###Response:""" # noqa: E501
|
||||
|
||||
EXPECTED_LORA_OUTPUT = [
|
||||
"SELECT count(*) FROM singer",
|
||||
"SELECT avg(age) , min(age) , max(age) FROM singer WHERE country = 'France'", # noqa: E501
|
||||
"SELECT DISTINCT Country FROM singer WHERE Age > 20",
|
||||
]
|
||||
|
||||
|
||||
def do_sample(llm: vllm.LLM, lora_path: str, lora_id: int) -> list[str]:
|
||||
prompts = [
|
||||
PROMPT_TEMPLATE.format(query="How many singers do we have?"),
|
||||
PROMPT_TEMPLATE.format(
|
||||
query="What is the average, minimum, and maximum age of all singers from France?" # noqa: E501
|
||||
),
|
||||
PROMPT_TEMPLATE.format(
|
||||
query="What are all distinct countries where singers above age 20 are from?" # noqa: E501
|
||||
),
|
||||
]
|
||||
sampling_params = vllm.SamplingParams(temperature=0, max_tokens=32)
|
||||
outputs = llm.generate(
|
||||
prompts, sampling_params, lora_request=LoRARequest(str(lora_id), lora_id, lora_path) if lora_id else None
|
||||
)
|
||||
# Print the outputs.
|
||||
generated_texts: list[str] = []
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text.strip()
|
||||
generated_texts.append(generated_text)
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
return generated_texts
|
||||
|
||||
|
||||
def test_ilama_lora(ilama_lora_files):
|
||||
with VllmRunner(
|
||||
MODEL_PATH,
|
||||
enable_lora=True,
|
||||
dtype="half",
|
||||
max_loras=4,
|
||||
max_model_len=1024,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
max_num_seqs=16,
|
||||
enforce_eager=True,
|
||||
) as vllm_model:
|
||||
output1 = do_sample(vllm_model.model, ilama_lora_files, lora_id=1)
|
||||
for i in range(len(EXPECTED_LORA_OUTPUT)):
|
||||
assert output1[i] == EXPECTED_LORA_OUTPUT[i]
|
||||
|
||||
output2 = do_sample(vllm_model.model, ilama_lora_files, lora_id=2)
|
||||
for i in range(len(EXPECTED_LORA_OUTPUT)):
|
||||
assert output2[i] == EXPECTED_LORA_OUTPUT[i]
|
||||
140
tests/e2e/pull_request/one_card/lora/test_llama32_lora.py
Normal file
140
tests/e2e/pull_request/one_card/lora/test_llama32_lora.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import vllm
|
||||
import vllm.config
|
||||
from vllm.lora.request import LoRARequest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from vllm_ascend.utils import enable_custom_op
|
||||
|
||||
enable_custom_op()
|
||||
|
||||
PROMPT_TEMPLATE = """<|eot_id|><|start_header_id|>user<|end_header_id|>
|
||||
I want you to act as a SQL terminal in front of an example database, you need only to return the sql command to me.Below is an instruction that describes a task, Write a response that appropriately completes the request.
|
||||
"
|
||||
##Instruction:
|
||||
candidate_poll contains tables such as candidate, people. Table candidate has columns such as Candidate_ID, People_ID, Poll_Source, Date, Support_rate, Consider_rate, Oppose_rate, Unsure_rate. Candidate_ID is the primary key.
|
||||
Table people has columns such as People_ID, Sex, Name, Date_of_Birth, Height, Weight. People_ID is the primary key.
|
||||
The People_ID of candidate is the foreign key of People_ID of people.
|
||||
###Input:
|
||||
{context}
|
||||
###Response:<|eot_id|><|start_header_id|>assistant<|end_header_id|>
|
||||
""" # noqa: E501
|
||||
|
||||
EXPECTED_LORA_OUTPUT = [
|
||||
"SELECT count(*) FROM candidate",
|
||||
"SELECT count(*) FROM candidate",
|
||||
"SELECT poll_source FROM candidate GROUP BY poll_source ORDER BY count(*) DESC LIMIT 1", # noqa: E501
|
||||
"SELECT poll_source FROM candidate GROUP BY poll_source ORDER BY count(*) DESC LIMIT 1", # noqa: E501
|
||||
]
|
||||
|
||||
EXPECTED_BASE_MODEL_OUTPUT = [
|
||||
"SELECT COUNT(*) FROM candidate",
|
||||
"`SELECT COUNT(*) FROM candidate;`",
|
||||
"SELECT Poll_Source FROM candidate GROUP BY Poll_Source ORDER BY COUNT(*) DESC LIMIT 1;",
|
||||
"SELECT * FROM candidate ORDER BY Candidate_ID DESC LIMIT 1",
|
||||
]
|
||||
|
||||
# For hk region, we need to use the model from hf to avoid the network issue
|
||||
MODEL_PATH = "meta-llama/Llama-3.2-3B-Instruct"
|
||||
|
||||
|
||||
def do_sample(
|
||||
llm: vllm.LLM,
|
||||
lora_path: str,
|
||||
lora_id: int,
|
||||
tensorizer_config_dict: dict | None = None,
|
||||
) -> list[str]:
|
||||
prompts = [
|
||||
PROMPT_TEMPLATE.format(context="How many candidates are there?"),
|
||||
PROMPT_TEMPLATE.format(context="Count the number of candidates."),
|
||||
PROMPT_TEMPLATE.format(
|
||||
context="Which poll resource provided the most number of candidate information?" # noqa: E501
|
||||
),
|
||||
PROMPT_TEMPLATE.format(context="Return the poll resource associated with the most candidates."),
|
||||
]
|
||||
|
||||
sampling_params = vllm.SamplingParams(temperature=0, max_tokens=64, stop=["<|im_end|>"])
|
||||
if tensorizer_config_dict is not None:
|
||||
outputs = llm.generate(
|
||||
prompts,
|
||||
sampling_params,
|
||||
lora_request=LoRARequest(
|
||||
str(lora_id),
|
||||
lora_id,
|
||||
lora_path,
|
||||
tensorizer_config_dict=tensorizer_config_dict,
|
||||
)
|
||||
if lora_id
|
||||
else None,
|
||||
)
|
||||
else:
|
||||
outputs = llm.generate(
|
||||
prompts,
|
||||
sampling_params,
|
||||
lora_request=LoRARequest(str(lora_id), lora_id, lora_path) if lora_id else None,
|
||||
)
|
||||
|
||||
generated_texts: list[str] = []
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
generated_texts.append(generated_text)
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
return generated_texts
|
||||
|
||||
|
||||
def generate_and_test(llm, llama32_lora_files, tensorizer_config_dict: dict | None = None):
|
||||
print("lora adapter created")
|
||||
print("lora 1")
|
||||
assert (
|
||||
do_sample(
|
||||
llm,
|
||||
llama32_lora_files,
|
||||
tensorizer_config_dict=tensorizer_config_dict,
|
||||
lora_id=1,
|
||||
)
|
||||
== EXPECTED_LORA_OUTPUT
|
||||
)
|
||||
|
||||
print("lora 2")
|
||||
assert (
|
||||
do_sample(
|
||||
llm,
|
||||
llama32_lora_files,
|
||||
tensorizer_config_dict=tensorizer_config_dict,
|
||||
lora_id=2,
|
||||
)
|
||||
== EXPECTED_LORA_OUTPUT
|
||||
)
|
||||
|
||||
print("base model")
|
||||
assert (
|
||||
do_sample(
|
||||
llm,
|
||||
llama32_lora_files,
|
||||
tensorizer_config_dict=tensorizer_config_dict,
|
||||
lora_id=0,
|
||||
)
|
||||
== EXPECTED_BASE_MODEL_OUTPUT
|
||||
)
|
||||
|
||||
print("removing lora")
|
||||
|
||||
|
||||
@patch.dict("os.environ", {"VLLM_USE_MODELSCOPE": "False"})
|
||||
def test_llama_lora(llama32_lora_files):
|
||||
vllm_model = VllmRunner(
|
||||
MODEL_PATH,
|
||||
enable_lora=True,
|
||||
# also test odd max_num_seqs
|
||||
max_num_seqs=7,
|
||||
max_model_len=1024,
|
||||
max_loras=4,
|
||||
compilation_config={"cudagraph_mode": "PIECEWISE"},
|
||||
)
|
||||
llm = vllm_model.model
|
||||
generate_and_test(llm, llama32_lora_files)
|
||||
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
This script contains:
|
||||
1. test lora with speculative decoding for batch inference
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from vllm import LLM, SamplingParams
|
||||
from vllm.lora.request import LoRARequest
|
||||
|
||||
LORA_TEST_PROMPT_MAP: dict[str, str] = {}
|
||||
|
||||
LORA_TEST_PROMPT_MAP["vllm-ascend/qwen-linear-algebra-coder"] = """
|
||||
### INSTRUCTION:
|
||||
You are an AI assistant that generates Python code to solve linear
|
||||
algebra problems.
|
||||
|
||||
### PROBLEM:
|
||||
Find the eigenvalues and eigenvectors of the following 3x3 matrix:
|
||||
[[3, 2, 0],
|
||||
[2, 3, 0],
|
||||
[0, 0, 2]]
|
||||
|
||||
### OUTPUT FORMAT (STRICT):
|
||||
Numbers should be represented as integers only.
|
||||
|
||||
### PYTHON SOLUTION:
|
||||
"""
|
||||
|
||||
SEED = 42
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_setup",
|
||||
[
|
||||
(
|
||||
"eagle3",
|
||||
"Qwen/Qwen3-1.7B",
|
||||
"vllm-ascend/Qwen3-1.7B_eagle3",
|
||||
"vllm-ascend/qwen-linear-algebra-coder",
|
||||
1,
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_batch_inference_correctness(
|
||||
model_setup: tuple[str, str, str, str, int],
|
||||
):
|
||||
"""
|
||||
Compare the outputs of a LLM with only Lora and a LLM with both SD and Lora.
|
||||
Should be the same and no failure when doing batch inference.
|
||||
model_setup: (method, model_name, spec_model_name, lora_path, tp_size)
|
||||
"""
|
||||
# Disable randomness
|
||||
torch.manual_seed(SEED)
|
||||
np.random.seed(SEED)
|
||||
random.seed(SEED)
|
||||
torch.use_deterministic_algorithms(True)
|
||||
|
||||
method, model_name, spec_model_name, lora_path, tp_size = model_setup
|
||||
prompts = [LORA_TEST_PROMPT_MAP[lora_path]] * 100
|
||||
lora_request = LoRARequest("adapter", 1, lora_path)
|
||||
sampling_params = SamplingParams(temperature=0.0, top_p=1.0, top_k=-1, seed=SEED, max_tokens=128)
|
||||
|
||||
# without speculative decoding
|
||||
ref_llm = LLM(
|
||||
model=model_name,
|
||||
trust_remote_code=True,
|
||||
tensor_parallel_size=tp_size,
|
||||
max_model_len=2048,
|
||||
max_num_seqs=4,
|
||||
enable_lora=True,
|
||||
max_loras=1,
|
||||
max_cpu_loras=1,
|
||||
max_lora_rank=16,
|
||||
)
|
||||
ref_outputs = ref_llm.generate(prompts, sampling_params, lora_request=lora_request)
|
||||
del ref_llm
|
||||
|
||||
# speculative decoding
|
||||
lora_spec_llm = LLM(
|
||||
model=model_name,
|
||||
trust_remote_code=True,
|
||||
tensor_parallel_size=tp_size,
|
||||
speculative_config={
|
||||
"method": method,
|
||||
"model": spec_model_name,
|
||||
"num_speculative_tokens": 3,
|
||||
"max_model_len": 2048,
|
||||
},
|
||||
max_model_len=2048,
|
||||
max_num_seqs=4,
|
||||
enable_lora=True,
|
||||
max_loras=1,
|
||||
max_cpu_loras=1,
|
||||
max_lora_rank=16,
|
||||
)
|
||||
lora_spec_outputs = lora_spec_llm.generate(prompts, sampling_params, lora_request=lora_request)
|
||||
del lora_spec_llm
|
||||
|
||||
matches = 0
|
||||
misses = 0
|
||||
for ref_output, spec_output in zip(ref_outputs, lora_spec_outputs):
|
||||
if ref_output.outputs[0].text == spec_output.outputs[0].text:
|
||||
matches += 1
|
||||
else:
|
||||
misses += 1
|
||||
print(f"ref_output: {ref_output.outputs[0].text}")
|
||||
print(f"spec_output: {spec_output.outputs[0].text}")
|
||||
|
||||
# Heuristic: expect at least 90% of the prompts to match exactly
|
||||
# Upon failure, inspect the outputs to check for inaccuracy.
|
||||
print(f"match ratio: {matches}/{len(ref_outputs)}")
|
||||
assert matches > int(0.90 * len(ref_outputs))
|
||||
106
tests/e2e/pull_request/one_card/lora/test_olmoe_lora.py
Normal file
106
tests/e2e/pull_request/one_card/lora/test_olmoe_lora.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
import vllm
|
||||
from vllm.lora.request import LoRARequest
|
||||
|
||||
MODEL_PATH = "allenai/OLMoE-1B-7B-0125-Instruct"
|
||||
|
||||
PROMPT_TEMPLATE = """I want you to act as a SQL terminal in front of an example database, you need only to return the sql command to me. Do not return any additional explanation. Below is an instruction that describes a task, Write a response that appropriately completes the request.
|
||||
"
|
||||
##Instruction:
|
||||
candidate_poll contains tables such as candidate, people. Table candidate has columns such as Candidate_ID, People_ID, Poll_Source, Date, Support_rate, Consider_rate, Oppose_rate, Unsure_rate. Candidate_ID is the primary key.
|
||||
Table people has columns such as People_ID, Sex, Name, Date_of_Birth, Height, Weight. People_ID is the primary key.
|
||||
The People_ID of candidate is the foreign key of People_ID of people.
|
||||
|
||||
|
||||
###Input:
|
||||
{context}
|
||||
|
||||
###Response:""" # noqa: E501
|
||||
|
||||
EXPECTED_LORA_OUTPUT = [
|
||||
"SELECT count(*) FROM candidate",
|
||||
"SELECT count(*) FROM candidate",
|
||||
"SELECT poll_source FROM candidate GROUP BY poll_source ORDER BY count(*) DESC LIMIT 1", # noqa: E501
|
||||
"SELECT poll_source FROM candidate GROUP BY poll_source ORDER BY count(*) DESC LIMIT 1", # noqa: E501
|
||||
]
|
||||
|
||||
EXPECTED_BASE_MODEL_OUTPUT = [
|
||||
"SELECT COUNT(Candidate_ID) FROM candidate",
|
||||
"SELECT COUNT(Candidate_ID) FROM candidate",
|
||||
"SELECT Candidate_ID, COUNT(*) as Total_Candidates\nFROM candidate\nINNER JOIN people ON candidate.People_ID = people.People_ID", # noqa: E501
|
||||
# There are multiple acceptable responses
|
||||
(
|
||||
"SELECT Candidate_ID, Poll_Source FROM candidate WHERE People_ID IN (SELECT People_ID FROM people) ORDER BY COUNT(*) DESC LIMIT 1", # noqa: E501
|
||||
"SELECT Candidate_ID, Poll_Source FROM candidate WHERE COUNT(People_ID) = (SELECT COUNT(People_ID) FROM people) ORDER BY Candidate_ID DESC LIMIT 1", # noqa: E501
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _output_matches(generated: str, accepted: str | Sequence[str]) -> bool:
|
||||
if isinstance(accepted, str):
|
||||
accepted = (accepted,)
|
||||
return any(generated.startswith(s) for s in accepted)
|
||||
|
||||
|
||||
def generate_and_test(
|
||||
llm: vllm.LLM,
|
||||
lora_path: str,
|
||||
lora_id: list[int | None] | int | None,
|
||||
compare_lower: bool = False,
|
||||
) -> None:
|
||||
prompts = [
|
||||
PROMPT_TEMPLATE.format(context="How many candidates are there?"),
|
||||
PROMPT_TEMPLATE.format(context="Count the number of candidates."),
|
||||
PROMPT_TEMPLATE.format(
|
||||
context="Which poll resource provided the most number of candidate information?" # noqa: E501
|
||||
),
|
||||
PROMPT_TEMPLATE.format(context="Return the poll resource associated with the most candidates."),
|
||||
]
|
||||
|
||||
lora_request = None
|
||||
if isinstance(lora_id, int):
|
||||
lora_request = LoRARequest(str(lora_id), lora_id, lora_path)
|
||||
elif isinstance(lora_id, list):
|
||||
lora_request = [LoRARequest(str(i), i, lora_path) if i is not None else None for i in lora_id]
|
||||
|
||||
sampling_params = vllm.SamplingParams(temperature=0, max_tokens=64)
|
||||
outputs = llm.generate(prompts, sampling_params, lora_request=lora_request)
|
||||
# Print the outputs.
|
||||
generated_texts: list[str] = []
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text.strip()
|
||||
generated_texts.append(generated_text)
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
|
||||
for i in range(len(EXPECTED_LORA_OUTPUT)):
|
||||
req_lora_id = lora_id[i] if isinstance(lora_id, list) else lora_id
|
||||
generated_text = generated_texts[i]
|
||||
expected_output = EXPECTED_LORA_OUTPUT[i] if req_lora_id is not None else EXPECTED_BASE_MODEL_OUTPUT[i]
|
||||
|
||||
if compare_lower:
|
||||
generated_text = generated_text.lower()
|
||||
if isinstance(expected_output, str):
|
||||
expected_output = (expected_output.lower(),)
|
||||
else:
|
||||
expected_output = tuple(s.lower() for s in expected_output)
|
||||
assert _output_matches(generated_text, expected_output), (
|
||||
f"Output {i}: {generated_text!r} does not match any of {expected_output!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_olmoe_lora(olmoe_lora_files):
|
||||
# We enable enforce_eager=True here to reduce VRAM usage for lora-test CI,
|
||||
# Otherwise, the lora-test will fail due to CUDA OOM.
|
||||
llm = vllm.LLM(
|
||||
MODEL_PATH,
|
||||
max_model_len=1024,
|
||||
enable_lora=True,
|
||||
max_loras=4,
|
||||
enforce_eager=False,
|
||||
trust_remote_code=True,
|
||||
enable_chunked_prefill=True,
|
||||
)
|
||||
|
||||
generate_and_test(llm, olmoe_lora_files, lora_id=1)
|
||||
@@ -0,0 +1,90 @@
|
||||
import vllm
|
||||
from transformers import AutoTokenizer
|
||||
from vllm.lora.request import LoRARequest
|
||||
|
||||
MODEL_PATH = "Qwen/Qwen3.5-4B"
|
||||
TEXT_LORA_ID = 1
|
||||
|
||||
# text-only task
|
||||
TEXT_PROMPT_TEMPLATE = """Write a SQL query for the given database.\nSchema:\nTables:\n - stadium(Stadium_ID, Location, Name, Capacity, Highest, Lowest, Average)\n - singer(Singer_ID, Name, Country, Song_Name, Song_release_year, Age, Is_male)\n - concert(concert_ID, concert_Name, Theme, Stadium_ID, Year)\n - singer_in_concert(concert_ID, Singer_ID)\n\nQuestion:\n{query}""" # noqa: E501
|
||||
|
||||
TEXT_EXPECTED_LORA_OUTPUT = [
|
||||
"SELECT count(*) FROM singer",
|
||||
"SELECT avg(age) , min(age) , max(age) FROM singer WHERE country = 'France'",
|
||||
"SELECT name FROM stadium WHERE stadium_id NOT IN (SELECT stadium_id FROM concert)",
|
||||
]
|
||||
|
||||
|
||||
TOKENIZER = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
|
||||
|
||||
|
||||
def _assert_exact_outputs(generated_texts: list[str], expected_outputs: list[str]) -> None:
|
||||
assert generated_texts == expected_outputs
|
||||
|
||||
|
||||
def _run_text_lora_sample(
|
||||
llm: vllm.LLM,
|
||||
lora_path: str,
|
||||
lora_id: int,
|
||||
) -> list[str]:
|
||||
prompts = [
|
||||
TEXT_PROMPT_TEMPLATE.format(query="How many singers do we have?"),
|
||||
TEXT_PROMPT_TEMPLATE.format(
|
||||
query=("What is the average, minimum, and maximum age of all singers from France?")
|
||||
),
|
||||
TEXT_PROMPT_TEMPLATE.format(query="What are the names of the stadiums without any concerts?"),
|
||||
]
|
||||
input_templates = []
|
||||
for prompt_text in prompts:
|
||||
messages = [{"role": "user", "content": prompt_text}]
|
||||
prompt = TOKENIZER.apply_chat_template(
|
||||
messages,
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
enable_thinking=False, # disable thinking
|
||||
)
|
||||
input_templates.append(prompt)
|
||||
|
||||
outputs = llm.generate(
|
||||
input_templates,
|
||||
vllm.SamplingParams(temperature=0.01, max_tokens=512),
|
||||
lora_request=LoRARequest(str(lora_id), lora_id, lora_path),
|
||||
)
|
||||
|
||||
generated_texts: list[str] = []
|
||||
for output in outputs:
|
||||
generated_text = output.outputs[0].text.strip()
|
||||
generated_texts.append(generated_text)
|
||||
print(f"Prompt: {output.prompt!r}, Generated text: {generated_text!r}")
|
||||
return generated_texts
|
||||
|
||||
|
||||
def _assert_qwen35_text_lora(
|
||||
llm: vllm.LLM,
|
||||
qwen35_text_lora_files: str,
|
||||
) -> None:
|
||||
generated_texts = _run_text_lora_sample(
|
||||
llm,
|
||||
qwen35_text_lora_files,
|
||||
TEXT_LORA_ID,
|
||||
)
|
||||
|
||||
_assert_exact_outputs(generated_texts, TEXT_EXPECTED_LORA_OUTPUT)
|
||||
|
||||
|
||||
def test_qwen35_text_lora(qwen35_text_lora_files):
|
||||
llm = vllm.LLM(
|
||||
model=MODEL_PATH,
|
||||
max_model_len=4096,
|
||||
enable_lora=True,
|
||||
max_loras=2,
|
||||
max_num_seqs=4,
|
||||
max_lora_rank=8,
|
||||
enforce_eager=True,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
|
||||
_assert_qwen35_text_lora(
|
||||
llm,
|
||||
qwen35_text_lora_files,
|
||||
)
|
||||
150
tests/e2e/pull_request/one_card/lora/test_qwen3_multi_loras.py
Normal file
150
tests/e2e/pull_request/one_card/lora/test_qwen3_multi_loras.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from unittest.mock import patch
|
||||
|
||||
from vllm import SamplingParams
|
||||
from vllm.lora.request import LoRARequest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from vllm_ascend.utils import enable_custom_op
|
||||
|
||||
enable_custom_op()
|
||||
|
||||
MODEL_PATH = "Qwen/Qwen3-0.6B"
|
||||
LORA_NAME_PATH_MAP = {
|
||||
"Alice": "charent/self_cognition_Alice",
|
||||
"Bob": "charent/self_cognition_Bob",
|
||||
"Cat": "charent/self_cognition_Bob", # same as Bob
|
||||
}
|
||||
|
||||
LORA_RANK = 8
|
||||
|
||||
LORA_TEST_PROMPTS = ["What is GitHub?", "Hi, tell me about you"]
|
||||
LORA_TEST_EXPECTED = [
|
||||
"GitHub is an open-source platform that provides a way to manage and develop software projects. It allows developers to store and manage code, collaborate on projects, and automate tasks.", # noqa: E501
|
||||
"I am Alice, an AI assistant developed by GitHub/Charent.", # noqa: E501
|
||||
]
|
||||
|
||||
|
||||
def format_chatml_messages(prompt: str):
|
||||
return [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
|
||||
|
||||
@patch.dict("os.environ", {"VLLM_USE_MODELSCOPE": "False"})
|
||||
def test_multi_loras_with_tp_sync():
|
||||
lora_name_id_map = {}
|
||||
increase_lora_id = 0
|
||||
|
||||
def make_add_lora_request(name: str, path: str):
|
||||
nonlocal increase_lora_id
|
||||
increase_lora_id += 1
|
||||
lora_name_id_map[name] = increase_lora_id
|
||||
|
||||
return LoRARequest(
|
||||
lora_name=name,
|
||||
lora_int_id=increase_lora_id,
|
||||
lora_path=path,
|
||||
)
|
||||
|
||||
vllm_model = VllmRunner(
|
||||
MODEL_PATH,
|
||||
enable_lora=True,
|
||||
# dtype="half",
|
||||
max_loras=2, # ensure max_loras < max_cpu_loras
|
||||
max_lora_rank=LORA_RANK,
|
||||
max_model_len=512,
|
||||
gpu_memory_utilization=0.9,
|
||||
enforce_eager=True,
|
||||
# tensor_parallel_size=2, # ensure tp >= 2
|
||||
max_cpu_loras=4, # ensure max_cpu_loras >= 2
|
||||
)
|
||||
llm = vllm_model.model
|
||||
|
||||
def run_check_lora(fn, args, expected: list):
|
||||
fn(args)
|
||||
assert set(llm.llm_engine.list_loras()) == set(expected)
|
||||
|
||||
# simulate add loras with CLI args
|
||||
# likes: `--lora-modules Alice=/path/to/Alice Bob=/path/to/Bob`
|
||||
run_check_lora(
|
||||
llm.llm_engine.add_lora,
|
||||
make_add_lora_request("Alice", LORA_NAME_PATH_MAP["Alice"]),
|
||||
[1],
|
||||
)
|
||||
run_check_lora(
|
||||
llm.llm_engine.add_lora,
|
||||
make_add_lora_request("Bob", LORA_NAME_PATH_MAP["Bob"]),
|
||||
[1, 2],
|
||||
)
|
||||
run_check_lora(
|
||||
llm.llm_engine.add_lora,
|
||||
make_add_lora_request("Cat", LORA_NAME_PATH_MAP["Cat"]),
|
||||
[1, 2, 3],
|
||||
)
|
||||
|
||||
# set temperature = 0 for greedy search
|
||||
sampling_params = SamplingParams(temperature=0, max_tokens=64)
|
||||
|
||||
def call_llm_get_outputs(prompt: str, lora_name: str):
|
||||
lora_request = LoRARequest(
|
||||
lora_name=lora_name,
|
||||
lora_int_id=lora_name_id_map[lora_name],
|
||||
lora_path=LORA_NAME_PATH_MAP[lora_name],
|
||||
)
|
||||
messages = format_chatml_messages(prompt)
|
||||
outputs = llm.chat(
|
||||
[messages],
|
||||
sampling_params,
|
||||
chat_template_kwargs={"enable_thinking": False}, # for those loras, ensure enable_thinking=False
|
||||
lora_request=lora_request,
|
||||
use_tqdm=False,
|
||||
)
|
||||
output_text = outputs[0].outputs[0].text
|
||||
return output_text
|
||||
|
||||
def reload_lora(name: str):
|
||||
"""
|
||||
reload a lora to simulate the case:
|
||||
setting `VLLM_ALLOW_RUNTIME_LORA_UPDATING=true`
|
||||
for dynamic lora loading and unloading
|
||||
"""
|
||||
remove_lora_response = llm.llm_engine.remove_lora(lora_id=lora_name_id_map[name])
|
||||
|
||||
add_lora_response = llm.llm_engine.add_lora(make_add_lora_request(name, LORA_NAME_PATH_MAP[name]))
|
||||
|
||||
print(f"{remove_lora_response=}, {add_lora_response=}")
|
||||
|
||||
def check_outputs(outputs: str, expected: str, prompt: str):
|
||||
print(f"{prompt=}.\n{expected=}\n{outputs=}")
|
||||
print("\n----------------------------\n")
|
||||
assert outputs == expected
|
||||
|
||||
for prompt, expected_output in zip(LORA_TEST_PROMPTS, LORA_TEST_EXPECTED):
|
||||
output_text = call_llm_get_outputs(prompt, "Alice")
|
||||
check_outputs(output_text, expected_output, prompt)
|
||||
|
||||
# call Bob, ignore what it is output
|
||||
call_llm_get_outputs(prompt, "Bob")
|
||||
print("After call Bob:")
|
||||
|
||||
# call Alice
|
||||
output_text = call_llm_get_outputs(prompt, "Alice")
|
||||
check_outputs(output_text, expected_output, prompt)
|
||||
|
||||
# reload Bob Lora
|
||||
reload_lora("Bob")
|
||||
print("After reload Bob:")
|
||||
|
||||
# call Alice
|
||||
output_text = call_llm_get_outputs(prompt, "Alice")
|
||||
check_outputs(output_text, expected_output, prompt)
|
||||
|
||||
# reload Alice Lora
|
||||
reload_lora("Alice")
|
||||
print("After reload Alice:")
|
||||
|
||||
output_text = call_llm_get_outputs(prompt, "Alice")
|
||||
check_outputs(output_text, expected_output, prompt)
|
||||
74
tests/e2e/pull_request/one_card/lora/test_qwen3_reranker_lora.py
Executable file
74
tests/e2e/pull_request/one_card/lora/test_qwen3_reranker_lora.py
Executable file
@@ -0,0 +1,74 @@
|
||||
from pathlib import Path
|
||||
|
||||
from vllm import LLM
|
||||
|
||||
model_name = "Qwen/Qwen3-Reranker-0.6B"
|
||||
|
||||
|
||||
def get_llm() -> LLM:
|
||||
"""
|
||||
Initializes and returns the LLM model for Qwen3-Reranker.
|
||||
|
||||
Returns:
|
||||
LLM: Configured vLLM instance for reranking tasks.
|
||||
|
||||
Note:
|
||||
This function loads the ORIGINAL Qwen3-Reranker model with specific
|
||||
overrides to make it compatible with vLLM's score API.
|
||||
"""
|
||||
return LLM(
|
||||
# Specify the original model from HuggingFace
|
||||
model=model_name,
|
||||
# Use pooling runner for score task
|
||||
runner="pooling",
|
||||
# HuggingFace model configuration overrides required for compatibility
|
||||
hf_overrides={
|
||||
# Manually route to sequence classification architecture
|
||||
# This tells vLLM to use Qwen3ForSequenceClassification instead of
|
||||
# the default Qwen3ForCausalLM
|
||||
"architectures": ["Qwen3ForSequenceClassification"],
|
||||
# Specify which token logits to extract from the language model head
|
||||
# The original reranker uses "no" and "yes" token logits for scoring
|
||||
"classifier_from_token": ["no", "yes"],
|
||||
# Enable special handling for original Qwen3-Reranker models
|
||||
# This flag triggers conversion logic that transforms the two token
|
||||
# vectors into a single classification vector
|
||||
"is_original_qwen3_reranker": True,
|
||||
},
|
||||
enable_lora=True,
|
||||
)
|
||||
|
||||
|
||||
def test_reranker_models_lora():
|
||||
# Load the Jinja template for formatting query-document pairs
|
||||
# The template ensures proper formatting for the reranker model
|
||||
template_home = Path(__file__).parents[1] / "pooling" / "template"
|
||||
template_path = "qwen3_reranker.jinja"
|
||||
chat_template = (template_home / template_path).read_text()
|
||||
|
||||
# Sample queries for testing the reranker
|
||||
queries = [
|
||||
"What is the capital of China?",
|
||||
"Explain gravity",
|
||||
]
|
||||
|
||||
# Corresponding documents to be scored against each query
|
||||
documents = [
|
||||
"The capital of China is Beijing.",
|
||||
"Gravity is a force that attracts two bodies towards each other. It gives weight to physical objects and is "
|
||||
"responsible for the movement of planets around the sun.",
|
||||
]
|
||||
|
||||
# Initialize the LLM model with the original Qwen3-Reranker configuration
|
||||
llm = get_llm()
|
||||
|
||||
# Compute relevance scores for each query-document pair
|
||||
# The score() method returns a relevance score for each pair
|
||||
# Higher scores indicate better relevance
|
||||
outputs = llm.score(queries, documents, chat_template=chat_template)
|
||||
|
||||
# Extract and print the relevance scores from the outputs
|
||||
# Each output contains a score representing query-document relevance
|
||||
print("-" * 30)
|
||||
print("Relevance scores:", [output.outputs.score for output in outputs])
|
||||
print("-" * 30)
|
||||
171
tests/e2e/pull_request/one_card/model_runner_v2/test_basic.py
Normal file
171
tests/e2e/pull_request/one_card/model_runner_v2/test_basic.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from vllm_ascend.utils import vllm_version_is
|
||||
|
||||
MODELS = ["Qwen/Qwen3-0.6B", "vllm-ascend/DeepSeek-V2-Lite-W8A8"]
|
||||
|
||||
MAIN_MODELS = ["LLM-Research/Meta-Llama-3.1-8B-Instruct"]
|
||||
EGALE_MODELS = ["vllm-ascend/EAGLE-LLaMA3.1-Instruct-8B"]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
vllm_version_is("0.23.0"),
|
||||
reason="v2 model runner patches not supported on v0.23.0",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(True, reason="Fix me, it's broken after CANN and trition-ascend are upgraded.")
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [32])
|
||||
@pytest.mark.parametrize("enforce_eager", [True])
|
||||
@patch.dict(os.environ, {"VLLM_USE_V2_MODEL_RUNNER": "1"})
|
||||
def test_qwen3_dense_eager_mode(
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
enforce_eager: bool,
|
||||
) -> None:
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
sampling_params = SamplingParams(
|
||||
max_tokens=max_tokens,
|
||||
temperature=0.5,
|
||||
logprobs=2,
|
||||
prompt_logprobs=2,
|
||||
logit_bias={0: -1.0, 1: 0.5},
|
||||
min_p=0.01,
|
||||
bad_words=["the", " the"],
|
||||
)
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
enforce_eager=enforce_eager,
|
||||
async_scheduling=True,
|
||||
) as runner:
|
||||
runner.model.generate(prompts, sampling_params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MAIN_MODELS)
|
||||
@pytest.mark.parametrize("eagle_model", EGALE_MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [32])
|
||||
@pytest.mark.parametrize("enforce_eager", [False])
|
||||
@pytest.mark.parametrize(
|
||||
"compilation_config",
|
||||
[
|
||||
pytest.param(
|
||||
{"cudagraph_mode": "FULL_DECODE_ONLY", "cudagraph_capture_sizes": [4, 8]},
|
||||
id="full_decode_only",
|
||||
),
|
||||
pytest.param({}, id="default_full_and_piecewise"),
|
||||
],
|
||||
)
|
||||
@patch.dict(os.environ, {"VLLM_USE_V2_MODEL_RUNNER": "1"})
|
||||
def test_egale_spec_decoding(
|
||||
model: str,
|
||||
eagle_model: str,
|
||||
max_tokens: int,
|
||||
enforce_eager: bool,
|
||||
compilation_config: dict,
|
||||
) -> None:
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
sampling_params = SamplingParams(max_tokens=max_tokens, temperature=0.0)
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
enforce_eager=enforce_eager,
|
||||
async_scheduling=True,
|
||||
speculative_config={
|
||||
"model": eagle_model,
|
||||
"method": "eagle",
|
||||
"num_speculative_tokens": 3,
|
||||
},
|
||||
compilation_config=compilation_config,
|
||||
) as runner:
|
||||
runner.model.generate(prompts, sampling_params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [32])
|
||||
@pytest.mark.parametrize("enforce_eager", [False])
|
||||
@pytest.mark.parametrize(
|
||||
"compilation_config",
|
||||
[
|
||||
pytest.param({"cudagraph_mode": "FULL_DECODE_ONLY"}, id="full_decode_only"),
|
||||
pytest.param({}, id="default_full_and_piecewise"),
|
||||
],
|
||||
)
|
||||
@patch.dict(os.environ, {"VLLM_USE_V2_MODEL_RUNNER": "1"})
|
||||
def test_qwen3_dense_graph_mode(
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
enforce_eager: bool,
|
||||
compilation_config: dict,
|
||||
) -> None:
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
sampling_params = SamplingParams(max_tokens=max_tokens, temperature=0.0)
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
enforce_eager=enforce_eager,
|
||||
compilation_config=compilation_config,
|
||||
) as runner:
|
||||
outputs = runner.model.generate(prompts, sampling_params)
|
||||
|
||||
if model != "Qwen/Qwen3-0.6B":
|
||||
return
|
||||
|
||||
expected_outputs = [
|
||||
" Lina. I'm a 22-year-old student from China.",
|
||||
" the same as the president of the United Nations. This is because the president",
|
||||
" Paris. The capital of France is also the capital of the Republic of France",
|
||||
" not just about the technology itself but also about the human aspect-how we",
|
||||
]
|
||||
|
||||
matches = 0
|
||||
misses = 0
|
||||
for output, expected_output in zip(outputs, expected_outputs):
|
||||
if output.outputs[0].text[:10] == expected_output[:10]:
|
||||
matches += 1
|
||||
else:
|
||||
misses += 1
|
||||
print(f"output: {output.outputs[0].text}")
|
||||
print(f"expected_output: {expected_output}")
|
||||
|
||||
assert misses == 0
|
||||
171
tests/e2e/pull_request/one_card/model_runner_v2/test_uva.py
Normal file
171
tests/e2e/pull_request/one_card/model_runner_v2/test_uva.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from vllm_ascend.utils import vllm_version_is
|
||||
|
||||
MODELS = ["Qwen/Qwen3-0.6B", "vllm-ascend/DeepSeek-V2-Lite-W8A8"]
|
||||
|
||||
MAIN_MODELS = ["LLM-Research/Meta-Llama-3.1-8B-Instruct"]
|
||||
EGALE_MODELS = ["vllm-ascend/EAGLE-LLaMA3.1-Instruct-8B"]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
vllm_version_is("0.23.0"),
|
||||
reason="v2 model runner patches not supported on v0.23.0",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(True, reason="Fix me, it's broken after CANN and trition-ascend are upgraded.")
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [32])
|
||||
@pytest.mark.parametrize("enforce_eager", [True])
|
||||
@patch.dict(os.environ, {"VLLM_USE_V2_MODEL_RUNNER": "1", "PYTORCH_NPU_ALLOC_CONF": "pinned_mem_register:True"})
|
||||
def test_qwen3_dense_eager_mode(
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
enforce_eager: bool,
|
||||
) -> None:
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
sampling_params = SamplingParams(
|
||||
max_tokens=max_tokens,
|
||||
temperature=0.5,
|
||||
logprobs=2,
|
||||
prompt_logprobs=2,
|
||||
logit_bias={0: -1.0, 1: 0.5},
|
||||
min_p=0.01,
|
||||
bad_words=["the", " the"],
|
||||
)
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
enforce_eager=enforce_eager,
|
||||
async_scheduling=True,
|
||||
) as runner:
|
||||
runner.model.generate(prompts, sampling_params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MAIN_MODELS)
|
||||
@pytest.mark.parametrize("eagle_model", EGALE_MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [32])
|
||||
@pytest.mark.parametrize("enforce_eager", [False])
|
||||
@pytest.mark.parametrize(
|
||||
"compilation_config",
|
||||
[
|
||||
pytest.param(
|
||||
{"cudagraph_mode": "FULL_DECODE_ONLY", "cudagraph_capture_sizes": [4, 8]},
|
||||
id="full_decode_only",
|
||||
),
|
||||
pytest.param({}, id="default_full_and_piecewise"),
|
||||
],
|
||||
)
|
||||
@patch.dict(os.environ, {"VLLM_USE_V2_MODEL_RUNNER": "1", "PYTORCH_NPU_ALLOC_CONF": "pinned_mem_register:True"})
|
||||
def test_egale_spec_decoding(
|
||||
model: str,
|
||||
eagle_model: str,
|
||||
max_tokens: int,
|
||||
enforce_eager: bool,
|
||||
compilation_config: dict,
|
||||
) -> None:
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
sampling_params = SamplingParams(max_tokens=max_tokens, temperature=0.0)
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
enforce_eager=enforce_eager,
|
||||
async_scheduling=True,
|
||||
speculative_config={
|
||||
"model": eagle_model,
|
||||
"method": "eagle",
|
||||
"num_speculative_tokens": 3,
|
||||
},
|
||||
compilation_config=compilation_config,
|
||||
) as runner:
|
||||
runner.model.generate(prompts, sampling_params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [32])
|
||||
@pytest.mark.parametrize("enforce_eager", [False])
|
||||
@pytest.mark.parametrize(
|
||||
"compilation_config",
|
||||
[
|
||||
pytest.param({"cudagraph_mode": "FULL_DECODE_ONLY"}, id="full_decode_only"),
|
||||
pytest.param({}, id="default_full_and_piecewise"),
|
||||
],
|
||||
)
|
||||
@patch.dict(os.environ, {"VLLM_USE_V2_MODEL_RUNNER": "1", "PYTORCH_NPU_ALLOC_CONF": "pinned_mem_register:True"})
|
||||
def test_qwen3_dense_graph_mode(
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
enforce_eager: bool,
|
||||
compilation_config: dict,
|
||||
) -> None:
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
sampling_params = SamplingParams(max_tokens=max_tokens, temperature=0.0)
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
enforce_eager=enforce_eager,
|
||||
compilation_config=compilation_config,
|
||||
) as runner:
|
||||
outputs = runner.model.generate(prompts, sampling_params)
|
||||
|
||||
if model != "Qwen/Qwen3-0.6B":
|
||||
return
|
||||
|
||||
expected_outputs = [
|
||||
" Lina. I'm a 22-year-old student from China.",
|
||||
" the same as the president of the United Nations. This is because the president",
|
||||
" Paris. The capital of France is also the capital of the Republic of France",
|
||||
" not just about the technology itself but also about the human aspect-how we",
|
||||
]
|
||||
|
||||
matches = 0
|
||||
misses = 0
|
||||
for output, expected_output in zip(outputs, expected_outputs):
|
||||
if output.outputs[0].text[:10] == expected_output[:10]:
|
||||
matches += 1
|
||||
else:
|
||||
misses += 1
|
||||
print(f"output: {output.outputs[0].text}")
|
||||
print(f"expected_output: {expected_output}")
|
||||
|
||||
assert misses == 0
|
||||
0
tests/e2e/pull_request/one_card/pooling/__init__.py
Normal file
0
tests/e2e/pull_request/one_card/pooling/__init__.py
Normal file
11
tests/e2e/pull_request/one_card/pooling/template/qwen3_reranker.jinja
Executable file
11
tests/e2e/pull_request/one_card/pooling/template/qwen3_reranker.jinja
Executable file
@@ -0,0 +1,11 @@
|
||||
<|im_start|>system
|
||||
Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be "yes" or "no".<|im_end|>
|
||||
<|im_start|>user
|
||||
<Instruct>: {{ messages | selectattr("role", "eq", "system") | map(attribute="content") | first | default("Given a web search query, retrieve relevant passages that answer the query") }}
|
||||
<Query>: {{ messages | selectattr("role", "eq", "query") | map(attribute="content") | first }}
|
||||
<Document>: {{ messages | selectattr("role", "eq", "document") | map(attribute="content") | first }}<|im_end|>
|
||||
<|im_start|>assistant
|
||||
<think>
|
||||
|
||||
</think>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import huggingface_hub
|
||||
import torch
|
||||
from modelscope import snapshot_download # type: ignore[import-untyped]
|
||||
from transformers import AutoModelForSequenceClassification
|
||||
|
||||
from tests.e2e.conftest import (
|
||||
HfRunner,
|
||||
VllmRunner,
|
||||
cleanup_dist_env_and_memory,
|
||||
wait_until_npu_memory_free,
|
||||
)
|
||||
|
||||
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.7)
|
||||
def test_qwen_pooling_classify_correctness() -> None:
|
||||
model_name = snapshot_download(
|
||||
"Howeee/Qwen2.5-1.5B-apeach",
|
||||
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
||||
)
|
||||
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is what",
|
||||
]
|
||||
|
||||
with HfRunner(model_name, dtype="float32", auto_cls=AutoModelForSequenceClassification) as hf_runner:
|
||||
hf_outputs = hf_runner.classify(prompts)
|
||||
cleanup_dist_env_and_memory()
|
||||
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
runner="pooling",
|
||||
max_model_len=None,
|
||||
cudagraph_capture_sizes=[4],
|
||||
) as vllm_runner:
|
||||
vllm_outputs = vllm_runner.classify(prompts)
|
||||
|
||||
for hf_output, vllm_output in zip(hf_outputs, vllm_outputs):
|
||||
hf_output = torch.tensor(hf_output)
|
||||
vllm_output = torch.tensor(vllm_output)
|
||||
assert torch.allclose(hf_output, vllm_output, 1e-2)
|
||||
135
tests/e2e/pull_request/one_card/pooling/test_embedding.py
Normal file
135
tests/e2e/pull_request/one_card/pooling/test_embedding.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
import huggingface_hub
|
||||
import pytest
|
||||
from modelscope import snapshot_download # type: ignore[import-untyped]
|
||||
|
||||
from tests.e2e.conftest import HfRunner, VllmRunner
|
||||
from tests.e2e.utils import check_embeddings_close
|
||||
|
||||
MODELS = [
|
||||
"Qwen/Qwen3-Embedding-0.6B", # lasttoken
|
||||
"intfloat/multilingual-e5-small", # mean_tokens
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
def test_embed_models_correctness(model: str):
|
||||
queries = ["What is the capital of China?", "Explain gravity"]
|
||||
|
||||
model_name = snapshot_download(
|
||||
model,
|
||||
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
||||
)
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
runner="pooling",
|
||||
max_model_len=None,
|
||||
cudagraph_capture_sizes=[4],
|
||||
) as vllm_runner:
|
||||
vllm_outputs = vllm_runner.embed(queries)
|
||||
|
||||
with HfRunner(
|
||||
model_name,
|
||||
dtype="float32",
|
||||
is_sentence_transformer=True,
|
||||
) as hf_runner:
|
||||
hf_outputs = hf_runner.encode(queries)
|
||||
|
||||
check_embeddings_close(
|
||||
embeddings_0_lst=hf_outputs,
|
||||
embeddings_1_lst=vllm_outputs,
|
||||
name_0="hf",
|
||||
name_1="vllm",
|
||||
tol=1e-2,
|
||||
)
|
||||
|
||||
|
||||
def test_causal_embed_models_using_prefix_caching_correctness():
|
||||
# This test is to verify the correctness of prefix caching for embedding models.
|
||||
# We compare the outputs of vLLM with and without prefix caching enabled, and check if they are close enough.
|
||||
# We set the input query to be very long to make sure prefix caching is triggered.
|
||||
queries = ["What is the capital of China?" * 256, "Explain gravity"]
|
||||
|
||||
model_name = snapshot_download(
|
||||
"Qwen/Qwen3-Embedding-0.6B",
|
||||
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
||||
)
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
runner="pooling",
|
||||
max_model_len=None,
|
||||
cudagraph_capture_sizes=[4],
|
||||
enable_prefix_caching=True,
|
||||
) as vllm_runner_using_caching:
|
||||
vllm_outputs_without_caching = vllm_runner_using_caching.embed(queries)
|
||||
vllm_outputs_with_caching = vllm_runner_using_caching.embed(queries)
|
||||
|
||||
check_embeddings_close(
|
||||
embeddings_0_lst=vllm_outputs_without_caching,
|
||||
embeddings_1_lst=vllm_outputs_with_caching,
|
||||
name_0="without_caching",
|
||||
name_1="with_caching",
|
||||
tol=1e-2,
|
||||
)
|
||||
|
||||
|
||||
def test_bge_m3_correctness():
|
||||
queries = ["What is the capital of China?", "Explain gravity"]
|
||||
|
||||
model_name = snapshot_download(
|
||||
"BAAI/bge-m3",
|
||||
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
||||
)
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
runner="pooling",
|
||||
cudagraph_capture_sizes=[4],
|
||||
) as vllm_aclgraph_runner:
|
||||
vllm_aclgraph_outputs = vllm_aclgraph_runner.embed(queries)
|
||||
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
runner="pooling",
|
||||
enforce_eager=True,
|
||||
) as vllm_runner:
|
||||
vllm_eager_outputs = vllm_runner.embed(queries)
|
||||
|
||||
with HfRunner(
|
||||
model_name,
|
||||
dtype="float32",
|
||||
is_sentence_transformer=True,
|
||||
) as hf_runner:
|
||||
hf_outputs = hf_runner.encode(queries)
|
||||
|
||||
check_embeddings_close(
|
||||
embeddings_0_lst=hf_outputs,
|
||||
embeddings_1_lst=vllm_eager_outputs,
|
||||
name_0="hf",
|
||||
name_1="vllm",
|
||||
tol=1e-2,
|
||||
)
|
||||
|
||||
check_embeddings_close(
|
||||
embeddings_0_lst=vllm_eager_outputs,
|
||||
embeddings_1_lst=vllm_aclgraph_outputs,
|
||||
name_0="eager",
|
||||
name_1="aclgraph",
|
||||
tol=1e-2,
|
||||
)
|
||||
167
tests/e2e/pull_request/one_card/pooling/test_scoring.py
Normal file
167
tests/e2e/pull_request/one_card/pooling/test_scoring.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import huggingface_hub
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from modelscope import snapshot_download # type: ignore[import-untyped]
|
||||
|
||||
from tests.e2e.conftest import HfRunner, VllmRunner
|
||||
|
||||
CROSS_ENCODER_MODELS = [
|
||||
"dengcao/ms-marco-MiniLM-L6-v2", # Bert
|
||||
"BAAI/bge-reranker-v2-m3", # Roberta
|
||||
]
|
||||
|
||||
EMBEDDING_MODELS = [
|
||||
"sentence-transformers/all-MiniLM-L12-v2",
|
||||
]
|
||||
|
||||
TEXTS_1 = [
|
||||
"What is the capital of France?",
|
||||
"What is the capital of Germany?",
|
||||
]
|
||||
|
||||
TEXTS_2 = [
|
||||
"The capital of France is Paris.",
|
||||
"The capital of Germany is Berlin.",
|
||||
]
|
||||
|
||||
DTYPE = "half"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=CROSS_ENCODER_MODELS)
|
||||
def model_name(request):
|
||||
yield snapshot_download(
|
||||
request.param,
|
||||
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
||||
)
|
||||
|
||||
|
||||
def test_cross_encoder_score_1_to_1(model_name):
|
||||
text_pair = [TEXTS_1[0], TEXTS_2[0]]
|
||||
|
||||
with HfRunner(model_name, dtype=DTYPE, is_cross_encoder=True) as hf_model:
|
||||
hf_outputs = hf_model.predict([text_pair]).tolist()
|
||||
|
||||
with VllmRunner(
|
||||
model_name, runner="pooling", dtype=DTYPE, cudagraph_capture_sizes=[4], max_model_len=None
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.score(text_pair[0], text_pair[1])
|
||||
|
||||
assert len(vllm_outputs) == 1
|
||||
assert len(hf_outputs) == 1
|
||||
|
||||
assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.01)
|
||||
|
||||
|
||||
def test_cross_encoder_score_1_to_N(model_name):
|
||||
text_pairs = [
|
||||
[TEXTS_1[0], TEXTS_2[0]],
|
||||
[TEXTS_1[0], TEXTS_2[1]],
|
||||
]
|
||||
|
||||
with HfRunner(model_name, dtype=DTYPE, is_cross_encoder=True) as hf_model:
|
||||
hf_outputs = hf_model.predict(text_pairs).tolist()
|
||||
|
||||
with VllmRunner(
|
||||
model_name, runner="pooling", dtype=DTYPE, cudagraph_capture_sizes=[4], max_model_len=None
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.score(TEXTS_1[0], TEXTS_2)
|
||||
|
||||
assert len(vllm_outputs) == 2
|
||||
assert len(hf_outputs) == 2
|
||||
|
||||
assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.01)
|
||||
assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.01)
|
||||
|
||||
|
||||
def test_cross_encoder_score_N_to_N(model_name):
|
||||
text_pairs = [
|
||||
[TEXTS_1[0], TEXTS_2[0]],
|
||||
[TEXTS_1[1], TEXTS_2[1]],
|
||||
]
|
||||
|
||||
with HfRunner(model_name, dtype=DTYPE, is_cross_encoder=True) as hf_model:
|
||||
hf_outputs = hf_model.predict(text_pairs).tolist()
|
||||
|
||||
with VllmRunner(
|
||||
model_name, runner="pooling", dtype=DTYPE, cudagraph_capture_sizes=[4], max_model_len=None
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.score(TEXTS_1, TEXTS_2)
|
||||
|
||||
assert len(vllm_outputs) == 2
|
||||
assert len(hf_outputs) == 2
|
||||
|
||||
assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.01)
|
||||
assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.01)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=EMBEDDING_MODELS)
|
||||
def emb_model_name(request):
|
||||
yield snapshot_download(
|
||||
request.param,
|
||||
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
||||
)
|
||||
|
||||
|
||||
def test_embedding_score_1_to_1(emb_model_name):
|
||||
text_pair = [TEXTS_1[0], TEXTS_2[0]]
|
||||
|
||||
with HfRunner(emb_model_name, dtype=DTYPE, is_sentence_transformer=True) as hf_model:
|
||||
hf_embeddings = hf_model.encode(text_pair)
|
||||
hf_outputs = [F.cosine_similarity(*map(torch.tensor, hf_embeddings), dim=0)]
|
||||
|
||||
with VllmRunner(
|
||||
emb_model_name, runner="pooling", dtype=DTYPE, cudagraph_capture_sizes=[4], max_model_len=None
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.score(text_pair[0], text_pair[1])
|
||||
|
||||
assert len(vllm_outputs) == 1
|
||||
assert len(hf_outputs) == 1
|
||||
|
||||
assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.01)
|
||||
|
||||
|
||||
def test_embedding_score_1_to_N(emb_model_name):
|
||||
text_pairs = [
|
||||
[TEXTS_1[0], TEXTS_2[0]],
|
||||
[TEXTS_1[0], TEXTS_2[1]],
|
||||
]
|
||||
|
||||
with HfRunner(emb_model_name, dtype=DTYPE, is_sentence_transformer=True) as hf_model:
|
||||
hf_embeddings = [hf_model.encode(text_pair) for text_pair in text_pairs]
|
||||
hf_outputs = [F.cosine_similarity(*map(torch.tensor, pair), dim=0) for pair in hf_embeddings]
|
||||
|
||||
with VllmRunner(
|
||||
emb_model_name, runner="pooling", dtype=DTYPE, cudagraph_capture_sizes=[4], max_model_len=None
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.score(TEXTS_1[0], TEXTS_2)
|
||||
|
||||
assert len(vllm_outputs) == 2
|
||||
assert len(hf_outputs) == 2
|
||||
|
||||
assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.01)
|
||||
assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.01)
|
||||
|
||||
|
||||
def test_embedding_score_N_to_N(emb_model_name):
|
||||
text_pairs = [
|
||||
[TEXTS_1[0], TEXTS_2[0]],
|
||||
[TEXTS_1[1], TEXTS_2[1]],
|
||||
]
|
||||
|
||||
with HfRunner(emb_model_name, dtype=DTYPE, is_sentence_transformer=True) as hf_model:
|
||||
hf_embeddings = [hf_model.encode(text_pair) for text_pair in text_pairs]
|
||||
hf_outputs = [F.cosine_similarity(*map(torch.tensor, pair), dim=0) for pair in hf_embeddings]
|
||||
|
||||
with VllmRunner(
|
||||
emb_model_name, runner="pooling", dtype=DTYPE, cudagraph_capture_sizes=[4], max_model_len=None
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.score(TEXTS_1, TEXTS_2)
|
||||
|
||||
assert len(vllm_outputs) == 2
|
||||
assert len(hf_outputs) == 2
|
||||
|
||||
assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.01)
|
||||
assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.01)
|
||||
53
tests/e2e/pull_request/one_card/spec_decode/conftest.py
Normal file
53
tests/e2e/pull_request/one_card/spec_decode/conftest.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_prompts() -> list[list[dict[str, Any]]]:
|
||||
prompt_types = ["repeat", "sentence"]
|
||||
num_prompts = 100
|
||||
prompts = []
|
||||
|
||||
random.seed(0)
|
||||
random_prompt_type_choices = random.choices(prompt_types, k=num_prompts)
|
||||
|
||||
for kind in random_prompt_type_choices:
|
||||
word_choices = ["test", "temp", "hello", "where"]
|
||||
word = random.choice(word_choices)
|
||||
if kind == "repeat":
|
||||
prompt = f"""
|
||||
please repeat the word '{word}' 10 times.
|
||||
give no other output than the word at least ten times in a row,
|
||||
in lowercase with spaces between each word and without quotes.
|
||||
"""
|
||||
elif kind == "sentence":
|
||||
prompt = f"""
|
||||
please give a ten-word sentence that
|
||||
uses the word {word} at least once.
|
||||
give no other output than that simple sentence without quotes.
|
||||
"""
|
||||
else:
|
||||
raise ValueError(f"Unknown prompt type: {kind}")
|
||||
prompts.append([{"role": "user", "content": prompt}])
|
||||
|
||||
return prompts
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sampling_config() -> SamplingParams:
|
||||
return SamplingParams(temperature=0, max_tokens=10, ignore_eos=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model_name() -> str:
|
||||
return "LLM-Research/Meta-Llama-3.1-8B-Instruct"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vl_model_name() -> str:
|
||||
return "Qwen/Qwen3-VL-8B-Instruct"
|
||||
81
tests/e2e/pull_request/one_card/spec_decode/test_dflash.py
Normal file
81
tests/e2e/pull_request/one_card/spec_decode/test_dflash.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from transformers import AutoTokenizer
|
||||
from vllm import SamplingParams
|
||||
from vllm.config import CompilationConfig
|
||||
from vllm.v1.metrics.reader import Counter, Vector
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from tests.e2e.pull_request.one_card.spec_decode.utils import BASELINES, DFLASH, calculate_acceptance_per_pos
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", DFLASH.keys())
|
||||
@pytest.mark.parametrize("num_speculative_tokens", [8])
|
||||
def test_dflash_acceptance(
|
||||
method: str,
|
||||
num_speculative_tokens: int,
|
||||
):
|
||||
main_model_name = DFLASH[method]["main"]
|
||||
spec_model_name = DFLASH[method]["spec"]
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
main_model_name,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0,
|
||||
ignore_eos=False,
|
||||
max_tokens=256,
|
||||
)
|
||||
|
||||
prompts = [{"role": "user", "content": "Hello, your name is"}]
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[prompt],
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
enable_thinking=False,
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
||||
speculative_config = {
|
||||
"method": "dflash",
|
||||
"model": spec_model_name,
|
||||
"num_speculative_tokens": num_speculative_tokens,
|
||||
}
|
||||
|
||||
compilation_config = CompilationConfig(cudagraph_mode="FULL_DECODE_ONLY", cudagraph_capture_sizes=[9, 18])
|
||||
|
||||
with VllmRunner(
|
||||
main_model_name,
|
||||
max_model_len=4096,
|
||||
disable_log_stats=False,
|
||||
tensor_parallel_size=1,
|
||||
max_num_seqs=256,
|
||||
distributed_executor_backend="mp",
|
||||
gpu_memory_utilization=0.8,
|
||||
speculative_config=speculative_config,
|
||||
compilation_config=compilation_config,
|
||||
enable_prefix_caching=False,
|
||||
) as llm:
|
||||
outputs = llm.model.generate(prompts, sampling_params)
|
||||
metrics = llm.model.get_metrics()
|
||||
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
output_tokens = output.outputs[0].token_ids
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
print(f"Output tokens: {output_tokens}")
|
||||
|
||||
acceptance_per_pos = calculate_acceptance_per_pos(metrics, num_speculative_tokens, Counter, Vector)
|
||||
golden = BASELINES[method]
|
||||
|
||||
match = all(abs(a - b) < 0.1 for a, b in zip(acceptance_per_pos, golden))
|
||||
if not match:
|
||||
print(f"acceptance_per_pos: {acceptance_per_pos}")
|
||||
print(f"golden: {golden}")
|
||||
|
||||
assert match
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from transformers import AutoTokenizer
|
||||
from vllm import SamplingParams
|
||||
from vllm.config import CompilationConfig
|
||||
from vllm.tokenizers.registry import resolve_tokenizer_args
|
||||
from vllm.v1.metrics.reader import Counter, Vector
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from tests.e2e.pull_request.one_card.spec_decode.utils import (
|
||||
BASELINES,
|
||||
DRAFT_PARALLEL_MODELS,
|
||||
calculate_acceptance_per_pos,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", DRAFT_PARALLEL_MODELS.keys())
|
||||
@pytest.mark.parametrize("num_speculative_tokens", [8])
|
||||
@pytest.mark.parametrize("draft_tensor_parallel_size", [1])
|
||||
def test_parallel_drafting_acceptance(
|
||||
method: str,
|
||||
num_speculative_tokens: int,
|
||||
draft_tensor_parallel_size: None | int,
|
||||
):
|
||||
main_model_name = DRAFT_PARALLEL_MODELS[method]["main"]
|
||||
spec_model_name = DRAFT_PARALLEL_MODELS[method]["spec"]
|
||||
|
||||
tokenizer_path = resolve_tokenizer_args(main_model_name)[1]
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
tokenizer_path,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0,
|
||||
ignore_eos=False,
|
||||
max_tokens=256,
|
||||
)
|
||||
|
||||
prompts = [{"role": "user", "content": "Hello, your name is"}]
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[prompt],
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
||||
speculative_config = {
|
||||
"method": "draft_model",
|
||||
"model": spec_model_name,
|
||||
"num_speculative_tokens": num_speculative_tokens,
|
||||
"draft_tensor_parallel_size": draft_tensor_parallel_size,
|
||||
"parallel_drafting": True,
|
||||
}
|
||||
|
||||
compilation_config = CompilationConfig(
|
||||
cudagraph_mode="PIECEWISE",
|
||||
cudagraph_capture_sizes=[12],
|
||||
)
|
||||
|
||||
with VllmRunner(
|
||||
main_model_name,
|
||||
max_model_len=4096,
|
||||
disable_log_stats=False,
|
||||
tensor_parallel_size=1,
|
||||
max_num_seqs=256,
|
||||
distributed_executor_backend="mp",
|
||||
gpu_memory_utilization=0.8,
|
||||
speculative_config=speculative_config,
|
||||
compilation_config=compilation_config,
|
||||
enable_prefix_caching=False,
|
||||
) as llm:
|
||||
outputs = llm.model.generate(prompts, sampling_params)
|
||||
metrics = llm.model.get_metrics()
|
||||
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
output_tokens = output.outputs[0].token_ids
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
print(f"Output tokens: {output_tokens}")
|
||||
|
||||
acceptance_per_pos = calculate_acceptance_per_pos(metrics, num_speculative_tokens, Counter, Vector)
|
||||
golden = BASELINES[method]
|
||||
|
||||
match = all(abs(a - b) < 0.1 for a, b in zip(acceptance_per_pos, golden))
|
||||
if not match:
|
||||
print(f"acceptance_per_pos: {acceptance_per_pos}")
|
||||
print(f"golden: {golden}")
|
||||
|
||||
assert match
|
||||
110
tests/e2e/pull_request/one_card/spec_decode/test_eagle.py
Normal file
110
tests/e2e/pull_request/one_card/spec_decode/test_eagle.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from transformers import AutoTokenizer
|
||||
from vllm import SamplingParams
|
||||
from vllm.config import CompilationConfig
|
||||
from vllm.tokenizers.registry import resolve_tokenizer_args
|
||||
from vllm.v1.metrics.reader import Counter, Vector
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from tests.e2e.pull_request.one_card.spec_decode.utils import MODELS, calculate_acceptance_per_pos
|
||||
|
||||
|
||||
def test_qwen3_vl_eagle(
|
||||
test_prompts: list[list[dict[str, Any]]],
|
||||
sampling_config: SamplingParams,
|
||||
vl_model_name: str,
|
||||
):
|
||||
with VllmRunner(
|
||||
vl_model_name,
|
||||
max_model_len=1024,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
) as ref_llm:
|
||||
ref_llm.model.chat(test_prompts, sampling_config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", MODELS.keys())
|
||||
@pytest.mark.parametrize("num_speculative_tokens", [3])
|
||||
@pytest.mark.parametrize("draft_tensor_parallel_size", [1])
|
||||
@pytest.mark.parametrize("disable_padded_drafter_batch", [False])
|
||||
@pytest.mark.parametrize("async_scheduling", [True])
|
||||
def test_qwen_eagle3_acceptance(
|
||||
method: str,
|
||||
num_speculative_tokens: int,
|
||||
draft_tensor_parallel_size: None | int,
|
||||
disable_padded_drafter_batch: bool,
|
||||
async_scheduling: bool,
|
||||
):
|
||||
main_model_name = MODELS[method]["main"]
|
||||
spec_model_name = MODELS[method]["spec"]
|
||||
|
||||
tokenizer_path = resolve_tokenizer_args(main_model_name)[1]
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
tokenizer_path,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0,
|
||||
ignore_eos=False,
|
||||
max_tokens=256,
|
||||
)
|
||||
|
||||
prompts = [
|
||||
{"role": "user", "content": "Hello, my name is"},
|
||||
{"role": "user", "content": "The president of the United States is"},
|
||||
{"role": "user", "content": "The capital of France is"},
|
||||
{"role": "user", "content": "The future of AI is"},
|
||||
]
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[prompt],
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
||||
speculative_config = {
|
||||
"method": method,
|
||||
"num_speculative_tokens": num_speculative_tokens,
|
||||
"draft_tensor_parallel_size": draft_tensor_parallel_size,
|
||||
"disable_padded_drafter_batch": disable_padded_drafter_batch,
|
||||
"model": spec_model_name,
|
||||
}
|
||||
|
||||
compilation_config = CompilationConfig(cudagraph_mode="FULL_DECODE_ONLY", cudagraph_capture_sizes=[12])
|
||||
|
||||
with VllmRunner(
|
||||
main_model_name,
|
||||
max_model_len=2048,
|
||||
disable_log_stats=False,
|
||||
tensor_parallel_size=1,
|
||||
max_num_seqs=256,
|
||||
distributed_executor_backend="mp",
|
||||
gpu_memory_utilization=0.7,
|
||||
speculative_config=speculative_config,
|
||||
compilation_config=compilation_config,
|
||||
async_scheduling=async_scheduling,
|
||||
) as llm:
|
||||
outputs = llm.model.generate(prompts, sampling_params)
|
||||
metrics = llm.model.get_metrics()
|
||||
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
output_tokens = output.outputs[0].token_ids
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
print(f"Output tokens: {output_tokens}")
|
||||
|
||||
acceptance_per_pos = calculate_acceptance_per_pos(metrics, num_speculative_tokens, Counter, Vector)
|
||||
golden = [0.68, 0.40, 0.18]
|
||||
|
||||
match = all(abs(a - b) < 0.08 for a, b in zip(acceptance_per_pos, golden))
|
||||
if not match:
|
||||
print(f"acceptance_per_pos: {acceptance_per_pos}")
|
||||
print(f"golden: {golden}")
|
||||
|
||||
assert match
|
||||
@@ -0,0 +1,213 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""E2E tests for the extract_hidden_states speculative decoding method.
|
||||
|
||||
Follows the pattern from vllm's test_extraction.py, validating that hidden
|
||||
states are correctly extracted and saved on the Ascend NPU. Parametrized over:
|
||||
|
||||
* a dense model (Qwen3-8B) in both eager and ACL graph modes, using real
|
||||
weights so outputs can be checked to be non-zero, and
|
||||
* a hybrid attention model (Qwen3.5-0.8B, GatedDeltaNet + full_attention)
|
||||
loaded with dummy weights as a shape/round-trip smoke test. The hybrid case
|
||||
mirrors upstream vLLM PR #39949.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
from vllm_ascend.utils import vllm_version_is
|
||||
|
||||
if vllm_version_is("0.23.0"):
|
||||
from safetensors import safe_open
|
||||
else:
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1 import example_hidden_states_connector
|
||||
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
DENSE_MODEL = "Qwen/Qwen3-8B"
|
||||
# Qwen3-8B has 36 layers; pick a spread of layer indices to extract.
|
||||
DENSE_AUX_HIDDEN_STATE_LAYER_IDS = [2, 18, 34]
|
||||
|
||||
HYBRID_MODEL = "Qwen/Qwen3.5-0.8B"
|
||||
HYBRID_AUX_HIDDEN_STATE_LAYER_IDS = [5, 11, 17]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtractHiddenStatesCase:
|
||||
model_name: str
|
||||
aux_hidden_state_layer_ids: list[int]
|
||||
prompts: list[str]
|
||||
enforce_eager: bool
|
||||
# ``None`` means "do not pass the argument", preserving each model's
|
||||
# original defaults.
|
||||
gpu_memory_utilization: float | None = None
|
||||
max_num_seqs: int | None = None
|
||||
max_model_len: int | None = None
|
||||
load_format: str | None = None
|
||||
# Dummy-weight runs can't assert non-zero outputs; real-weight runs can.
|
||||
verify_nonzero: bool = True
|
||||
# Hybrid smoke test additionally checks the token_ids round-trip.
|
||||
verify_token_ids: bool = False
|
||||
|
||||
|
||||
CASES = [
|
||||
pytest.param(
|
||||
ExtractHiddenStatesCase(
|
||||
model_name=DENSE_MODEL,
|
||||
aux_hidden_state_layer_ids=DENSE_AUX_HIDDEN_STATE_LAYER_IDS,
|
||||
prompts=[
|
||||
"Hello, how are you?",
|
||||
"What is machine learning?",
|
||||
"Explain quantum computing briefly.",
|
||||
],
|
||||
enforce_eager=True,
|
||||
gpu_memory_utilization=0.8,
|
||||
max_num_seqs=16,
|
||||
),
|
||||
id="dense_eager",
|
||||
),
|
||||
pytest.param(
|
||||
ExtractHiddenStatesCase(
|
||||
model_name=DENSE_MODEL,
|
||||
aux_hidden_state_layer_ids=DENSE_AUX_HIDDEN_STATE_LAYER_IDS,
|
||||
prompts=[
|
||||
"Hello, how are you?",
|
||||
"What is machine learning?",
|
||||
],
|
||||
enforce_eager=False,
|
||||
max_num_seqs=16,
|
||||
),
|
||||
id="dense_aclgraph",
|
||||
),
|
||||
pytest.param(
|
||||
ExtractHiddenStatesCase(
|
||||
model_name=HYBRID_MODEL,
|
||||
aux_hidden_state_layer_ids=HYBRID_AUX_HIDDEN_STATE_LAYER_IDS,
|
||||
prompts=[
|
||||
"Hello world",
|
||||
"Test prompt with several tokens",
|
||||
],
|
||||
enforce_eager=True,
|
||||
gpu_memory_utilization=0.4,
|
||||
max_model_len=256,
|
||||
load_format="dummy",
|
||||
verify_nonzero=False,
|
||||
verify_token_ids=True,
|
||||
),
|
||||
id="hybrid_dummy_eager",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sampling_config():
|
||||
return SamplingParams(temperature=0, max_tokens=1)
|
||||
|
||||
|
||||
def _verify_output(output, expected_shape, *, verify_nonzero, verify_token_ids):
|
||||
"""Verify a single hidden-states dump (matches vllm's check pattern)."""
|
||||
assert output.kv_transfer_params is not None
|
||||
hidden_states_path = output.kv_transfer_params.get("hidden_states_path")
|
||||
assert hidden_states_path is not None
|
||||
|
||||
if vllm_version_is("0.23.0"):
|
||||
assert os.path.exists(hidden_states_path)
|
||||
with safe_open(hidden_states_path, "pt") as f:
|
||||
tensor_names = f.keys()
|
||||
assert "hidden_states" in tensor_names
|
||||
hidden_states = f.get_tensor("hidden_states")
|
||||
assert hidden_states.shape == expected_shape
|
||||
|
||||
if verify_token_ids:
|
||||
token_ids = f.get_tensor("token_ids")
|
||||
assert torch.equal(token_ids, torch.tensor(output.prompt_token_ids))
|
||||
|
||||
if verify_nonzero:
|
||||
assert not torch.allclose(hidden_states, torch.zeros_like(hidden_states))
|
||||
else:
|
||||
obj = example_hidden_states_connector.load_hidden_states(hidden_states_path)
|
||||
example_hidden_states_connector.cleanup_hidden_states(hidden_states_path)
|
||||
hidden_states = obj["hidden_states"]
|
||||
assert hidden_states.shape == expected_shape
|
||||
|
||||
if verify_token_ids:
|
||||
token_ids = obj["token_ids"]
|
||||
assert torch.equal(token_ids, torch.tensor(output.prompt_token_ids))
|
||||
|
||||
if verify_nonzero:
|
||||
assert not torch.allclose(hidden_states, torch.zeros_like(hidden_states))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", CASES)
|
||||
def test_extract_hidden_states(case: ExtractHiddenStatesCase, sampling_config):
|
||||
"""Extract hidden states from the target model and validate the dump."""
|
||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||
llm_kwargs = dict(
|
||||
model=case.model_name,
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=case.enforce_eager,
|
||||
enable_chunked_prefill=False,
|
||||
speculative_config={
|
||||
"method": "extract_hidden_states",
|
||||
"num_speculative_tokens": 1,
|
||||
"draft_model_config": {
|
||||
"hf_config": {
|
||||
"eagle_aux_hidden_state_layer_ids": case.aux_hidden_state_layer_ids,
|
||||
}
|
||||
},
|
||||
},
|
||||
kv_transfer_config={
|
||||
"kv_connector": "ExampleHiddenStatesConnector",
|
||||
"kv_role": "kv_producer",
|
||||
"kv_connector_extra_config": {
|
||||
"shared_storage_path": tmpdirname,
|
||||
},
|
||||
},
|
||||
)
|
||||
if case.gpu_memory_utilization is not None:
|
||||
llm_kwargs["gpu_memory_utilization"] = case.gpu_memory_utilization
|
||||
if case.max_num_seqs is not None:
|
||||
llm_kwargs["max_num_seqs"] = case.max_num_seqs
|
||||
if case.max_model_len is not None:
|
||||
llm_kwargs["max_model_len"] = case.max_model_len
|
||||
if case.load_format is not None:
|
||||
llm_kwargs["load_format"] = case.load_format
|
||||
|
||||
llm = LLM(**llm_kwargs)
|
||||
|
||||
outputs = llm.generate(case.prompts, sampling_config)
|
||||
hidden_size = llm.llm_engine.model_config.get_hidden_size()
|
||||
num_layers = len(case.aux_hidden_state_layer_ids)
|
||||
|
||||
assert len(outputs) == len(case.prompts)
|
||||
|
||||
for output in outputs:
|
||||
num_tokens = len(output.prompt_token_ids)
|
||||
expected_shape = (num_tokens, num_layers, hidden_size)
|
||||
_verify_output(
|
||||
output,
|
||||
expected_shape,
|
||||
verify_nonzero=case.verify_nonzero,
|
||||
verify_token_ids=case.verify_token_ids,
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2025 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
"""Compare the short outputs of HF and vLLM when using greedy sampling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams
|
||||
from vllm.config import CompilationConfig
|
||||
|
||||
from tests.e2e.conftest import VllmRunner, cleanup_dist_env_and_memory
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
MODELS = ["wemaster/deepseek_mtp_main_random_bf16"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", MODELS)
|
||||
@pytest.mark.parametrize("num_speculative_tokens", [3])
|
||||
@pytest.mark.parametrize("cudagraph_mode", ["FULL_DECODE_ONLY"])
|
||||
@pytest.mark.parametrize("disable_padded_drafter_batch", [False])
|
||||
def test_deepseek_mtp(
|
||||
model_name: str, num_speculative_tokens: int, cudagraph_mode: str, disable_padded_drafter_batch: bool
|
||||
):
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
"""
|
||||
Compare the outputs of a original LLM and a speculative LLM
|
||||
should be the same when using mtp speculative decoding.
|
||||
"""
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
tensor_parallel_size=1,
|
||||
max_num_seqs=256,
|
||||
gpu_memory_utilization=0.7,
|
||||
distributed_executor_backend="mp",
|
||||
enable_expert_parallel=True,
|
||||
speculative_config={
|
||||
"method": "mtp",
|
||||
"num_speculative_tokens": num_speculative_tokens,
|
||||
"disable_padded_drafter_batch": disable_padded_drafter_batch,
|
||||
},
|
||||
max_model_len=2000,
|
||||
compilation_config=CompilationConfig(
|
||||
cudagraph_mode=cudagraph_mode,
|
||||
cudagraph_capture_sizes=[20],
|
||||
),
|
||||
) as spec_llm:
|
||||
sampling_config = SamplingParams(temperature=0, max_tokens=256, ignore_eos=False)
|
||||
spec_llm.generate(example_prompts, sampling_config)
|
||||
|
||||
cleanup_dist_env_and_memory()
|
||||
del spec_llm
|
||||
26
tests/e2e/pull_request/one_card/spec_decode/test_ngram.py
Normal file
26
tests/e2e/pull_request/one_card/spec_decode/test_ngram.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from vllm import SamplingParams
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
|
||||
def test_ngram(
|
||||
test_prompts: list[list[dict[str, Any]]],
|
||||
sampling_config: SamplingParams,
|
||||
model_name: str,
|
||||
):
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
speculative_config={
|
||||
"method": "ngram",
|
||||
"prompt_lookup_max": 5,
|
||||
"prompt_lookup_min": 3,
|
||||
"num_speculative_tokens": 3,
|
||||
},
|
||||
max_model_len=1024,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
) as runner:
|
||||
runner.model.chat(test_prompts, sampling_config)
|
||||
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams
|
||||
from vllm.config import CompilationConfig
|
||||
from vllm.v1.metrics.reader import Counter, Vector
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from tests.e2e.pull_request.one_card.spec_decode.utils import calculate_acceptance_per_pos
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_speculative_tokens", [3])
|
||||
def test_ngram_npu_async_acceptance(
|
||||
test_prompts: list[list[dict[str, Any]]],
|
||||
num_speculative_tokens: int,
|
||||
model_name: str,
|
||||
):
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0,
|
||||
ignore_eos=False,
|
||||
max_tokens=256,
|
||||
)
|
||||
|
||||
speculative_config = {
|
||||
"method": "ngram_gpu",
|
||||
"prompt_lookup_max": 2,
|
||||
"prompt_lookup_min": 2,
|
||||
"num_speculative_tokens": num_speculative_tokens,
|
||||
}
|
||||
|
||||
compilation_config = CompilationConfig(
|
||||
cudagraph_mode="PIECEWISE",
|
||||
cudagraph_capture_sizes=[12],
|
||||
)
|
||||
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
max_model_len=2048,
|
||||
disable_log_stats=False,
|
||||
tensor_parallel_size=1,
|
||||
max_num_seqs=256,
|
||||
distributed_executor_backend="mp",
|
||||
gpu_memory_utilization=0.7,
|
||||
speculative_config=speculative_config,
|
||||
compilation_config=compilation_config,
|
||||
async_scheduling=True,
|
||||
) as llm:
|
||||
outputs = llm.model.chat(test_prompts, sampling_params)
|
||||
metrics = llm.model.get_metrics()
|
||||
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
output_tokens = output.outputs[0].token_ids
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
print(f"Output tokens: {output_tokens}")
|
||||
|
||||
acceptance_per_pos = calculate_acceptance_per_pos(metrics, num_speculative_tokens, Counter, Vector)
|
||||
golden = [0.50, 0.30, 0.20]
|
||||
|
||||
match = all(abs(a - b) < 1.0 for a, b in zip(acceptance_per_pos, golden))
|
||||
if not match:
|
||||
print(f"acceptance_per_pos: {acceptance_per_pos}")
|
||||
print(f"golden: {golden}")
|
||||
|
||||
assert match
|
||||
49
tests/e2e/pull_request/one_card/spec_decode/test_suffix.py
Normal file
49
tests/e2e/pull_request/one_card/spec_decode/test_suffix.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from vllm import SamplingParams
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
|
||||
def test_suffix_acceptance(
|
||||
test_prompts: list[list[dict[str, Any]]],
|
||||
sampling_config: SamplingParams,
|
||||
model_name: str,
|
||||
):
|
||||
num_draft = []
|
||||
num_accept = []
|
||||
with VllmRunner(
|
||||
model_name,
|
||||
speculative_config={
|
||||
"method": "suffix",
|
||||
"suffix_decoding_max_spec_factor": 2.0,
|
||||
"suffix_decoding_max_cached_requests": 1000,
|
||||
"num_speculative_tokens": 10,
|
||||
},
|
||||
max_model_len=1024,
|
||||
max_cudagraph_capture_size=16,
|
||||
disable_log_stats=False,
|
||||
) as runner:
|
||||
for i in range(10):
|
||||
runner.model.chat(test_prompts[i], sampling_config)
|
||||
metrics = runner.model.get_metrics()
|
||||
for metric in metrics:
|
||||
print(metric)
|
||||
if metric.name == "vllm:spec_decode_num_draft_tokens":
|
||||
num_draft.append(metric.value)
|
||||
if metric.name == "vllm:spec_decode_num_accepted_tokens":
|
||||
num_accept.append(metric.value)
|
||||
|
||||
first_accept_tokens = num_accept[0]
|
||||
first_draft_tokens = num_draft[0]
|
||||
first_accept_rate = first_accept_tokens / first_draft_tokens
|
||||
|
||||
last_accept_tokens = num_accept[-1] - num_accept[-2]
|
||||
last_draft_tokens = num_draft[-1] - num_draft[-2]
|
||||
last_accept_rate = last_accept_tokens / last_draft_tokens
|
||||
|
||||
assert first_accept_tokens < last_accept_tokens
|
||||
assert first_accept_rate < last_accept_rate
|
||||
assert last_accept_rate > 0.60
|
||||
61
tests/e2e/pull_request/one_card/spec_decode/utils.py
Normal file
61
tests/e2e/pull_request/one_card/spec_decode/utils.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
MODELS = {
|
||||
"eagle3": {
|
||||
"main": "Qwen/Qwen3-8B",
|
||||
"spec": "RedHatAI/Qwen3-8B-speculator.eagle3",
|
||||
},
|
||||
}
|
||||
|
||||
DRAFT_PARALLEL_MODELS = {
|
||||
"draft_parallel": {
|
||||
"main": "LLM-Research/Meta-Llama-3.1-8B-Instruct",
|
||||
"spec": "amd/PARD-Llama-3.2-1B",
|
||||
},
|
||||
}
|
||||
|
||||
DFLASH = {
|
||||
"dflash": {
|
||||
"main": "Qwen/Qwen3-8B",
|
||||
"spec": "z-lab/Qwen3-8B-DFlash-b16",
|
||||
}
|
||||
}
|
||||
|
||||
BASELINES = {
|
||||
"eagle": [0.74, 0.44, 0.29],
|
||||
"eagle3": [0.68, 0.40, 0.18],
|
||||
"draft_parallel": [0.83, 0.50, 0.33, 0.17, 0.17, 0.17, 0.17, 0.00],
|
||||
"dflash": [0.60, 0.50, 0.30, 0.20, 0.20, 0.10, 0.00, 0.00],
|
||||
}
|
||||
|
||||
|
||||
def eagle_model_name():
|
||||
return "vllm-ascend/EAGLE-LLaMA3.1-Instruct-8B"
|
||||
|
||||
|
||||
def eagle3_model_name():
|
||||
return "vllm-ascend/EAGLE3-LLaMA3.1-Instruct-8B"
|
||||
|
||||
|
||||
def vl_eagle3_model_name():
|
||||
return "MNN/Qwen3-VL-8B-Instruct-Eagle3"
|
||||
|
||||
|
||||
def calculate_acceptance_per_pos(metrics: list[Any], num_speculative_tokens: int, counter_type: Any, vector_type: Any):
|
||||
num_drafts = 0
|
||||
num_accepted_tokens_per_pos = [0] * num_speculative_tokens
|
||||
for metric in metrics:
|
||||
if metric.name == "vllm:spec_decode_num_drafts":
|
||||
assert isinstance(metric, counter_type)
|
||||
num_drafts += metric.value
|
||||
elif metric.name == "vllm:spec_decode_num_accepted_tokens_per_pos":
|
||||
assert isinstance(metric, vector_type)
|
||||
for pos in range(len(metric.values)):
|
||||
num_accepted_tokens_per_pos[pos] += metric.values[pos]
|
||||
|
||||
return [num_accepted_tokens / num_drafts for num_accepted_tokens in num_accepted_tokens_per_pos]
|
||||
153
tests/e2e/pull_request/one_card/test_attention_fa3.py
Normal file
153
tests/e2e/pull_request/one_card/test_attention_fa3.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
End-to-end test for Flash Attention 3 (FA3) on Ascend.
|
||||
|
||||
This test verifies that FA3 produces correct results by comparing its output
|
||||
with the default Fused Infer Attention (FIA) backend. Both backends are run
|
||||
with the same model and prompts in eager mode, and the generated token ids
|
||||
are compared for consistency.
|
||||
"""
|
||||
|
||||
import os
|
||||
from importlib import import_module, util
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_BATCH_INVARIANT"] = "1"
|
||||
|
||||
MODEL_NAME = "Qwen/Qwen3-0.6B"
|
||||
MAX_MODEL_LEN = 512
|
||||
MAX_TOKENS = 32
|
||||
|
||||
SHORT_PROMPTS = [
|
||||
"The capital of France is",
|
||||
"In a hole in the ground there lived a hobbit.",
|
||||
"The meaning of life is",
|
||||
"To be or not to be, that is the",
|
||||
]
|
||||
|
||||
LONG_PROMPT = "The quick brown fox jumps over the lazy dog. " * 50
|
||||
|
||||
|
||||
def _fa3_available() -> bool:
|
||||
try:
|
||||
if util.find_spec("flash_attn_npu_v3") is None:
|
||||
return False
|
||||
mod = import_module("flash_attn_npu_v3")
|
||||
return hasattr(mod, "flash_attn_with_kvcache")
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def _generate_with_backend(prompts, max_tokens=MAX_TOKENS, **runner_kwargs):
|
||||
with VllmRunner(
|
||||
MODEL_NAME,
|
||||
max_model_len=MAX_MODEL_LEN,
|
||||
enforce_eager=True,
|
||||
gpu_memory_utilization=0.7,
|
||||
**runner_kwargs,
|
||||
) as runner:
|
||||
return runner.generate_greedy(prompts, max_tokens)
|
||||
|
||||
|
||||
def _generate_logprobs_with_backend(prompts, max_tokens=5, num_logprobs=5, **runner_kwargs):
|
||||
with VllmRunner(
|
||||
MODEL_NAME,
|
||||
max_model_len=MAX_MODEL_LEN,
|
||||
enforce_eager=True,
|
||||
gpu_memory_utilization=0.7,
|
||||
**runner_kwargs,
|
||||
) as runner:
|
||||
return runner.generate_greedy_logprobs(prompts, max_tokens=max_tokens, num_logprobs=num_logprobs)
|
||||
|
||||
|
||||
def _assert_outputs_match(fia_outputs, fa3_outputs, label=""):
|
||||
for i, (fia_out, fa3_out) in enumerate(zip(fia_outputs, fa3_outputs)):
|
||||
fia_ids, fia_text = fia_out
|
||||
fa3_ids, fa3_text = fa3_out
|
||||
assert fia_ids == fa3_ids, (
|
||||
f"{label}Prompt {i}: FA3 and FIA token ids differ.\n"
|
||||
f" FIA ids: {fia_ids}\n"
|
||||
f" FA3 ids: {fa3_ids}\n"
|
||||
f" FIA text: {fia_text}\n"
|
||||
f" FA3 text: {fa3_text}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _fa3_available(), reason="flash_attn_npu_v3 is not installed")
|
||||
def test_fa3_vs_fia_single_prompt():
|
||||
"""Compare FA3 and FIA with a single prompt (minimal batch size).
|
||||
|
||||
This is an edge case that verifies FA3 works correctly when there
|
||||
is only one sequence in the batch, which may exercise different
|
||||
code paths in the attention kernel.
|
||||
"""
|
||||
single_prompt = ["Explain quantum computing in simple terms."]
|
||||
fia_outputs = _generate_with_backend(single_prompt)
|
||||
fa3_outputs = _generate_with_backend(single_prompt, attention_backend="FLASH_ATTN")
|
||||
_assert_outputs_match(fia_outputs, fa3_outputs, label="[SinglePrompt] ")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _fa3_available(), reason="flash_attn_npu_v3 is not installed")
|
||||
def test_fa3_vs_fia_mixed_lengths():
|
||||
"""Compare FA3 and FIA with mixed prompt lengths in the same batch.
|
||||
|
||||
This exercises both prefill and decode paths within a single batch,
|
||||
verifying that FA3 handles variable-length sequences correctly.
|
||||
"""
|
||||
mixed_prompts = [
|
||||
"Hi",
|
||||
"The capital of France is",
|
||||
LONG_PROMPT[:256],
|
||||
"What is 2+2?",
|
||||
LONG_PROMPT[:MAX_MODEL_LEN],
|
||||
]
|
||||
fia_outputs = _generate_with_backend(mixed_prompts)
|
||||
fa3_outputs = _generate_with_backend(mixed_prompts, attention_backend="FLASH_ATTN")
|
||||
_assert_outputs_match(fia_outputs, fa3_outputs, label="[MixedLen] ")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _fa3_available(), reason="flash_attn_npu_v3 is not installed")
|
||||
def test_fa3_vs_fia_with_chunkprefill():
|
||||
"""Compare FA3 and FIA with single token generation where chunkprefill is used."""
|
||||
fia_outputs = _generate_with_backend(SHORT_PROMPTS, max_tokens=2, max_num_seqs=2, max_num_batched_tokens=5)
|
||||
fa3_outputs = _generate_with_backend(
|
||||
SHORT_PROMPTS, attention_backend="FLASH_ATTN", max_tokens=2, max_num_seqs=2, max_num_batched_tokens=5
|
||||
)
|
||||
_assert_outputs_match(fia_outputs, fa3_outputs, label="[Chunkprefill] ")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _fa3_available(), reason="flash_attn_npu_v3 is not installed")
|
||||
def test_fa3_vs_fia_logprobs():
|
||||
"""Compare FA3 and FIA logprobs for fine-grained numerical verification."""
|
||||
fia_logprobs = _generate_logprobs_with_backend(SHORT_PROMPTS[:1])
|
||||
fa3_logprobs = _generate_logprobs_with_backend(SHORT_PROMPTS[:1], attention_backend="FLASH_ATTN")
|
||||
|
||||
for i, (fia_out, fa3_out) in enumerate(zip(fia_logprobs, fa3_logprobs)):
|
||||
fia_ids, _, fia_lp = fia_out
|
||||
fa3_ids, _, fa3_lp = fa3_out
|
||||
|
||||
assert fia_ids == fa3_ids, (
|
||||
f"Prompt {i}: FA3 and FIA token ids differ.\n FIA ids: {fia_ids}\n FA3 ids: {fa3_ids}"
|
||||
)
|
||||
|
||||
assert len(fia_lp) == len(fa3_lp), (
|
||||
f"Prompt {i}: Different number of logprob steps: FIA {len(fia_lp)} vs FA3 {len(fa3_lp)}"
|
||||
)
|
||||
|
||||
for t, (fia_token_lp, fa3_token_lp) in enumerate(zip(fia_lp, fa3_lp)):
|
||||
assert set(fia_token_lp.keys()) == set(fa3_token_lp.keys()), (
|
||||
f"Prompt {i}, token {t}: Logprob token sets differ.\n"
|
||||
f" FIA keys: {sorted(fia_token_lp.keys())}\n"
|
||||
f" FA3 keys: {sorted(fa3_token_lp.keys())}"
|
||||
)
|
||||
for token_id in fia_token_lp:
|
||||
fia_logprob = fia_token_lp[token_id].logprob
|
||||
fa3_logprob = fa3_token_lp[token_id].logprob
|
||||
assert abs(fia_logprob - fa3_logprob) < 1e-3, (
|
||||
f"Prompt {i}, token {t}, token_id {token_id} ("
|
||||
f"'{fia_token_lp[token_id].decoded_token}'): "
|
||||
f"logprobs differ: FIA {fia_logprob:.6f} vs FA3 {fa3_logprob:.6f}"
|
||||
)
|
||||
641
tests/e2e/pull_request/one_card/test_batch_invariant.py
Normal file
641
tests/e2e/pull_request/one_card/test_batch_invariant.py
Normal file
@@ -0,0 +1,641 @@
|
||||
# Adapt from https://github.com/vllm-project/vllm/blob/main/tests/v1/determinism/test_batch_invariant.py
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
import gc
|
||||
import os
|
||||
import random
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
from tests.e2e.conftest import ModelName, cleanup_dist_env_and_memory, model_cache
|
||||
|
||||
os.environ["VLLM_BATCH_INVARIANT"] = "1"
|
||||
|
||||
DEFAULT_MODEL = ModelName.QWEN3_06B
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def enable_batch_invariant_mode(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Automatically enable batch invariant kernel overrides for all tests."""
|
||||
monkeypatch.setenv("VLLM_BATCH_INVARIANT", "1")
|
||||
|
||||
|
||||
def _random_prompt(min_words: int = 1024, max_words: int = 1024 * 2) -> str:
|
||||
# Generate more realistic prompts that will actually produce varied tokens
|
||||
# Use a mix of common English text patterns
|
||||
|
||||
prompt_templates = [
|
||||
# Question-answer style
|
||||
"Question: What is the capital of France?\nAnswer: The capital of France is",
|
||||
"Q: How does photosynthesis work?\nA: Photosynthesis is the process by which",
|
||||
"User: Can you explain quantum mechanics?\nAssistant: Quantum mechanics is",
|
||||
# Story/narrative style
|
||||
"Once upon a time in a distant galaxy, there lived",
|
||||
"The old man walked slowly down the street, remembering",
|
||||
"In the year 2157, humanity finally discovered",
|
||||
# Technical/code style
|
||||
"To implement a binary search tree in Python, first we need to",
|
||||
"The algorithm works by iterating through the array and",
|
||||
"Here's how to optimize database queries using indexing:",
|
||||
# Factual/informative style
|
||||
"The Renaissance was a period in European history that",
|
||||
"Climate change is caused by several factors including",
|
||||
"The human brain contains approximately 86 billion neurons which",
|
||||
# Conversational style
|
||||
"I've been thinking about getting a new laptop because",
|
||||
"Yesterday I went to the store and bought",
|
||||
"My favorite thing about summer is definitely",
|
||||
]
|
||||
|
||||
# Pick a random template
|
||||
base_prompt = random.choice(prompt_templates)
|
||||
|
||||
if max_words < min_words:
|
||||
max_words = min_words
|
||||
target_words = random.randint(min_words, max_words)
|
||||
|
||||
if target_words > 50:
|
||||
# For longer prompts, repeat context
|
||||
padding_text = " This is an interesting topic that deserves more explanation. " * (target_words // 50)
|
||||
base_prompt = base_prompt + padding_text
|
||||
|
||||
return base_prompt
|
||||
|
||||
|
||||
def _extract_step_logprobs(request_output):
|
||||
if getattr(request_output, "outputs", None):
|
||||
inner = request_output.outputs[0]
|
||||
if hasattr(inner, "logprobs") and inner.logprobs is not None:
|
||||
t = torch.tensor(
|
||||
[inner.logprobs[i][tid].logprob for i, tid in enumerate(inner.token_ids)],
|
||||
dtype=torch.float32,
|
||||
)
|
||||
return t, inner.token_ids
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=DEFAULT_MODEL,
|
||||
max_num_seqs=int(os.getenv("VLLM_NEEDLE_BATCH_SIZE", "64")),
|
||||
gpu_memory_utilization=float(os.getenv("VLLM_GPU_MEMORY_UTILIZATION", "0.95")),
|
||||
max_model_len=int(os.getenv("VLLM_MAX_MODEL_LEN", "8192")),
|
||||
dtype="bfloat16",
|
||||
tensor_parallel_size=int(os.getenv("VLLM_TP_SIZE", "1")),
|
||||
enable_prefix_caching=False,
|
||||
distributed_executor_backend="mp",
|
||||
compilation_config={
|
||||
"cudagraph_mode": "PIECEWISE",
|
||||
"cudagraph_capture_sizes": [1, 32, 64],
|
||||
},
|
||||
)
|
||||
def test_v1_generation_is_deterministic_across_batch_sizes_with_needle(
|
||||
vllm_runner,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
"""
|
||||
Ensures that the same request (the 'needle' prompt) yields identical output
|
||||
whether run alone (bs=1) or mixed into a larger batch (e.g., bs=64),
|
||||
using the high-level v1 LLM() API only (no manual batching).
|
||||
|
||||
Strategy:
|
||||
- Create two LLM engines with identical config except max_num_seqs: 1 vs N.
|
||||
- Compute a baseline output for the needle prompt with the bs=1 engine.
|
||||
- For many trials, generate a batch (size N) where the needle appears at a
|
||||
random position among random filler prompts using the bs=N engine.
|
||||
- Track how many trials match vs mismatch, and report totals at the end.
|
||||
The test fails if any mismatches occur, but we still dump pass/fail
|
||||
counts.
|
||||
|
||||
Notes:
|
||||
- Use seeded stochastic sampling with a fixed seed to test determinism.
|
||||
- Outputs are intentionally longer and sampled at higher temperature/top_p
|
||||
to produce a more random-sounding phrase, yet remain deterministic by
|
||||
seed.
|
||||
- Keep max_tokens and max_model_len bounded for speed and memory use.
|
||||
"""
|
||||
seed = int(os.getenv("VLLM_TEST_SEED", "12345"))
|
||||
random.seed(seed)
|
||||
|
||||
# Allow overrides from environment (useful for CI tuning)
|
||||
num_trials = int(os.getenv("VLLM_NEEDLE_TRIALS", "5"))
|
||||
max_batch_size = int(os.getenv("VLLM_NEEDLE_BATCH_SIZE", "144"))
|
||||
min_random_prompt = int(os.getenv("VLLM_MIN_PROMPT", "1024"))
|
||||
max_random_prompt = int(os.getenv("VLLM_MAX_PROMPT", "2048"))
|
||||
assert max_batch_size >= 2, "Batch size should be >= 2 to mix needle."
|
||||
|
||||
# Sampling parameters: longer outputs with a more random-sounding
|
||||
# continuation,but still deterministic due to fixed seed.
|
||||
temperature = float(os.getenv("VLLM_NEEDLE_TEMPERATURE", "0.0"))
|
||||
top_p = float(os.getenv("VLLM_NEEDLE_TOP_P", "0.95"))
|
||||
max_tokens = int(os.getenv("VLLM_NEEDLE_MAX_TOKENS", "35"))
|
||||
|
||||
sampling = SamplingParams(
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
max_tokens=max_tokens,
|
||||
seed=20240919,
|
||||
)
|
||||
|
||||
needle_prompt = "There once was a "
|
||||
|
||||
# Baseline generation for the needle prompt alone.
|
||||
baseline_out = vllm_runner.model.generate([needle_prompt], sampling)
|
||||
assert len(baseline_out) == 1
|
||||
assert len(baseline_out[0].outputs) >= 1
|
||||
baseline_text = baseline_out[0].outputs[0].text
|
||||
|
||||
mismatches = 0
|
||||
|
||||
for trial in range(num_trials):
|
||||
# Create a batch of size `max_batch_size` and insert the needle at
|
||||
# a random index
|
||||
prompts: list[str] = []
|
||||
batch_size = random.randint(max_batch_size // 2, max_batch_size)
|
||||
needle_pos = random.randint(0, batch_size - 1)
|
||||
for i in range(batch_size):
|
||||
if i == needle_pos:
|
||||
prompts.append(needle_prompt)
|
||||
else:
|
||||
prompts.append(_random_prompt(min_random_prompt, max_random_prompt))
|
||||
|
||||
# Generate with the larger-batch engine
|
||||
outputs = vllm_runner.model.generate(prompts, sampling)
|
||||
# Find the needle output by position
|
||||
needle_output = outputs[needle_pos]
|
||||
assert needle_output.prompt == needle_prompt
|
||||
assert len(needle_output.outputs) >= 1
|
||||
text = needle_output.outputs[0].text
|
||||
|
||||
if text != baseline_text:
|
||||
print(f"{text}\n\n== Not the same as ==\n\n{baseline_text}\n\n")
|
||||
mismatches += 1
|
||||
|
||||
passes = num_trials - mismatches
|
||||
# Dump how many passed vs failed
|
||||
print(f"[determinism] total={num_trials}, passed={passes}, failed={mismatches}, max_batch_size={max_batch_size}")
|
||||
|
||||
if mismatches > 0:
|
||||
pytest.fail(
|
||||
f"Nondeterministic outputs detected: {mismatches} failed out "
|
||||
f"of {num_trials} trials (max_batch_size={max_batch_size})."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.model(
|
||||
model_name=DEFAULT_MODEL,
|
||||
max_num_seqs=144,
|
||||
gpu_memory_utilization=float(os.getenv("VLLM_GPU_MEMORY_UTILIZATION", "0.95")),
|
||||
max_model_len=8192,
|
||||
dtype="bfloat16",
|
||||
tensor_parallel_size=int(os.getenv("VLLM_TEST_TP_SIZE", "1")),
|
||||
enable_prefix_caching=False,
|
||||
distributed_executor_backend="mp",
|
||||
compilation_config={
|
||||
"cudagraph_mode": "PIECEWISE",
|
||||
"cudagraph_capture_sizes": [1, 32, 64],
|
||||
},
|
||||
)
|
||||
def test_logprobs_bitwise_batch_invariance_bs1_vs_bsN(vllm_runner, monkeypatch: pytest.MonkeyPatch):
|
||||
seed = int(os.getenv("VLLM_TEST_SEED", "12345"))
|
||||
random.seed(seed)
|
||||
tp_size = int(os.getenv("VLLM_TEST_TP_SIZE", "1"))
|
||||
|
||||
# For batch invariance, disable custom all-reduce to ensure deterministic
|
||||
# all-reduce operations (custom all-reduce may not be deterministic)
|
||||
import vllm.envs as envs
|
||||
|
||||
disable_custom_ar = envs.VLLM_BATCH_INVARIANT
|
||||
|
||||
if disable_custom_ar:
|
||||
print(f"\n{'=' * 80}")
|
||||
print(f"BATCH INVARIANCE MODE: Disabling custom all-reduce (TP={tp_size})")
|
||||
print(f"{'=' * 80}\n")
|
||||
|
||||
# Use more realistic prompts for better token generation
|
||||
prompts = [_random_prompt(10, 50) for i in range(32)]
|
||||
|
||||
sp = SamplingParams(
|
||||
temperature=0.6,
|
||||
top_p=1.0,
|
||||
max_tokens=8,
|
||||
seed=1234,
|
||||
logprobs=5,
|
||||
)
|
||||
|
||||
# BS=1: run prompts individually and collect logprobs per step.
|
||||
print("\n" + "=" * 80)
|
||||
print("STARTING BS=1 RUNS (each prompt individually)")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
bs1_logprobs_per_prompt = []
|
||||
bs1_tokens_per_prompt = []
|
||||
for idx, p in enumerate(prompts):
|
||||
print(f"\n[BS=1] Running prompt {idx}/{len(prompts)} - Preview: {p[:80]}...")
|
||||
outs = vllm_runner.model.generate([p], sp, use_tqdm=False)
|
||||
assert len(outs) == 1
|
||||
step_logprobs, token_ids = _extract_step_logprobs(outs[0])
|
||||
if step_logprobs is None:
|
||||
pytest.skip("Logits are not available on RequestOutput; enable logprobs return to run this test.")
|
||||
bs1_logprobs_per_prompt.append(step_logprobs)
|
||||
bs1_tokens_per_prompt.append(token_ids)
|
||||
print(f"[BS=1] Prompt {idx} generated tokens: {token_ids}")
|
||||
|
||||
# BS=N: run prompts in a batch and collect logprobs per step for each
|
||||
# prompt.
|
||||
print("\n" + "=" * 80)
|
||||
print(f"STARTING BS={len(prompts)} RUN (all prompts batched)")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
outs_batched = vllm_runner.model.generate(prompts, sp, use_tqdm=False)
|
||||
assert len(outs_batched) == len(prompts)
|
||||
bsN_logprobs_per_prompt = []
|
||||
bsN_tokens_per_prompt = []
|
||||
|
||||
print(f"\n[BS={len(prompts)}] Processing batched outputs...")
|
||||
for idx, o in enumerate(outs_batched):
|
||||
tokens = o.outputs[0].token_ids if o.outputs else "N/A"
|
||||
print(f"[BS={len(prompts)}] Prompt {idx} generated tokens: {tokens}")
|
||||
step_logprobs, token_ids = _extract_step_logprobs(o)
|
||||
if step_logprobs is None:
|
||||
pytest.skip("Logits are not available on RequestOutput; enable logprobs return to run this test.")
|
||||
bsN_logprobs_per_prompt.append(step_logprobs)
|
||||
bsN_tokens_per_prompt.append(token_ids)
|
||||
|
||||
# Compare step-by-step logprobs for each prompt between BS=1 and BS=N runs.
|
||||
failed_prompts = []
|
||||
for i, (logprobs_bs1, logprobs_bsN, tokens_bs1, tokens_bsN) in enumerate(
|
||||
zip(
|
||||
bs1_logprobs_per_prompt,
|
||||
bsN_logprobs_per_prompt,
|
||||
bs1_tokens_per_prompt,
|
||||
bsN_tokens_per_prompt,
|
||||
)
|
||||
):
|
||||
if len(logprobs_bs1) != len(logprobs_bsN):
|
||||
reason = f"Different number of steps: {len(logprobs_bs1)} (BS=1) vs {len(logprobs_bsN)} (BS=N)"
|
||||
failed_prompts.append(
|
||||
{
|
||||
"prompt_idx": i,
|
||||
"step": "all",
|
||||
"reason": reason,
|
||||
"prompt_preview": prompts[i][:100],
|
||||
"bs1_tokens": tokens_bs1,
|
||||
"bsN_tokens": tokens_bsN,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Check if tokens match first
|
||||
if tokens_bs1 != tokens_bsN:
|
||||
failed_prompts.append(
|
||||
{
|
||||
"prompt_idx": i,
|
||||
"step": "sampling",
|
||||
"reason": "Different tokens sampled",
|
||||
"prompt_preview": prompts[i][:100],
|
||||
"bs1_tokens": tokens_bs1,
|
||||
"bsN_tokens": tokens_bsN,
|
||||
"bs1_all_logprobs": [logprobs_bs1[s].tolist() for s in range(len(logprobs_bs1))],
|
||||
"bsN_all_logprobs": [logprobs_bsN[s].tolist() for s in range(len(logprobs_bsN))],
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
for t, (a, b) in enumerate(zip(logprobs_bs1, logprobs_bsN)):
|
||||
if a.shape != b.shape:
|
||||
failed_prompts.append(
|
||||
{
|
||||
"prompt_idx": i,
|
||||
"step": t,
|
||||
"reason": f"Shape mismatch: {a.shape} vs {b.shape}",
|
||||
"prompt_preview": prompts[i][:100],
|
||||
"bs1_tokens": tokens_bs1,
|
||||
"bsN_tokens": tokens_bsN,
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
if not torch.equal(a, b):
|
||||
max_diff = torch.abs(a - b).max().item()
|
||||
# Print which token failed
|
||||
print(f"\n[DIVERGENCE] Prompt {i}, Token {t}: max_diff={max_diff:.6e}")
|
||||
bs1_tok = tokens_bs1[t] if t < len(tokens_bs1) else "N/A"
|
||||
bsN_tok = tokens_bsN[t] if t < len(tokens_bsN) else "N/A"
|
||||
print(f" Token IDs: bs1={bs1_tok}, bsN={bsN_tok}")
|
||||
print(f" BS=1 logprob: {a.tolist()}")
|
||||
print(f" BS=N logprob: {b.tolist()}")
|
||||
failed_prompts.append(
|
||||
{
|
||||
"prompt_idx": i,
|
||||
"step": t,
|
||||
"reason": f"Bitwise mismatch (max_diff={max_diff:.6e})",
|
||||
"prompt_preview": prompts[i][:100],
|
||||
"bs1_tokens": tokens_bs1,
|
||||
"bsN_tokens": tokens_bsN,
|
||||
"bs1_all_logprobs": [logprobs_bs1[s].tolist() for s in range(len(logprobs_bs1))],
|
||||
"bsN_all_logprobs": [logprobs_bsN[s].tolist() for s in range(len(logprobs_bsN))],
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
# Print summary of all failures
|
||||
if failed_prompts:
|
||||
print(f"\n{'=' * 80}")
|
||||
fail_msg = f"BATCH INVARIANCE FAILURES: {len(failed_prompts)}/{len(prompts)} prompts failed"
|
||||
print(fail_msg)
|
||||
print(f"{'=' * 80}")
|
||||
for fail in failed_prompts:
|
||||
print(f"\nPrompt {fail['prompt_idx']} (step {fail['step']}):")
|
||||
print(f" Reason: {fail['reason']}")
|
||||
print(f" Preview: {fail['prompt_preview']}...")
|
||||
|
||||
# Always show the tokens
|
||||
if "bs1_tokens" in fail:
|
||||
print(f" BS=1 tokens: {fail['bs1_tokens']}")
|
||||
if "bsN_tokens" in fail:
|
||||
print(f" BS=N tokens: {fail['bsN_tokens']}")
|
||||
|
||||
if "bs1_all_logprobs" in fail:
|
||||
print(f" BS=1 logprobs for all {len(fail['bs1_all_logprobs'])} steps:")
|
||||
for step_idx, logprobs in enumerate(fail["bs1_all_logprobs"]):
|
||||
print(f" Step {step_idx}: {logprobs}")
|
||||
print(f" BS=N logprobs for all {len(fail['bsN_all_logprobs'])} steps:")
|
||||
for step_idx, logprobs in enumerate(fail["bsN_all_logprobs"]):
|
||||
print(f" Step {step_idx}: {logprobs}")
|
||||
print(f"{'=' * 80}\n")
|
||||
|
||||
# Fail the test with summary
|
||||
msg = (
|
||||
f"Batch invariance violated in {len(failed_prompts)}/{len(prompts)} prompts. See output above for details."
|
||||
)
|
||||
pytest.fail(msg)
|
||||
|
||||
|
||||
@pytest.mark.model(
|
||||
model_name=DEFAULT_MODEL,
|
||||
max_num_seqs=144,
|
||||
gpu_memory_utilization=0.95,
|
||||
max_model_len=8192,
|
||||
dtype="float16",
|
||||
tensor_parallel_size=int(os.getenv("VLLM_TP_SIZE", "1")),
|
||||
enable_prefix_caching=False,
|
||||
distributed_executor_backend="mp",
|
||||
compilation_config={
|
||||
"cudagraph_mode": "PIECEWISE",
|
||||
"cudagraph_capture_sizes": [1, 32, 64],
|
||||
},
|
||||
)
|
||||
def test_simple_generation(vllm_runner, monkeypatch: pytest.MonkeyPatch):
|
||||
"""
|
||||
Simple test that runs the model with a basic prompt and prints the output.
|
||||
Useful for quick smoke testing and debugging.
|
||||
"""
|
||||
prompt = "The capital of France is"
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0.0,
|
||||
max_tokens=20,
|
||||
)
|
||||
|
||||
print(f"\n{'=' * 80}")
|
||||
print("Running simple generation test")
|
||||
print(f"Prompt: '{prompt}'")
|
||||
print(f"{'=' * 80}\n")
|
||||
|
||||
outputs = vllm_runner.model.generate([prompt], sampling_params)
|
||||
|
||||
assert len(outputs) == 1
|
||||
output_text = outputs[0].outputs[0].text
|
||||
|
||||
print(f"Output: '{output_text}'")
|
||||
print(f"\n{'=' * 80}")
|
||||
print(f"Full completion: '{prompt}{output_text}'")
|
||||
print(f"{'=' * 80}\n")
|
||||
|
||||
|
||||
def test_logprobs_without_batch_invariance_should_fail(monkeypatch: pytest.MonkeyPatch):
|
||||
"""
|
||||
This test is the inverse of test_logprobs_bitwise_batch_invariance_bs1_vs_bsN.
|
||||
It DISABLES batch invariance mode and expects to see non-deterministic behavior
|
||||
between BS=1 and BS=N runs. This demonstrates that batch invariance is actually
|
||||
doing something useful.
|
||||
|
||||
The test will PASS if we detect differences (proving batch invariance matters).
|
||||
The test will FAIL if everything matches (suggesting batch invariance isn't needed).
|
||||
"""
|
||||
# CRITICAL: Clear model cache to free up memory before creating a new LLM instance
|
||||
# This test uses different configuration (batch invariance disabled) so it cannot reuse cached models
|
||||
model_cache.clear()
|
||||
|
||||
gc.collect()
|
||||
torch.npu.empty_cache()
|
||||
|
||||
# CRITICAL: Disable batch invariance for this test
|
||||
monkeypatch.setenv("VLLM_BATCH_INVARIANT", "0")
|
||||
seed = int(os.getenv("VLLM_TEST_SEED", "12345"))
|
||||
random.seed(seed)
|
||||
model_name = DEFAULT_MODEL
|
||||
tp_size = int(os.getenv("VLLM_TEST_TP_SIZE", "1"))
|
||||
|
||||
print(f"\n{'=' * 80}")
|
||||
print("BATCH INVARIANCE DISABLED: Expecting non-deterministic behavior")
|
||||
print(f"{'=' * 80}\n")
|
||||
|
||||
llm = LLM(
|
||||
model=model_name,
|
||||
tensor_parallel_size=tp_size,
|
||||
enable_prefix_caching=False,
|
||||
max_num_seqs=32,
|
||||
max_model_len=8192,
|
||||
dtype="bfloat16",
|
||||
compilation_config={
|
||||
"cudagraph_mode": "PIECEWISE",
|
||||
"cudagraph_capture_sizes": [1, 32, 64],
|
||||
},
|
||||
distributed_executor_backend="mp",
|
||||
)
|
||||
|
||||
# build ragged prompts to change shapes significantly across BS=1 vs BS=N
|
||||
long_min = int(os.getenv("VLLM_MIN_PROMPT", "768"))
|
||||
long_max = int(os.getenv("VLLM_MAX_PROMPT", "2048"))
|
||||
prompts: list[str] = []
|
||||
options = [
|
||||
(max(long_min, 1536), max(long_max, 3072)), # very long
|
||||
(max(1024, long_min), max(2048, long_max)), # long
|
||||
(256, 512), # mid
|
||||
(10, 20), # short
|
||||
]
|
||||
|
||||
for _ in range(32):
|
||||
lo, hi = random.choice(options)
|
||||
prompts.append(_random_prompt(lo, hi))
|
||||
|
||||
sp = SamplingParams(
|
||||
temperature=0.6,
|
||||
top_p=1.0,
|
||||
max_tokens=8,
|
||||
seed=1234,
|
||||
logprobs=5,
|
||||
)
|
||||
|
||||
# BS=1: run prompts individually and collect logprobs per step.
|
||||
print("\n" + "=" * 80)
|
||||
print("STARTING BS=1 RUNS (each prompt individually)")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
bs1_logprobs_per_prompt = []
|
||||
bs1_tokens_per_prompt = []
|
||||
for idx, p in enumerate(prompts):
|
||||
print(f"\n[BS=1] Running prompt {idx}/{len(prompts)} - Preview: {p[:80]}...")
|
||||
outs = llm.generate([p], sp, use_tqdm=False)
|
||||
assert len(outs) == 1
|
||||
step_logprobs, token_ids = _extract_step_logprobs(outs[0])
|
||||
if step_logprobs is None:
|
||||
pytest.skip("Logits are not available on RequestOutput; enable logprobs return to run this test.")
|
||||
bs1_logprobs_per_prompt.append(step_logprobs)
|
||||
bs1_tokens_per_prompt.append(token_ids)
|
||||
print(f"[BS=1] Prompt {idx} generated tokens: {token_ids}")
|
||||
|
||||
# BS=N: run prompts in a batch and collect logprobs per step for each prompt.
|
||||
print("\n" + "=" * 80)
|
||||
print(f"STARTING BS={len(prompts)} RUN (all prompts batched)")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
outs_batched = llm.generate(prompts, sp, use_tqdm=False)
|
||||
assert len(outs_batched) == len(prompts)
|
||||
bsN_logprobs_per_prompt = []
|
||||
bsN_tokens_per_prompt = []
|
||||
|
||||
print(f"\n[BS={len(prompts)}] Processing batched outputs...")
|
||||
for idx, o in enumerate(outs_batched):
|
||||
tokens = o.outputs[0].token_ids if o.outputs else "N/A"
|
||||
print(f"[BS={len(prompts)}] Prompt {idx} generated tokens: {tokens}")
|
||||
step_logprobs, token_ids = _extract_step_logprobs(o)
|
||||
if step_logprobs is None:
|
||||
pytest.skip("Logits are not available on RequestOutput; enable logprobs return to run this test.")
|
||||
bsN_logprobs_per_prompt.append(step_logprobs)
|
||||
bsN_tokens_per_prompt.append(token_ids)
|
||||
|
||||
# Compare step-by-step logprobs for each prompt between BS=1 and BS=N runs.
|
||||
differences_found = []
|
||||
for i, (logprobs_bs1, logprobs_bsN, tokens_bs1, tokens_bsN) in enumerate(
|
||||
zip(
|
||||
bs1_logprobs_per_prompt,
|
||||
bsN_logprobs_per_prompt,
|
||||
bs1_tokens_per_prompt,
|
||||
bsN_tokens_per_prompt,
|
||||
)
|
||||
):
|
||||
if len(logprobs_bs1) != len(logprobs_bsN):
|
||||
reason = f"Different number of steps: {len(logprobs_bs1)} (BS=1) vs {len(logprobs_bsN)} (BS=N)"
|
||||
differences_found.append(
|
||||
{
|
||||
"prompt_idx": i,
|
||||
"step": "all",
|
||||
"reason": reason,
|
||||
"prompt_preview": prompts[i][:100],
|
||||
"bs1_tokens": tokens_bs1,
|
||||
"bsN_tokens": tokens_bsN,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Check if tokens match first
|
||||
if tokens_bs1 != tokens_bsN:
|
||||
differences_found.append(
|
||||
{
|
||||
"prompt_idx": i,
|
||||
"step": "sampling",
|
||||
"reason": "Different tokens sampled",
|
||||
"prompt_preview": prompts[i][:100],
|
||||
"bs1_tokens": tokens_bs1,
|
||||
"bsN_tokens": tokens_bsN,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
for t, (a, b) in enumerate(zip(logprobs_bs1, logprobs_bsN)):
|
||||
if a.shape != b.shape:
|
||||
differences_found.append(
|
||||
{
|
||||
"prompt_idx": i,
|
||||
"step": t,
|
||||
"reason": f"Shape mismatch: {a.shape} vs {b.shape}",
|
||||
"prompt_preview": prompts[i][:100],
|
||||
"bs1_tokens": tokens_bs1,
|
||||
"bsN_tokens": tokens_bsN,
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
if not torch.equal(a, b):
|
||||
max_diff = torch.abs(a - b).max().item()
|
||||
print(f"\n[EXPECTED DIVERGENCE FOUND] Prompt {i}, Token {t}: max_diff={max_diff:.6e}")
|
||||
bs1_tok = tokens_bs1[t] if t < len(tokens_bs1) else "N/A"
|
||||
bsN_tok = tokens_bsN[t] if t < len(tokens_bsN) else "N/A"
|
||||
print(f" Token IDs: bs1={bs1_tok}, bsN={bsN_tok}")
|
||||
print(f" BS=1 logprob: {a.tolist()}")
|
||||
print(f" BS=N logprob: {b.tolist()}")
|
||||
differences_found.append(
|
||||
{
|
||||
"prompt_idx": i,
|
||||
"step": t,
|
||||
"reason": f"Bitwise mismatch (max_diff={max_diff:.6e})",
|
||||
"prompt_preview": prompts[i][:100],
|
||||
"bs1_tokens": tokens_bs1,
|
||||
"bsN_tokens": tokens_bsN,
|
||||
}
|
||||
)
|
||||
break
|
||||
del llm
|
||||
cleanup_dist_env_and_memory()
|
||||
# Print summary
|
||||
print(f"\n{'=' * 80}")
|
||||
if differences_found:
|
||||
success_msg = (
|
||||
f"✓ SUCCESS: Batch invariance is doing something! "
|
||||
f"Found {len(differences_found)}/{len(prompts)} prompts "
|
||||
f"with differences when batch invariance was DISABLED."
|
||||
)
|
||||
print(success_msg)
|
||||
print(f"{'=' * 80}")
|
||||
for diff in differences_found:
|
||||
print(f"\nPrompt {diff['prompt_idx']} (step {diff['step']}):")
|
||||
print(f" Reason: {diff['reason']}")
|
||||
print(f" Preview: {diff['prompt_preview']}...")
|
||||
if "bs1_tokens" in diff:
|
||||
print(f" BS=1 tokens: {diff['bs1_tokens']}")
|
||||
if "bsN_tokens" in diff:
|
||||
print(f" BS=N tokens: {diff['bsN_tokens']}")
|
||||
print(f"{'=' * 80}\n")
|
||||
# Test PASSES because we found differences (batch invariance matters!)
|
||||
return
|
||||
else:
|
||||
# Test FAILS because everything matched even without batch invariance
|
||||
fail_msg = (
|
||||
f"✗ UNEXPECTED: All {len(prompts)} prompts matched "
|
||||
f"between BS=1 and BS=N even with batch invariance DISABLED. "
|
||||
f"This suggests batch invariance might not be necessary, "
|
||||
f"or the test needs more sensitive prompts."
|
||||
)
|
||||
print(fail_msg)
|
||||
print(f"{'=' * 80}\n")
|
||||
pytest.fail(fail_msg)
|
||||
57
tests/e2e/pull_request/one_card/test_camem.py
Normal file
57
tests/e2e/pull_request/one_card/test_camem.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
from vllm import SamplingParams
|
||||
from vllm.utils.mem_constants import GiB_bytes
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from tests.e2e.utils import fork_new_process_for_each_test
|
||||
|
||||
|
||||
@fork_new_process_for_each_test
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_NZ": "0"})
|
||||
def test_end_to_end():
|
||||
free, total = torch.npu.mem_get_info()
|
||||
used_bytes_baseline = total - free # in case other process is running
|
||||
|
||||
prompt = "How are you?"
|
||||
sampling_params = SamplingParams(temperature=0, max_tokens=10)
|
||||
|
||||
with VllmRunner("Qwen/Qwen3-0.6B", enable_sleep_mode=True, cudagraph_capture_sizes=[1, 2, 4, 8]) as runner:
|
||||
output = runner.model.generate(prompt, sampling_params)
|
||||
# the benefit of `llm.sleep(level=2)` is mainly CPU memory usage,
|
||||
# which is difficult to measure in the test. therefore, we only
|
||||
# test sleep level 1 here.
|
||||
runner.model.sleep(level=1)
|
||||
|
||||
free_gpu_bytes_after_sleep, total = torch.npu.mem_get_info()
|
||||
used_bytes = total - free_gpu_bytes_after_sleep - used_bytes_baseline
|
||||
# now the memory usage should be less than the model weights
|
||||
# (0.5B model, 1GiB weights)
|
||||
assert used_bytes < 1 * GiB_bytes
|
||||
|
||||
runner.model.wake_up()
|
||||
output2 = runner.model.generate(prompt, sampling_params)
|
||||
|
||||
# cmp output
|
||||
assert output[0].outputs[0].text == output2[0].outputs[0].text
|
||||
@@ -0,0 +1,74 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
|
||||
from tests.e2e.conftest import ModelName
|
||||
|
||||
os.environ["VLLM_USE_MODELSCOPE"] = "True"
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
MODELS = ModelName.QWEN3_06B
|
||||
|
||||
|
||||
def get_prompt_embeds(chat, tokenizer, embedding_layer):
|
||||
"""Convert chat messages to prompt embeddings."""
|
||||
token_ids = tokenizer.apply_chat_template(chat, add_generation_prompt=True, return_tensors="pt", return_dict=False)
|
||||
prompt_embeds = embedding_layer(token_ids).squeeze(0)
|
||||
return prompt_embeds
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=MODELS,
|
||||
compilation_config={"cudagraph_capture_sizes": [1, 2, 4, 8]},
|
||||
extra_kwargs={"enable_prompt_embeds": True},
|
||||
)
|
||||
def test_mixed_prompt_embeds_and_text(vllm_runner):
|
||||
"""Test mixed inputs with both prompt embeddings and text prompts."""
|
||||
# Prepare prompt embeddings for first request
|
||||
tokenizer = AutoTokenizer.from_pretrained(MODELS)
|
||||
transformers_model = AutoModelForCausalLM.from_pretrained(MODELS)
|
||||
embedding_layer = transformers_model.get_input_embeddings()
|
||||
|
||||
chat = [{"role": "user", "content": "What is AI?"}]
|
||||
prompt_embeds = get_prompt_embeds(chat, tokenizer, embedding_layer)
|
||||
|
||||
# Prepare text prompt for second request
|
||||
text_prompt = "What is machine learning?"
|
||||
|
||||
# Test prompt embeddings
|
||||
embeds_output = vllm_runner.model.generate(
|
||||
{
|
||||
"prompt_embeds": prompt_embeds,
|
||||
}
|
||||
)
|
||||
|
||||
# Test text prompt
|
||||
text_output = vllm_runner.model.generate(text_prompt)
|
||||
|
||||
# Verify both types of inputs work
|
||||
assert len(embeds_output) == 1
|
||||
assert len(text_output) == 1
|
||||
assert len(embeds_output[0].outputs[0].text) > 0
|
||||
assert len(text_output[0].outputs[0].text) > 0
|
||||
|
||||
print("\n[Prompt Embeds Output]:", embeds_output[0].outputs[0].text)
|
||||
print("[Text Prompt Output]:", text_output[0].outputs[0].text)
|
||||
177
tests/e2e/pull_request/one_card/test_cpu_offloading.py
Normal file
177
tests/e2e/pull_request/one_card/test_cpu_offloading.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import socket
|
||||
import time
|
||||
|
||||
import msgspec
|
||||
import msgspec.msgpack
|
||||
import pytest
|
||||
import zmq
|
||||
from vllm import LLM, SamplingParams, TokensPrompt
|
||||
from vllm.config import KVEventsConfig, KVTransferConfig
|
||||
from vllm.distributed.kv_events import BlockStored, KVEventBatch
|
||||
|
||||
|
||||
class MockSubscriber:
|
||||
"""Helper class to receive and verify published events"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
endpoint: str,
|
||||
topic: str,
|
||||
):
|
||||
self.ctx = zmq.Context.instance() # type: ignore
|
||||
self.topic_bytes = topic.encode("utf-8")
|
||||
|
||||
# Set up subscriber socket
|
||||
self.sub = self.ctx.socket(zmq.SUB) # type: ignore
|
||||
self.sub.setsockopt(zmq.SUBSCRIBE, self.topic_bytes) # type: ignore
|
||||
self.sub.connect(endpoint)
|
||||
|
||||
self.decoder = msgspec.msgpack.Decoder(type=KVEventBatch)
|
||||
|
||||
def get_new_cpu_stored_events(self) -> list[BlockStored]:
|
||||
cpu_stored_events: list[BlockStored] = []
|
||||
|
||||
poller = zmq.Poller() # type: ignore
|
||||
poller.register(self.sub, zmq.POLLIN) # type: ignore
|
||||
|
||||
timeout = 1000 # 1 second
|
||||
while True:
|
||||
events = dict(poller.poll(timeout))
|
||||
|
||||
if events.get(self.sub) != zmq.POLLIN: # type: ignore
|
||||
return cpu_stored_events
|
||||
|
||||
topic_bytes, _, payload = self.sub.recv_multipart()
|
||||
|
||||
assert topic_bytes == self.topic_bytes
|
||||
|
||||
event_batch = self.decoder.decode(payload)
|
||||
assert isinstance(event_batch, KVEventBatch)
|
||||
for event in event_batch.events:
|
||||
if isinstance(event, BlockStored) and event.medium == "CPU":
|
||||
cpu_stored_events.append(event)
|
||||
timeout = 100
|
||||
|
||||
def close(self):
|
||||
"""Clean up resources"""
|
||||
self.sub.close()
|
||||
|
||||
|
||||
def _latency_test(llm: LLM, subscriber: MockSubscriber):
|
||||
sampling_params = SamplingParams(max_tokens=1)
|
||||
|
||||
num_times_cpu_better_than_cold = 0
|
||||
num_tests = 10
|
||||
total_cold_time = 0.0
|
||||
total_gpu_hit_time = 0.0
|
||||
total_cpu_hit_time = 0.0
|
||||
prompt_token_ids = [0] * 10001
|
||||
for i in range(num_tests):
|
||||
prompt_token_ids[0] = i
|
||||
prompts = [TokensPrompt(prompt_token_ids=prompt_token_ids)]
|
||||
|
||||
# run generation - this should trigger saving KV cache
|
||||
start_time = time.time()
|
||||
llm.generate(prompts, sampling_params, use_tqdm=False)
|
||||
cold_time = time.time() - start_time
|
||||
total_cold_time += cold_time
|
||||
|
||||
# run generation again - should hit the GPU prefix cache
|
||||
start_time = time.time()
|
||||
llm.generate(prompts, sampling_params, use_tqdm=False)
|
||||
gpu_hit_time = time.time() - start_time
|
||||
total_gpu_hit_time += gpu_hit_time
|
||||
|
||||
# reset prefix cache to avoid GPU hit.
|
||||
llm.reset_prefix_cache()
|
||||
|
||||
assert subscriber.get_new_cpu_stored_events()
|
||||
|
||||
# run generation again - this should trigger loading from CPU
|
||||
start_time = time.time()
|
||||
llm.generate(prompts, sampling_params, use_tqdm=False)
|
||||
cpu_hit_time = time.time() - start_time
|
||||
total_cpu_hit_time += cpu_hit_time
|
||||
|
||||
if cpu_hit_time < cold_time:
|
||||
num_times_cpu_better_than_cold += 1
|
||||
|
||||
print("Average times:")
|
||||
print(f" Cold: {total_cold_time * 1000 / num_tests:.2f}ms")
|
||||
print(f" GPU hit: {total_gpu_hit_time * 1000 / num_tests:.2f}ms")
|
||||
print(f" CPU hit: {total_cpu_hit_time * 1000 / num_tests:.2f}ms")
|
||||
|
||||
assert num_times_cpu_better_than_cold >= 0.8 * num_tests
|
||||
|
||||
|
||||
def _accuracy_test(llm: LLM, subscriber: MockSubscriber):
|
||||
sampling_params = SamplingParams(max_tokens=1)
|
||||
cpu_block_size = llm.llm_engine.vllm_config.kv_transfer_config.kv_connector_extra_config["block_size"]
|
||||
|
||||
subscriber.get_new_cpu_stored_events()
|
||||
|
||||
# prepend prompt to be cpu block aligned
|
||||
prompt = "Let's count to 10. One, two, three, four,"
|
||||
while len(llm.generate(prompt, use_tqdm=False)[0].prompt_token_ids) % cpu_block_size != 0:
|
||||
prompt = ". " + prompt
|
||||
|
||||
assert subscriber.get_new_cpu_stored_events()
|
||||
|
||||
test_count = 100
|
||||
success_count = 0
|
||||
for i in range(test_count):
|
||||
if llm.generate(prompt, sampling_params, use_tqdm=False)[0].outputs[0].text == " five":
|
||||
success_count += 1
|
||||
|
||||
assert success_count >= 0.5 * test_count
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="cpu offload connector is deprecated.")
|
||||
def test_cpu_offloading() -> None:
|
||||
"""
|
||||
Tests OffloadingConnector with CPUOffloadingSpec.
|
||||
"""
|
||||
|
||||
# configure OffloadingConnector (spec_name=CPUOffloadingSpec by default)
|
||||
kv_transfer_config = KVTransferConfig(
|
||||
kv_connector="OffloadingConnector",
|
||||
kv_role="kv_both",
|
||||
kv_connector_extra_config={
|
||||
"num_cpu_blocks": 1000,
|
||||
"block_size": 128,
|
||||
"spec_name": "NPUOffloadingSpec",
|
||||
"spec_module_path": "vllm_ascend.kv_offload.npu",
|
||||
},
|
||||
)
|
||||
|
||||
port: int
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("0.0.0.0", 0))
|
||||
port = s.getsockname()[1]
|
||||
|
||||
events_endpoint = f"tcp://*:{port}"
|
||||
kv_events_config = KVEventsConfig(
|
||||
enable_kv_cache_events=True,
|
||||
publisher="zmq",
|
||||
endpoint=events_endpoint,
|
||||
topic="test",
|
||||
)
|
||||
|
||||
llm = LLM(
|
||||
model="Qwen/Qwen3-0.6B",
|
||||
gpu_memory_utilization=0.5,
|
||||
kv_events_config=kv_events_config,
|
||||
kv_transfer_config=kv_transfer_config,
|
||||
)
|
||||
|
||||
events_endpoint = events_endpoint.replace("*", "127.0.0.1")
|
||||
subscriber = MockSubscriber(events_endpoint, topic=kv_events_config.topic)
|
||||
|
||||
try:
|
||||
_latency_test(llm, subscriber)
|
||||
_accuracy_test(llm, subscriber)
|
||||
finally:
|
||||
subscriber.close()
|
||||
del llm
|
||||
130
tests/e2e/pull_request/one_card/test_cpu_weight_offload.py
Normal file
130
tests/e2e/pull_request/one_card/test_cpu_weight_offload.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
"""End-to-end tests for CPU weight offloading on Ascend NPU.
|
||||
|
||||
Covers both the prefetch backend (NPUPrefetchOffloader) and the UVA
|
||||
backend (functional_call fallback path, since UVA is not available on
|
||||
NPU hardware). Tests verify that offloading produces the same outputs
|
||||
as the baseline (no offloading).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import wait_until_npu_memory_free
|
||||
from tests.e2e.pull_request.utils import PROMPTS_SHORT, compare_logprobs
|
||||
|
||||
MODEL = "Qwen/Qwen3-0.6B"
|
||||
|
||||
|
||||
# -------------------- Prefetch backend tests --------------------
|
||||
|
||||
|
||||
@wait_until_npu_memory_free()
|
||||
def test_prefetch_offload_eager():
|
||||
"""Test prefetch CPU offloading in eager mode.
|
||||
|
||||
Compares outputs between:
|
||||
1. Baseline (eager, no offloading)
|
||||
2. Prefetch offloading (group_size=4, num_in_group=1)
|
||||
with enforce_eager=True (no ACL graph capture)
|
||||
"""
|
||||
runner_kwargs = {
|
||||
"model_name": MODEL,
|
||||
"max_model_len": 512,
|
||||
"enforce_eager": True,
|
||||
"offload_backend": "prefetch",
|
||||
"offload_group_size": 4,
|
||||
"offload_num_in_group": 1,
|
||||
}
|
||||
compare_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
|
||||
|
||||
|
||||
@wait_until_npu_memory_free()
|
||||
def test_prefetch_offload_aclgraph():
|
||||
"""Test prefetch CPU offloading with ACL graph capture.
|
||||
|
||||
Compares outputs between:
|
||||
1. Baseline (eager, no offloading)
|
||||
2. Prefetch offloading (group_size=4, num_in_group=1)
|
||||
with ACL graph capture enabled (default, non-eager)
|
||||
"""
|
||||
runner_kwargs = {
|
||||
"model_name": MODEL,
|
||||
"max_model_len": 512,
|
||||
"cudagraph_capture_sizes": [1, 2, 4, 8],
|
||||
"offload_backend": "prefetch",
|
||||
"offload_group_size": 4,
|
||||
"offload_num_in_group": 1,
|
||||
}
|
||||
compare_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
|
||||
|
||||
|
||||
@wait_until_npu_memory_free()
|
||||
def test_prefetch_offload_selective_params():
|
||||
"""Test selective parameter offloading (MLP weights only).
|
||||
|
||||
Only offloads gate_up_proj and down_proj parameters, leaving
|
||||
attention weights on NPU.
|
||||
"""
|
||||
runner_kwargs = {
|
||||
"model_name": MODEL,
|
||||
"max_model_len": 512,
|
||||
"enforce_eager": True,
|
||||
"offload_backend": "prefetch",
|
||||
"offload_group_size": 8,
|
||||
"offload_num_in_group": 2,
|
||||
"offload_prefetch_step": 1,
|
||||
"offload_params": {"gate_up_proj", "down_proj"},
|
||||
}
|
||||
compare_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
|
||||
|
||||
|
||||
# -------------------- UVA backend tests --------------------
|
||||
# UVA (Unified Virtual Addressing) is not available on Ascend NPU, so
|
||||
# the UVA offloader falls back to the functional_call path that moves
|
||||
# weights to device on-demand. Tests below mirror the upstream
|
||||
# test_cpu_offload.py parametrization but only exercise the non-UVA
|
||||
# (functional_call) path with enforce_eager, as NPU does not support
|
||||
# UVA zero-copy.
|
||||
|
||||
|
||||
@pytest.mark.parametrize("disable_pin_memory", [False, True])
|
||||
@wait_until_npu_memory_free()
|
||||
def test_uva_offload_functional_call(disable_pin_memory):
|
||||
"""Test UVA offloader's functional_call fallback on NPU.
|
||||
|
||||
With UVA disabled (forced by env var), the UVA offloader falls back
|
||||
to moving weights to device inside a functional_call wrapper.
|
||||
enforce_eager is required because this fallback is incompatible
|
||||
with graph capture.
|
||||
|
||||
Parametrized over pin_memory to cover both pinned and unpinned
|
||||
CPU storage paths.
|
||||
"""
|
||||
old_uva = os.environ.get("VLLM_WEIGHT_OFFLOADING_DISABLE_UVA")
|
||||
old_pin = os.environ.get("VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY")
|
||||
try:
|
||||
os.environ["VLLM_WEIGHT_OFFLOADING_DISABLE_UVA"] = "1"
|
||||
os.environ["VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY"] = str(int(disable_pin_memory))
|
||||
runner_kwargs = {
|
||||
"model_name": MODEL,
|
||||
"max_model_len": 512,
|
||||
"enforce_eager": True,
|
||||
"cpu_offload_gb": 1,
|
||||
}
|
||||
compare_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
|
||||
finally:
|
||||
if old_uva is None:
|
||||
os.environ.pop("VLLM_WEIGHT_OFFLOADING_DISABLE_UVA", None)
|
||||
else:
|
||||
os.environ["VLLM_WEIGHT_OFFLOADING_DISABLE_UVA"] = old_uva
|
||||
if old_pin is None:
|
||||
os.environ.pop("VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY", None)
|
||||
else:
|
||||
os.environ["VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY"] = old_pin
|
||||
291
tests/e2e/pull_request/one_card/test_guided_decoding.py
Normal file
291
tests/e2e/pull_request/one_card/test_guided_decoding.py
Normal file
@@ -0,0 +1,291 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/entrypoints/llm/test_guided_generate.py
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import jsonschema
|
||||
import pytest
|
||||
import regex as re
|
||||
from vllm.outputs import RequestOutput
|
||||
from vllm.sampling_params import SamplingParams, StructuredOutputsParams
|
||||
|
||||
from tests.e2e.conftest import ModelName
|
||||
from vllm_ascend.utils import vllm_version_is
|
||||
|
||||
os.environ["VLLM_BATCH_INVARIANT"] = "1"
|
||||
|
||||
MODEL_NAME = ModelName.QWEN3_06B
|
||||
|
||||
GuidedDecodingBackend = ["xgrammar", "guidance", "outlines"]
|
||||
REGEX_COMPILATION_TIMEOUT_ENV = {"VLLM_REGEX_COMPILATION_TIMEOUT_S": "30"}
|
||||
|
||||
|
||||
@pytest.fixture(params=[False, True], ids=["v1", "v2"])
|
||||
def model_runner_env(request):
|
||||
use_v2_model_runner = request.param
|
||||
if use_v2_model_runner and vllm_version_is("0.23.0"):
|
||||
pytest.skip("No need to support v2 model runner for vLLM tag version.")
|
||||
|
||||
with patch.dict(os.environ, {"VLLM_USE_V2_MODEL_RUNNER": "1" if use_v2_model_runner else "0"}):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def sample_regex():
|
||||
return (
|
||||
r"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.){3}"
|
||||
r"(25[0-5]|(2[0-4]|1\d|[1-9]|)\d)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def sample_json_schema():
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"},
|
||||
"skills": {"type": "array", "items": {"type": "string", "maxLength": 10}, "minItems": 3},
|
||||
"work_history": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"company": {"type": "string"},
|
||||
"duration": {"type": "number"},
|
||||
"position": {"type": "string"},
|
||||
},
|
||||
"required": ["company", "position"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["name", "age", "skills", "work_history"],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=MODEL_NAME,
|
||||
compilation_config={"cudagraph_capture_sizes": [1, 2, 4, 8]},
|
||||
extra_kwargs={"seed": 0, "structured_outputs_config": {"backend": "xgrammar"}},
|
||||
)
|
||||
def test_guided_json_completion_xgrammar(sample_json_schema, request):
|
||||
sampling_params = SamplingParams(
|
||||
temperature=1.0, max_tokens=500, structured_outputs=StructuredOutputsParams(json=sample_json_schema)
|
||||
)
|
||||
if not vllm_version_is("0.23.0"):
|
||||
model_marker = request.node.get_closest_marker("model")
|
||||
model_marker.kwargs["env_vars"] = REGEX_COMPILATION_TIMEOUT_ENV
|
||||
with patch.dict(os.environ, REGEX_COMPILATION_TIMEOUT_ENV, clear=False):
|
||||
vllm_runner = request.getfixturevalue("vllm_runner")
|
||||
prompts = [f"Give an example JSON for an employee profile that fits this schema: {sample_json_schema}"] * 2
|
||||
inputs = vllm_runner.get_inputs(prompts)
|
||||
outputs = vllm_runner.model.generate(inputs, sampling_params=sampling_params)
|
||||
|
||||
assert outputs is not None
|
||||
for output in outputs:
|
||||
assert output is not None
|
||||
assert isinstance(output, RequestOutput)
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
assert generated_text is not None
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
output_json = json.loads(generated_text)
|
||||
jsonschema.validate(instance=output_json, schema=sample_json_schema)
|
||||
else:
|
||||
vllm_runner = request.getfixturevalue("vllm_runner")
|
||||
prompts = [f"Give an example JSON for an employee profile that fits this schema: {sample_json_schema}"] * 2
|
||||
inputs = vllm_runner.get_inputs(prompts)
|
||||
outputs = vllm_runner.model.generate(inputs, sampling_params=sampling_params)
|
||||
|
||||
assert outputs is not None
|
||||
for output in outputs:
|
||||
assert output is not None
|
||||
assert isinstance(output, RequestOutput)
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
assert generated_text is not None
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
output_json = json.loads(generated_text)
|
||||
jsonschema.validate(instance=output_json, schema=sample_json_schema)
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=MODEL_NAME,
|
||||
compilation_config={"cudagraph_capture_sizes": [1, 2, 4, 8]},
|
||||
extra_kwargs={"seed": 0, "structured_outputs_config": {"backend": "xgrammar"}},
|
||||
)
|
||||
def test_guided_regex_xgrammar(sample_regex, vllm_runner):
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0.8, top_p=0.95, structured_outputs=StructuredOutputsParams(regex=sample_regex)
|
||||
)
|
||||
prompts = [f"Give an example IPv4 address with this regex: {sample_regex}"] * 2
|
||||
inputs = vllm_runner.get_inputs(prompts)
|
||||
outputs = vllm_runner.model.generate(inputs, sampling_params=sampling_params)
|
||||
assert outputs is not None
|
||||
for output in outputs:
|
||||
assert output is not None
|
||||
assert isinstance(output, RequestOutput)
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
print(generated_text)
|
||||
assert generated_text is not None
|
||||
assert re.fullmatch(".*", generated_text) is not None
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=MODEL_NAME,
|
||||
compilation_config={"cudagraph_capture_sizes": [1, 2, 4, 8]},
|
||||
extra_kwargs={"seed": 0, "structured_outputs_config": {"backend": "guidance"}},
|
||||
)
|
||||
def test_guided_json_completion_guidance(sample_json_schema, vllm_runner):
|
||||
sampling_params = SamplingParams(
|
||||
temperature=1.0, max_tokens=500, structured_outputs=StructuredOutputsParams(json=sample_json_schema)
|
||||
)
|
||||
prompts = [f"Give an example JSON for an employee profile that fits this schema: {sample_json_schema}"] * 2
|
||||
inputs = vllm_runner.get_inputs(prompts)
|
||||
outputs = vllm_runner.model.generate(inputs, sampling_params=sampling_params)
|
||||
|
||||
assert outputs is not None
|
||||
for output in outputs:
|
||||
assert output is not None
|
||||
assert isinstance(output, RequestOutput)
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
assert generated_text is not None
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
output_json = json.loads(generated_text)
|
||||
jsonschema.validate(instance=output_json, schema=sample_json_schema)
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=MODEL_NAME,
|
||||
compilation_config={"cudagraph_capture_sizes": [1, 2, 4, 8]},
|
||||
extra_kwargs={"seed": 0, "structured_outputs_config": {"backend": "guidance"}},
|
||||
)
|
||||
def test_guided_regex_guidance(sample_regex, vllm_runner):
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0.8, top_p=0.95, structured_outputs=StructuredOutputsParams(regex=sample_regex)
|
||||
)
|
||||
prompts = [f"Give an example IPv4 address with this regex: {sample_regex}"] * 2
|
||||
inputs = vllm_runner.get_inputs(prompts)
|
||||
outputs = vllm_runner.model.generate(inputs, sampling_params=sampling_params)
|
||||
assert outputs is not None
|
||||
for output in outputs:
|
||||
assert output is not None
|
||||
assert isinstance(output, RequestOutput)
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
print(generated_text)
|
||||
assert generated_text is not None
|
||||
assert re.fullmatch(".*", generated_text) is not None
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=MODEL_NAME,
|
||||
compilation_config={"cudagraph_capture_sizes": [1, 2, 4, 8]},
|
||||
extra_kwargs={"seed": 0, "structured_outputs_config": {"backend": "auto"}},
|
||||
)
|
||||
def test_guided_auto_rejects_mixed_structured_output_backends(vllm_runner):
|
||||
xgrammar_schema = {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
}
|
||||
guidance_schema = {
|
||||
"type": "object",
|
||||
"properties": {"count": {"type": "integer", "multipleOf": 2}},
|
||||
"required": ["count"],
|
||||
}
|
||||
|
||||
xgrammar_params = SamplingParams(
|
||||
temperature=0.0,
|
||||
max_tokens=32,
|
||||
structured_outputs=StructuredOutputsParams(json=xgrammar_schema),
|
||||
)
|
||||
prompts = [f"Give an example JSON that fits this schema: {xgrammar_schema}"]
|
||||
inputs = vllm_runner.get_inputs(prompts)
|
||||
outputs = vllm_runner.model.generate(inputs, sampling_params=xgrammar_params)
|
||||
|
||||
assert outputs is not None
|
||||
assert outputs[0] is not None
|
||||
|
||||
guidance_params = SamplingParams(
|
||||
temperature=0.0,
|
||||
max_tokens=32,
|
||||
structured_outputs=StructuredOutputsParams(json=guidance_schema),
|
||||
)
|
||||
prompts = [f"Give an example JSON that fits this schema: {guidance_schema}"]
|
||||
inputs = vllm_runner.get_inputs(prompts)
|
||||
with pytest.raises(ValueError, match="already using 'xgrammar'.*'guidance'"):
|
||||
vllm_runner.model.generate(inputs, sampling_params=guidance_params)
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=MODEL_NAME,
|
||||
compilation_config={"cudagraph_capture_sizes": [1, 2, 4, 8]},
|
||||
extra_kwargs={"seed": 0, "structured_outputs_config": {"backend": "outlines"}},
|
||||
)
|
||||
def test_guided_json_completion_outlines(sample_json_schema, request):
|
||||
sampling_params = SamplingParams(
|
||||
temperature=1.0, max_tokens=500, structured_outputs=StructuredOutputsParams(json=sample_json_schema)
|
||||
)
|
||||
if not vllm_version_is("0.23.0"):
|
||||
model_marker = request.node.get_closest_marker("model")
|
||||
model_marker.kwargs["env_vars"] = REGEX_COMPILATION_TIMEOUT_ENV
|
||||
with patch.dict(os.environ, REGEX_COMPILATION_TIMEOUT_ENV, clear=False):
|
||||
vllm_runner = request.getfixturevalue("vllm_runner")
|
||||
prompts = [f"Give an example JSON for an employee profile that fits this schema: {sample_json_schema}"] * 2
|
||||
inputs = vllm_runner.get_inputs(prompts)
|
||||
outputs = vllm_runner.model.generate(inputs, sampling_params=sampling_params)
|
||||
|
||||
assert outputs is not None
|
||||
for output in outputs:
|
||||
assert output is not None
|
||||
assert isinstance(output, RequestOutput)
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
assert generated_text is not None
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
output_json = json.loads(generated_text)
|
||||
jsonschema.validate(instance=output_json, schema=sample_json_schema)
|
||||
else:
|
||||
vllm_runner = request.getfixturevalue("vllm_runner")
|
||||
prompts = [f"Give an example JSON for an employee profile that fits this schema: {sample_json_schema}"] * 2
|
||||
inputs = vllm_runner.get_inputs(prompts)
|
||||
outputs = vllm_runner.model.generate(inputs, sampling_params=sampling_params)
|
||||
|
||||
assert outputs is not None
|
||||
for output in outputs:
|
||||
assert output is not None
|
||||
assert isinstance(output, RequestOutput)
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
assert generated_text is not None
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
output_json = json.loads(generated_text)
|
||||
jsonschema.validate(instance=output_json, schema=sample_json_schema)
|
||||
42
tests/e2e/pull_request/one_card/test_minicpm.py
Normal file
42
tests/e2e/pull_request/one_card/test_minicpm.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/entrypoints/llm/test_guided_generate.py
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
MINICPM_MODELS = [
|
||||
"openbmb/MiniCPM-2B-sft-bf16",
|
||||
"OpenBMB/MiniCPM4-0.5B",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MINICPM_MODELS)
|
||||
def test_minicpm(model) -> None:
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
|
||||
with VllmRunner(model, max_model_len=512, gpu_memory_utilization=0.7) as runner:
|
||||
runner.generate_greedy(example_prompts, max_tokens)
|
||||
93
tests/e2e/pull_request/one_card/test_multi_instance.py
Normal file
93
tests/e2e/pull_request/one_card/test_multi_instance.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Two VllmRunner instances are nested so that the first instance's worker
|
||||
process is still holding NPU memory when the second instance's worker process
|
||||
starts. Both instances must:
|
||||
|
||||
1. Initialize without raising any exception (no OOM during
|
||||
determine_available_memory / KV-cache allocation).
|
||||
2. Successfully complete a short generation request.
|
||||
|
||||
The model is Qwen/Qwen3-0.6B (~0.5 GiB weights) and gpu_memory_utilization
|
||||
is set to 0.4 per instance so that two instances comfortably fit on a single
|
||||
64 GiB Ascend 910B card while leaving enough headroom to avoid the
|
||||
pre-fix negative-KV-cache condition.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
MODEL = "Qwen/Qwen3-0.6B"
|
||||
_PROMPTS = ["Hello, my name is"]
|
||||
_MAX_TOKENS = 5
|
||||
# Use a low utilization so two instances fit side-by-side on one card:
|
||||
# 2 × 0.4 × card_total ≤ card_total (holds for any card ≥ 1 GiB)
|
||||
_GPU_MEM_UTIL = 0.4
|
||||
_MAX_MODEL_LEN = 512
|
||||
|
||||
|
||||
def test_two_instances_on_single_card() -> None:
|
||||
"""
|
||||
Regression test for PR #7427 (multi-instance OOM on single card).
|
||||
|
||||
Start a first vllm-ascend instance; while it is still running and holding
|
||||
NPU memory, start a second instance with identical settings. Both must
|
||||
initialize correctly and produce non-empty outputs.
|
||||
|
||||
Failure signature (pre-fix):
|
||||
RuntimeError / ValueError during the second instance's init, or
|
||||
"Available KV cache memory: -X.XX GiB" in the logs followed by
|
||||
zero KV blocks being allocated.
|
||||
"""
|
||||
# ── First instance ──────────────────────────────────────────────────
|
||||
with VllmRunner(
|
||||
MODEL,
|
||||
max_model_len=_MAX_MODEL_LEN,
|
||||
gpu_memory_utilization=_GPU_MEM_UTIL,
|
||||
enforce_eager=True,
|
||||
) as runner1:
|
||||
# ── Second instance starts while first is still alive ────────────
|
||||
# This is the exact scenario from PR #7427: the second worker process
|
||||
# sees a reduced init_snapshot.free_memory because the first instance's
|
||||
# worker is still holding NPU memory.
|
||||
with VllmRunner(
|
||||
MODEL,
|
||||
max_model_len=_MAX_MODEL_LEN,
|
||||
gpu_memory_utilization=_GPU_MEM_UTIL,
|
||||
enforce_eager=True,
|
||||
) as runner2:
|
||||
outputs2 = runner2.generate_greedy(_PROMPTS, max_tokens=_MAX_TOKENS)
|
||||
|
||||
outputs1 = runner1.generate_greedy(_PROMPTS, max_tokens=_MAX_TOKENS)
|
||||
|
||||
# ── Assertions ───────────────────────────────────────────────────────
|
||||
assert outputs1, "First instance produced no outputs"
|
||||
assert outputs2, "Second instance produced no outputs"
|
||||
|
||||
_, text1 = outputs1[0]
|
||||
_, text2 = outputs2[0]
|
||||
|
||||
assert text1, "First instance output text is empty — model may have failed to run"
|
||||
assert text2, (
|
||||
"Second instance output text is empty — "
|
||||
"KV cache may have been allocated with zero blocks (pre-fix OOM regression)"
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
"""
|
||||
Compare the outputs of vLLM with multistream_overlap_shared_expert
|
||||
enabled and disabled.
|
||||
|
||||
Run `pytest tests/e2e/pull_request/one_card/test_multistream_overlap_shared_expert.py`.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from tests.e2e.model_utils import check_outputs_equal
|
||||
|
||||
MODELS = [
|
||||
"vllm-ascend/DeepSeek-V2-Lite-W8A8",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [32])
|
||||
def test_models_with_multistream_overlap_shared_expert(
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
) -> None:
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
sampling_params = SamplingParams(max_tokens=max_tokens, temperature=0.0)
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
enforce_eager=True,
|
||||
cudagraph_capture_sizes=[4, 8, 16, 32],
|
||||
additional_config={
|
||||
"multistream_overlap_shared_expert": True,
|
||||
},
|
||||
quantization="ascend",
|
||||
) as runner:
|
||||
vllm_moe_ms_eager_outputs = runner.model.generate(prompts, sampling_params)
|
||||
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
cudagraph_capture_sizes=[4, 8, 16, 32],
|
||||
additional_config={
|
||||
"multistream_overlap_shared_expert": True,
|
||||
},
|
||||
quantization="ascend",
|
||||
) as runner:
|
||||
vllm_moe_ms_aclgraph_outputs = runner.model.generate(prompts, sampling_params)
|
||||
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
enforce_eager=True,
|
||||
cudagraph_capture_sizes=[4, 8, 16, 32],
|
||||
quantization="ascend",
|
||||
) as runner:
|
||||
vllm_eager_outputs = runner.model.generate(prompts, sampling_params)
|
||||
|
||||
vllm_moe_ms_eager_outputs_list = []
|
||||
for output in vllm_moe_ms_eager_outputs:
|
||||
vllm_moe_ms_eager_outputs_list.append((output.outputs[0].index, output.outputs[0].text))
|
||||
|
||||
vllm_moe_ms_aclgraph_outputs_list = []
|
||||
for output in vllm_moe_ms_aclgraph_outputs:
|
||||
vllm_moe_ms_aclgraph_outputs_list.append((output.outputs[0].index, output.outputs[0].text))
|
||||
|
||||
vllm_eager_outputs_list = []
|
||||
for output in vllm_eager_outputs:
|
||||
vllm_eager_outputs_list.append((output.outputs[0].index, output.outputs[0].text))
|
||||
|
||||
check_outputs_equal(
|
||||
outputs_0_lst=vllm_eager_outputs_list,
|
||||
outputs_1_lst=vllm_moe_ms_eager_outputs_list,
|
||||
name_0="vllm_eager_outputs",
|
||||
name_1="vllm_moe_ms_eager_outputs",
|
||||
)
|
||||
|
||||
check_outputs_equal(
|
||||
outputs_0_lst=vllm_eager_outputs_list,
|
||||
outputs_1_lst=vllm_moe_ms_aclgraph_outputs_list,
|
||||
name_0="vllm_eager_outputs",
|
||||
name_1="vllm_moe_ms_aclgraph_outputs",
|
||||
)
|
||||
176
tests/e2e/pull_request/one_card/test_npu_ipc_weight_transfer.py
Normal file
176
tests/e2e/pull_request/one_card/test_npu_ipc_weight_transfer.py
Normal file
@@ -0,0 +1,176 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
"""End-to-end test for the NPU IPC weight transfer engine.
|
||||
|
||||
Unlike the HCCL engine, NPU IPC requires the trainer and the inference worker
|
||||
to be co-located on the *same* physical NPU chip, so only a single NPU is
|
||||
needed. The trainer model is built from the architecture config with random
|
||||
weights (download-free); set ``WEIGHT_TRANSFER_TEST_MODEL=/path/to/checkpoint``
|
||||
to share real weights instead. See ``examples/rl/rlhf_http_npu_ipc.py`` for the
|
||||
end-user workflow.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import torch
|
||||
import torch_npu # noqa: F401 # registers the NPU backend
|
||||
from transformers import AutoConfig, AutoModelForCausalLM
|
||||
|
||||
from tests.e2e.conftest import RemoteOpenAIServer
|
||||
|
||||
MODEL_NAME = "Qwen/Qwen3-0.6B"
|
||||
|
||||
INFERENCE_DEVICE_INDEX = 0
|
||||
|
||||
PROMPTS = [
|
||||
"Hello, my name is",
|
||||
"The capital of France is",
|
||||
]
|
||||
|
||||
UPDATE_TIMEOUT = 300
|
||||
CONTROL_TIMEOUT = 60
|
||||
|
||||
|
||||
def _build_trainer_model(device_index: int):
|
||||
device = f"npu:{device_index}"
|
||||
override_path = os.getenv("WEIGHT_TRANSFER_TEST_MODEL")
|
||||
if override_path:
|
||||
model = AutoModelForCausalLM.from_pretrained(override_path, dtype=torch.bfloat16)
|
||||
else:
|
||||
config = AutoConfig.from_pretrained(MODEL_NAME, trust_remote_code=True)
|
||||
model = AutoModelForCausalLM.from_config(config)
|
||||
model = model.to(device=device, dtype=torch.bfloat16)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
|
||||
def _post(server: RemoteOpenAIServer, route: str, *, json=None, timeout=CONTROL_TIMEOUT):
|
||||
response = requests.post(server.url_for(route), json=json, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
|
||||
def _generate(client, model, prompts):
|
||||
completions = []
|
||||
for prompt in prompts:
|
||||
response = client.completions.create(model=model, prompt=prompt, max_tokens=16, temperature=0)
|
||||
completions.append(response.choices[0].text)
|
||||
return completions
|
||||
|
||||
|
||||
def _has_lifecycle_endpoints(server: RemoteOpenAIServer) -> bool:
|
||||
"""Probe ``/start_weight_update``; also performs the actual call when present."""
|
||||
try:
|
||||
response = requests.post(
|
||||
server.url_for("start_weight_update"),
|
||||
json={"is_checkpoint_format": True},
|
||||
timeout=CONTROL_TIMEOUT,
|
||||
)
|
||||
except requests.RequestException:
|
||||
return False
|
||||
if response.status_code == 404:
|
||||
return False
|
||||
response.raise_for_status()
|
||||
return True
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
torch.npu.device_count() < 1,
|
||||
reason="NPU IPC weight transfer e2e test requires at least 1 NPU.",
|
||||
)
|
||||
def test_npu_ipc_weight_transfer_updates_server_weights():
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
port = get_open_port()
|
||||
server_args = [
|
||||
"--enforce-eager",
|
||||
"--load-format",
|
||||
"dummy",
|
||||
"--weight-transfer-config",
|
||||
'{"backend": "ipc"}',
|
||||
"--max-model-len",
|
||||
"1024",
|
||||
# IPC co-locates the trainer on the same NPU, so leave room for it.
|
||||
"--gpu-memory-utilization",
|
||||
"0.5",
|
||||
"--port",
|
||||
str(port),
|
||||
"--trust-remote-code",
|
||||
]
|
||||
# VLLM_SERVER_DEV_MODE registers the dev endpoints; insecure serialization
|
||||
# lets the server unpickle the IPC handles sent over HTTP. Pin the server to
|
||||
# physical NPU 0 so its IPC UUID matches the trainer below.
|
||||
env_dict = {
|
||||
"VLLM_SERVER_DEV_MODE": "1",
|
||||
"VLLM_ALLOW_INSECURE_SERIALIZATION": "1",
|
||||
"ASCEND_RT_VISIBLE_DEVICES": str(INFERENCE_DEVICE_INDEX),
|
||||
"VLLM_ASCEND_ENABLE_NZ": "0",
|
||||
}
|
||||
|
||||
with RemoteOpenAIServer(
|
||||
MODEL_NAME,
|
||||
vllm_serve_args=server_args,
|
||||
server_host="127.0.0.1",
|
||||
server_port=port,
|
||||
env_dict=env_dict,
|
||||
auto_port=False,
|
||||
) as server:
|
||||
client = server.get_client()
|
||||
|
||||
outputs_before = _generate(client, MODEL_NAME, PROMPTS)
|
||||
|
||||
# Trainer shares physical NPU 0 with the server. It leaves
|
||||
# ASCEND_RT_VISIBLE_DEVICES unset (identity mapping logical 0 ->
|
||||
# physical 0) so both processes resolve to the same IPC UUID.
|
||||
torch.npu.set_device(INFERENCE_DEVICE_INDEX)
|
||||
train_model = _build_trainer_model(INFERENCE_DEVICE_INDEX)
|
||||
os.environ["VLLM_ALLOW_INSECURE_SERIALIZATION"] = "1"
|
||||
|
||||
from vllm_ascend.distributed.weight_transfer.npu_ipc_engine import (
|
||||
NPUIPCTrainerSendWeightsArgs,
|
||||
NPUIPCWeightTransferEngine,
|
||||
)
|
||||
|
||||
_post(server, "init_weight_transfer_engine", json={"init_info": {}})
|
||||
|
||||
_post(server, "pause")
|
||||
# The probe performs /start_weight_update when present, so it must not
|
||||
# be called again below. Older vLLM without the lifecycle endpoints is
|
||||
# out of scope for this IPC test.
|
||||
if not _has_lifecycle_endpoints(server):
|
||||
_post(server, "resume")
|
||||
pytest.skip("vLLM build lacks the /start_weight_update lifecycle endpoints required by NPU IPC.")
|
||||
|
||||
# trainer_send_weights POSTs to /update_weights itself; the server
|
||||
# rebuilds tensors locally and loads them before the POST returns (no
|
||||
# collective back to the trainer, so no background thread is needed).
|
||||
# train_model stays referenced so the shared NPU storage outlives it.
|
||||
trainer_args = NPUIPCTrainerSendWeightsArgs(send_mode="http", url=server.url_root)
|
||||
NPUIPCWeightTransferEngine.trainer_send_weights(
|
||||
iterator=train_model.named_parameters(),
|
||||
trainer_args=trainer_args,
|
||||
)
|
||||
|
||||
_post(server, "finish_weight_update")
|
||||
_post(server, "resume")
|
||||
|
||||
outputs_after = _generate(client, MODEL_NAME, PROMPTS)
|
||||
|
||||
assert outputs_after != outputs_before, "server weights did not change after NPU IPC transfer"
|
||||
30
tests/e2e/pull_request/one_card/test_qwen3_0_6b.py
Normal file
30
tests/e2e/pull_request/one_card/test_qwen3_0_6b.py
Normal file
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
|
||||
from tests.e2e.conftest import wait_until_npu_memory_free
|
||||
from tests.e2e.pull_request.utils import PROMPTS_SHORT, compare_logprobs
|
||||
|
||||
|
||||
@wait_until_npu_memory_free()
|
||||
def test_dense_default_full_and_piecewise_graph():
|
||||
"""Verify dense generation on the default FULL_AND_PIECEWISE graph path."""
|
||||
runner_kwargs = {
|
||||
"model_name": "Qwen/Qwen3-0.6B",
|
||||
"max_model_len": 1024,
|
||||
"cudagraph_capture_sizes": [1, 2, 4, 8],
|
||||
}
|
||||
compare_logprobs(runner_kwargs=runner_kwargs, prompts=PROMPTS_SHORT)
|
||||
71
tests/e2e/pull_request/one_card/test_qwen3_5_0_8b.py
Normal file
71
tests/e2e/pull_request/one_card/test_qwen3_5_0_8b.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
|
||||
import huggingface_hub
|
||||
from huggingface_hub import snapshot_download as hf_snapshot_download
|
||||
from vllm.assets.image import ImageAsset
|
||||
|
||||
from tests.e2e.conftest import VllmRunner, qwen_prompt, wait_until_npu_memory_free
|
||||
|
||||
|
||||
@wait_until_npu_memory_free()
|
||||
def test_mamba_ssm_multimodal_reasoning_mtp_full_decode_only():
|
||||
"""Verify Mamba/SSM multimodal reasoning with MTP and full decode only."""
|
||||
image = ImageAsset("cherry_blossom").pil_image.convert("RGB")
|
||||
img_questions = [
|
||||
"What is the content of this image?",
|
||||
"Describe the content of this image in detail.",
|
||||
"What's in the image?",
|
||||
"Where is this image taken?",
|
||||
]
|
||||
images = [image] * len(img_questions)
|
||||
prompts = qwen_prompt(img_questions)
|
||||
|
||||
model_path = hf_snapshot_download(
|
||||
"Qwen/Qwen3.5-0.8B",
|
||||
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
||||
)
|
||||
with VllmRunner(
|
||||
model_path,
|
||||
dtype="bfloat16",
|
||||
max_model_len=2048,
|
||||
max_num_batched_tokens=1024,
|
||||
limit_mm_per_prompt={"image": 1},
|
||||
mm_processor_kwargs={
|
||||
"min_pixels": 28 * 28,
|
||||
"max_pixels": 1280 * 28 * 28,
|
||||
"fps": 1,
|
||||
},
|
||||
compilation_config={
|
||||
"cudagraph_mode": "FULL_DECODE_ONLY",
|
||||
"cudagraph_capture_sizes": [2, 4, 6, 8],
|
||||
},
|
||||
speculative_config={
|
||||
"method": "mtp",
|
||||
"num_speculative_tokens": 1,
|
||||
},
|
||||
) as runner:
|
||||
outputs = runner.generate_greedy(
|
||||
prompts=prompts,
|
||||
images=images,
|
||||
max_tokens=64,
|
||||
)
|
||||
|
||||
assert len(outputs) == len(prompts)
|
||||
for _, output_str in outputs:
|
||||
assert output_str, "Generated output should not be empty."
|
||||
87
tests/e2e/pull_request/one_card/test_qwen3_8b_w8a8.py
Normal file
87
tests/e2e/pull_request/one_card/test_qwen3_8b_w8a8.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
|
||||
from vllm import SamplingParams
|
||||
from vllm.config import CompilationConfig
|
||||
|
||||
from tests.e2e.conftest import VllmRunner, cleanup_dist_env_and_memory, wait_until_npu_memory_free
|
||||
from tests.e2e.pull_request.utils import PROMPTS_SHORT
|
||||
|
||||
|
||||
@wait_until_npu_memory_free()
|
||||
def test_dense_w8a8_eagle3_full_graph():
|
||||
"""Verify dense W8A8 inference with Eagle-3 speculative decoding."""
|
||||
example_prompts = PROMPTS_SHORT
|
||||
sampling_params = SamplingParams(
|
||||
max_tokens=300,
|
||||
temperature=0.0,
|
||||
ignore_eos=False,
|
||||
)
|
||||
|
||||
with VllmRunner(
|
||||
"vllm-ascend/Qwen3-8B-W8A8",
|
||||
tensor_parallel_size=1,
|
||||
pipeline_parallel_size=1,
|
||||
data_parallel_size=1,
|
||||
disable_log_stats=False,
|
||||
max_model_len=4096,
|
||||
seed=1024,
|
||||
async_scheduling=False,
|
||||
quantization="ascend",
|
||||
speculative_config={
|
||||
"disable_padded_drafter_batch": False,
|
||||
"method": "eagle3",
|
||||
"model": "RedHatAI/Qwen3-8B-speculator.eagle3",
|
||||
"num_speculative_tokens": 2,
|
||||
"draft_tensor_parallel_size": 1,
|
||||
"max_model_len": 128,
|
||||
},
|
||||
compilation_config=CompilationConfig(cudagraph_mode="FULL", cudagraph_capture_sizes=[5, 12]),
|
||||
) as runner:
|
||||
spec_outputs = runner.generate(example_prompts, sampling_params)
|
||||
cleanup_dist_env_and_memory()
|
||||
del runner
|
||||
|
||||
with VllmRunner(
|
||||
"vllm-ascend/Qwen3-8B-W8A8",
|
||||
tensor_parallel_size=1,
|
||||
pipeline_parallel_size=1,
|
||||
data_parallel_size=1,
|
||||
disable_log_stats=False,
|
||||
max_model_len=4096,
|
||||
seed=1024,
|
||||
async_scheduling=False,
|
||||
quantization="ascend",
|
||||
compilation_config=CompilationConfig(cudagraph_mode="FULL_DECODE_ONLY", cudagraph_capture_sizes=[12]),
|
||||
) as runner:
|
||||
ref_outputs = runner.generate(example_prompts, sampling_params)
|
||||
cleanup_dist_env_and_memory()
|
||||
del runner
|
||||
|
||||
matches = 0
|
||||
threshold = 0.66
|
||||
for ref_output, spec_output in zip(ref_outputs, spec_outputs):
|
||||
ref_token_ids = ref_output[0][0]
|
||||
spec_token_ids = spec_output[0][0]
|
||||
if ref_token_ids == spec_token_ids[: len(ref_token_ids)]:
|
||||
matches += 1
|
||||
else:
|
||||
print(f"ref_output: {ref_output[1][0]}")
|
||||
print(f"spec_output: {spec_output[1][0]}")
|
||||
|
||||
assert matches > int(threshold * len(ref_outputs))
|
||||
55
tests/e2e/pull_request/one_card/test_qwen3_embedding_0_6b.py
Normal file
55
tests/e2e/pull_request/one_card/test_qwen3_embedding_0_6b.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
|
||||
import huggingface_hub
|
||||
from modelscope import snapshot_download as modelscope_snapshot_download # type: ignore[import-untyped]
|
||||
|
||||
from tests.e2e.conftest import HfRunner, VllmRunner, cleanup_dist_env_and_memory, wait_until_npu_memory_free
|
||||
from tests.e2e.utils import check_embeddings_close
|
||||
|
||||
|
||||
@wait_until_npu_memory_free()
|
||||
def test_embedding_full_decode_only():
|
||||
"""Verify embedding outputs with full decode only."""
|
||||
queries = ["What is the capital of China?", "Explain gravity"]
|
||||
model = "Qwen/Qwen3-Embedding-0.6B"
|
||||
model_name = modelscope_snapshot_download(
|
||||
model,
|
||||
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
||||
)
|
||||
with VllmRunner(model_name, runner="pooling", max_model_len=None, cudagraph_capture_sizes=[4]) as vllm_runner:
|
||||
vllm_outputs = vllm_runner.embed(queries)
|
||||
cleanup_dist_env_and_memory()
|
||||
del vllm_runner
|
||||
|
||||
with HfRunner(
|
||||
model_name,
|
||||
dtype="float32",
|
||||
is_sentence_transformer=True,
|
||||
) as hf_runner:
|
||||
hf_outputs = hf_runner.encode(queries)
|
||||
cleanup_dist_env_and_memory()
|
||||
del hf_runner
|
||||
|
||||
check_embeddings_close(
|
||||
embeddings_0_lst=hf_outputs,
|
||||
embeddings_1_lst=vllm_outputs,
|
||||
name_0="hf",
|
||||
name_1="vllm",
|
||||
tol=1e-2,
|
||||
)
|
||||
105
tests/e2e/pull_request/one_card/test_sampler.py
Normal file
105
tests/e2e/pull_request/one_card/test_sampler.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/entrypoints/llm/test_guided_generate.py
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import gc
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
from tests.e2e.conftest import ModelName, cleanup_dist_env_and_memory, model_cache
|
||||
|
||||
os.environ["VLLM_BATCH_INVARIANT"] = "1"
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=ModelName.QWEN3_06B,
|
||||
quantization=None,
|
||||
max_model_len=8192,
|
||||
dtype="bfloat16",
|
||||
gpu_memory_utilization=0.9,
|
||||
enable_prefix_caching=False,
|
||||
max_num_seqs=32,
|
||||
tensor_parallel_size=1,
|
||||
distributed_executor_backend="mp",
|
||||
compilation_config={"cudagraph_mode": "FULL_DECODE_ONLY", "cudagraph_capture_sizes": [1, 32, 64]},
|
||||
)
|
||||
def test_qwen3_topk(vllm_runner) -> None:
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
sampling_params = SamplingParams(max_tokens=5, temperature=0.0, top_k=50, top_p=0.9)
|
||||
vllm_runner.generate(example_prompts, sampling_params)
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
@pytest.mark.model(
|
||||
model_name=ModelName.QWEN3_06B,
|
||||
quantization=None,
|
||||
max_model_len=8192,
|
||||
dtype="bfloat16",
|
||||
gpu_memory_utilization=0.9,
|
||||
enable_prefix_caching=False,
|
||||
max_num_seqs=32,
|
||||
tensor_parallel_size=1,
|
||||
distributed_executor_backend="mp",
|
||||
compilation_config={"cudagraph_mode": "FULL_DECODE_ONLY", "cudagraph_capture_sizes": [1, 32, 64]},
|
||||
)
|
||||
def test_qwen3_prompt_logprobs(vllm_runner) -> None:
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
vllm_runner.generate_greedy_logprobs(example_prompts, max_tokens=5, num_logprobs=1)
|
||||
|
||||
|
||||
@pytest.mark.timeout(1000)
|
||||
def test_qwen3_exponential_overlap(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# enable_async_exponential is mutually exclusive with VLLM_BATCH_INVARIANT
|
||||
# (see vllm_ascend/ascend_config.py). The module-level os.environ setting
|
||||
# would silently disable async_exponential, so this test creates its own
|
||||
# LLM instance with batch invariant mode turned off.
|
||||
model_cache.clear()
|
||||
gc.collect()
|
||||
torch.npu.empty_cache()
|
||||
|
||||
monkeypatch.setenv("VLLM_BATCH_INVARIANT", "0")
|
||||
|
||||
llm = LLM(
|
||||
model=ModelName.QWEN3_06B,
|
||||
quantization=None,
|
||||
max_model_len=8192,
|
||||
dtype="bfloat16",
|
||||
gpu_memory_utilization=0.9,
|
||||
enable_prefix_caching=False,
|
||||
max_num_seqs=32,
|
||||
tensor_parallel_size=1,
|
||||
distributed_executor_backend="mp",
|
||||
compilation_config={"cudagraph_mode": "FULL_DECODE_ONLY", "cudagraph_capture_sizes": [1, 2, 4, 8]},
|
||||
additional_config={"enable_async_exponential": True},
|
||||
)
|
||||
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
sampling_params = SamplingParams(max_tokens=5, temperature=1.0, top_k=50, top_p=0.9)
|
||||
llm.generate(example_prompts, sampling_params)
|
||||
|
||||
del llm
|
||||
cleanup_dist_env_and_memory()
|
||||
103
tests/e2e/pull_request/one_card/test_simple_cpu_offload.py
Normal file
103
tests/e2e/pull_request/one_card/test_simple_cpu_offload.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
"""End-to-end tests for the Ascend ``SimpleCPUOffloadConnector``.
|
||||
|
||||
The simple CPU offloading scheduler/worker pair is reused from upstream
|
||||
vLLM; here we only exercise the NPU-native worker path
|
||||
(``aclrtMemcpyBatchAsync`` + ``torch.npu`` streams) to confirm that
|
||||
KV blocks are stored to and reloaded from CPU correctly on Ascend.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams, TokensPrompt
|
||||
from vllm.config import KVTransferConfig
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
|
||||
def _build_kv_transfer_config(
|
||||
cpu_bytes_to_use: int,
|
||||
lazy_offload: bool = False,
|
||||
) -> KVTransferConfig:
|
||||
return KVTransferConfig(
|
||||
kv_connector="SimpleCPUOffloadConnector",
|
||||
kv_role="kv_both",
|
||||
kv_connector_extra_config={
|
||||
"cpu_bytes_to_use": cpu_bytes_to_use,
|
||||
"lazy_offload": lazy_offload,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_simple_cpu_offload_accuracy() -> None:
|
||||
"""Reset GPU prefix cache after a cold run; verify the CPU-loaded KV
|
||||
cache reproduces the cold-run output deterministically."""
|
||||
sampling_params = SamplingParams(max_tokens=1, temperature=0)
|
||||
|
||||
# Long enough prompt to occupy multiple full KV blocks.
|
||||
prompt = "hi " * 500 + "Let's count to ten. One, two, three, "
|
||||
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-0.6B",
|
||||
max_model_len=4096,
|
||||
gpu_memory_utilization=0.5,
|
||||
enable_prefix_caching=True,
|
||||
kv_transfer_config=_build_kv_transfer_config(1 << 30), # 1 GiB
|
||||
enforce_eager=True,
|
||||
) as runner:
|
||||
llm = runner.model
|
||||
|
||||
# Cold run — populates GPU cache and triggers CPU offload.
|
||||
cold_output = llm.generate(prompt, sampling_params, use_tqdm=False)[0]
|
||||
expected = cold_output.outputs[0].text
|
||||
|
||||
success = 0
|
||||
attempts = 5
|
||||
for _ in range(attempts):
|
||||
# Let the engine core drain pending store transfers.
|
||||
time.sleep(2)
|
||||
# Reset GPU prefix cache so the next run must reload from CPU.
|
||||
if not llm.reset_prefix_cache():
|
||||
continue
|
||||
output = llm.generate(prompt, sampling_params, use_tqdm=False)[0]
|
||||
if output.outputs[0].text == expected:
|
||||
success += 1
|
||||
|
||||
assert success >= int(0.5 * attempts), (
|
||||
f"CPU-load accuracy too low: {success}/{attempts} matched baseline output {expected!r}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lazy", [False, True])
|
||||
def test_simple_cpu_offload_no_crash_on_repeat(lazy: bool) -> None:
|
||||
"""Smoke test: many short generations exercise both eager and lazy
|
||||
offload paths without errors and yield non-empty outputs."""
|
||||
sampling_params = SamplingParams(max_tokens=4, temperature=0)
|
||||
prompt_token_ids = [0] * 257
|
||||
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-0.6B",
|
||||
max_model_len=2048,
|
||||
gpu_memory_utilization=0.5,
|
||||
enable_prefix_caching=True,
|
||||
kv_transfer_config=_build_kv_transfer_config(
|
||||
cpu_bytes_to_use=512 * (1 << 20), # 512 MiB
|
||||
lazy_offload=lazy,
|
||||
),
|
||||
enforce_eager=True,
|
||||
) as runner:
|
||||
llm = runner.model
|
||||
for i in range(8):
|
||||
prompt_token_ids[0] = i
|
||||
prompts = [TokensPrompt(prompt_token_ids=prompt_token_ids)]
|
||||
outs = llm.generate(prompts, sampling_params, use_tqdm=False)
|
||||
assert outs and len(outs[0].outputs[0].token_ids) > 0
|
||||
137
tests/e2e/pull_request/one_card/test_vlm.py
Normal file
137
tests/e2e/pull_request/one_card/test_vlm.py
Normal file
@@ -0,0 +1,137 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
"""Compare the short outputs of HF and vLLM when using greedy sampling.
|
||||
|
||||
Run `pytest tests/e2e/pull_request/one_card/test_vlm.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams
|
||||
from vllm.assets.audio import AudioAsset
|
||||
from vllm.assets.image import ImageAsset
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
WHISPER_MODELS = [
|
||||
"openai-mirror/whisper-large-v3-turbo",
|
||||
]
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"VLLM_WORKER_MULTIPROC_METHOD": "spawn"})
|
||||
def test_multimodal_vl(vl_config):
|
||||
image = ImageAsset("cherry_blossom").pil_image.convert("RGB")
|
||||
|
||||
img_questions = [
|
||||
"What is the content of this image?",
|
||||
"Describe the content of this image in detail.",
|
||||
"What's in the image?",
|
||||
"Where is this image taken?",
|
||||
]
|
||||
|
||||
images = [image] * len(img_questions)
|
||||
prompts = vl_config["prompt_fn"](img_questions)
|
||||
|
||||
with VllmRunner(
|
||||
vl_config["model"],
|
||||
mm_processor_kwargs=vl_config["mm_processor_kwargs"],
|
||||
max_model_len=8192,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
limit_mm_per_prompt={"image": 1},
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.generate_greedy(
|
||||
prompts=prompts,
|
||||
images=images,
|
||||
max_tokens=64,
|
||||
)
|
||||
|
||||
assert len(outputs) == len(prompts)
|
||||
|
||||
for _, output_str in outputs:
|
||||
assert output_str, "Generated output should not be empty."
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"VLLM_WORKER_MULTIPROC_METHOD": "spawn"})
|
||||
def test_multimodal_vl_language_model_only():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-VL-8B-Instruct",
|
||||
max_model_len=4096,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
gpu_memory_utilization=0.90,
|
||||
language_model_only=True,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"VLLM_WORKER_MULTIPROC_METHOD": "spawn"})
|
||||
def test_multimodal_audio():
|
||||
audio_prompt = "".join([f"Audio {idx + 1}: <|audio_bos|><|AUDIO|><|audio_eos|>\n" for idx in range(2)])
|
||||
question = "What sport and what nursery rhyme are referenced?"
|
||||
prompt = (
|
||||
"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n"
|
||||
"<|im_start|>user\n"
|
||||
f"{audio_prompt}{question}<|im_end|>\n"
|
||||
"<|im_start|>assistant\n"
|
||||
)
|
||||
mm_data = {
|
||||
"audio": [asset.audio_and_sample_rate for asset in [AudioAsset("mary_had_lamb"), AudioAsset("winning_call")]]
|
||||
}
|
||||
inputs = {"prompt": prompt, "multi_modal_data": mm_data}
|
||||
|
||||
sampling_params = SamplingParams(temperature=0.2, max_tokens=10, stop_token_ids=None)
|
||||
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen2-Audio-7B-Instruct",
|
||||
max_model_len=4096,
|
||||
max_num_seqs=5,
|
||||
dtype="bfloat16",
|
||||
limit_mm_per_prompt={"audio": 2},
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
gpu_memory_utilization=0.9,
|
||||
) as runner:
|
||||
outputs = runner.generate(inputs, sampling_params=sampling_params)
|
||||
|
||||
assert outputs is not None, "Generated outputs should not be None."
|
||||
assert len(outputs) > 0, "Generated outputs should not be empty."
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", WHISPER_MODELS)
|
||||
@patch.dict(os.environ, {"VLLM_WORKER_MULTIPROC_METHOD": "spawn"})
|
||||
def test_whisper(model) -> None:
|
||||
prompts = ["<|startoftranscript|><|en|><|transcribe|><|notimestamps|>"]
|
||||
audios = [AudioAsset("mary_had_lamb").audio_and_sample_rate]
|
||||
|
||||
sampling_params = SamplingParams(temperature=0.2, max_tokens=10, stop_token_ids=None)
|
||||
|
||||
with VllmRunner(
|
||||
model, max_model_len=448, max_num_seqs=5, dtype="bfloat16", block_size=128, gpu_memory_utilization=0.9
|
||||
) as runner:
|
||||
outputs = runner.generate(prompts=prompts, audios=audios, sampling_params=sampling_params)
|
||||
|
||||
assert outputs is not None, "Generated outputs should not be None."
|
||||
assert len(outputs) > 0, "Generated outputs should not be empty."
|
||||
65
tests/e2e/pull_request/one_card/test_xlite.py
Normal file
65
tests/e2e/pull_request/one_card/test_xlite.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
"""
|
||||
Compare the outputs of vLLM with and without xlite via logprob-based accuracy
|
||||
check (3 tokens: 1 prefill + 2 decode).
|
||||
|
||||
Run `pytest tests/e2e/pull_request/one_card/test_xlite.py`.
|
||||
"""
|
||||
|
||||
# ruff: noqa: E501
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.pull_request.utils import PROMPTS_SHORT, LLMTestCase, compare_logprobs
|
||||
|
||||
os.environ["VLLM_ASCEND_ENABLE_NZ"] = "2"
|
||||
|
||||
CASE_DECODE_ONLY = LLMTestCase(
|
||||
model="Qwen/Qwen3-0.6B",
|
||||
prompts=PROMPTS_SHORT,
|
||||
)
|
||||
|
||||
CASE_FULL = LLMTestCase(
|
||||
model="Qwen/Qwen3-0.6B",
|
||||
prompts=PROMPTS_SHORT,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="TODO: Re-enable xlite_decode_only e2e test when stable.")
|
||||
@pytest.mark.parametrize("cur_case", [CASE_DECODE_ONLY])
|
||||
def test_models_with_xlite_decode_only(cur_case: LLMTestCase):
|
||||
runner_kwargs = {
|
||||
"model_name": cur_case.model,
|
||||
"max_model_len": 1024,
|
||||
"block_size": 128,
|
||||
"additional_config": {"xlite_graph_config": {"enabled": True}},
|
||||
}
|
||||
compare_logprobs(runner_kwargs=runner_kwargs, prompts=cur_case.prompts)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cur_case", [CASE_FULL])
|
||||
def test_models_with_xlite_full_mode(cur_case: LLMTestCase):
|
||||
runner_kwargs = {
|
||||
"model_name": cur_case.model,
|
||||
"max_model_len": 1024,
|
||||
"block_size": 128,
|
||||
"additional_config": {"xlite_graph_config": {"enabled": True, "full_mode": True}},
|
||||
}
|
||||
compare_logprobs(runner_kwargs=runner_kwargs, prompts=cur_case.prompts)
|
||||
0
tests/e2e/pull_request/two_card/__init__.py
Normal file
0
tests/e2e/pull_request/two_card/__init__.py
Normal file
@@ -0,0 +1,232 @@
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import contextlib
|
||||
import gc
|
||||
import math
|
||||
import multiprocessing
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
from tests.e2e.conftest import wait_until_npu_memory_free
|
||||
from vllm_ascend.utils import AscendDeviceType, get_ascend_device_type
|
||||
|
||||
MODELS = [
|
||||
# Offline data parallel mode will be not supported/useful for dense models
|
||||
# "Qwen/Qwen3-0.6B",
|
||||
"vllm-ascend/DeepSeek-V2-Lite-W8A8",
|
||||
]
|
||||
|
||||
|
||||
def _install_spies(counters: dict[str, Any]) -> contextlib.ExitStack:
|
||||
"""Installs thread-safe spies on NPU methods to track invocation counts."""
|
||||
from vllm_ascend.worker.model_runner_v1 import NPUModelRunner
|
||||
|
||||
def make_spy(cls, method_name, counter):
|
||||
original = getattr(cls, method_name)
|
||||
|
||||
def spy(self, *args, **kwargs):
|
||||
with counter.get_lock():
|
||||
counter.value += 1
|
||||
return original(self, *args, **kwargs)
|
||||
|
||||
return spy
|
||||
|
||||
stack = contextlib.ExitStack()
|
||||
hooks = [
|
||||
(torch.npu.NPUGraph, "replay", counters["replay"]),
|
||||
(torch.npu.NPUGraph, "__init__", counters["capture"]),
|
||||
(NPUModelRunner, "execute_model", counters["exec_model"]),
|
||||
(NPUModelRunner, "_dummy_run", counters["dummy_run"]),
|
||||
]
|
||||
|
||||
for cls, method, counter in hooks:
|
||||
stack.enter_context(patch.object(cls, method, make_spy(cls, method, counter)))
|
||||
|
||||
return stack
|
||||
|
||||
|
||||
def _run_worker_process(
|
||||
rank: int,
|
||||
local_rank: int,
|
||||
world_size: int,
|
||||
master_ip: str,
|
||||
master_port: int,
|
||||
counters: dict[str, Any],
|
||||
model_path: str,
|
||||
max_tokens: int,
|
||||
):
|
||||
"""Main entry point for the worker process."""
|
||||
os.environ.update(
|
||||
{
|
||||
"VLLM_DP_RANK": str(rank),
|
||||
"VLLM_DP_RANK_LOCAL": str(local_rank),
|
||||
"VLLM_DP_SIZE": str(world_size),
|
||||
"VLLM_DP_MASTER_IP": master_ip,
|
||||
"VLLM_DP_MASTER_PORT": str(master_port),
|
||||
}
|
||||
)
|
||||
|
||||
# Import vLLM only after environment setup
|
||||
from vllm import LLM, SamplingParams
|
||||
from vllm.distributed.parallel_state import destroy_distributed_environment, destroy_model_parallel
|
||||
|
||||
# Apply hooks and run inference
|
||||
with _install_spies(counters):
|
||||
prompts = [
|
||||
"Hello, my name is",
|
||||
"The president of the United States is",
|
||||
"The capital of France is",
|
||||
"The future of AI is",
|
||||
]
|
||||
|
||||
# Simple data sharding
|
||||
chunk_size = len(prompts) // world_size
|
||||
start_idx = rank * chunk_size
|
||||
end_idx = start_idx + chunk_size if rank < world_size - 1 else len(prompts)
|
||||
local_prompts = prompts[start_idx:end_idx]
|
||||
|
||||
llm = LLM(
|
||||
model=model_path,
|
||||
quantization="ascend" if "W8A8" in model_path else None,
|
||||
enable_expert_parallel="DeepSeek" in model_path,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
|
||||
# Expose model config to the main test process
|
||||
counters["hidden_layers"].value = llm.llm_engine.model_config.hf_text_config.num_hidden_layers
|
||||
|
||||
llm.generate(local_prompts, SamplingParams(max_tokens=max_tokens, temperature=0.0))
|
||||
|
||||
# Explicit cleanup is mandatory in multi-process vLLM tests
|
||||
del llm
|
||||
|
||||
destroy_model_parallel()
|
||||
destroy_distributed_environment()
|
||||
|
||||
with contextlib.suppress(AssertionError):
|
||||
torch.distributed.destroy_process_group()
|
||||
|
||||
gc.collect()
|
||||
torch.npu.empty_cache()
|
||||
torch.npu.reset_peak_memory_stats()
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="fix me")
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [4, 36])
|
||||
@patch.dict(os.environ, {"ASCEND_RT_VISIBLE_DEVICES": "0,1"})
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.6)
|
||||
def test_models_aclgraph_capture_replay_metrics_dp2(
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Counter doesn't work in default "spawn" mode
|
||||
monkeypatch.delenv("VLLM_WORKER_MULTIPROC_METHOD", raising=False)
|
||||
|
||||
# Shared counters for cross-process assertion
|
||||
counters = {
|
||||
"replay": multiprocessing.Value("i", 0),
|
||||
"capture": multiprocessing.Value("i", 0),
|
||||
"exec_model": multiprocessing.Value("i", 0),
|
||||
"dummy_run": multiprocessing.Value("i", 0),
|
||||
"hidden_layers": multiprocessing.Value("i", -1),
|
||||
}
|
||||
|
||||
dp_size = 2
|
||||
port = get_open_port()
|
||||
|
||||
# Launch workers
|
||||
workers = []
|
||||
for rank in range(dp_size):
|
||||
p = multiprocessing.Process(
|
||||
target=_run_worker_process,
|
||||
args=(rank, rank, dp_size, "127.0.0.1", port, counters, model, max_tokens),
|
||||
)
|
||||
p.start()
|
||||
workers.append(p)
|
||||
|
||||
# Supervision loop
|
||||
for p in workers:
|
||||
p.join(timeout=900)
|
||||
if p.exitcode != 0:
|
||||
for k in workers:
|
||||
if k.is_alive():
|
||||
k.kill()
|
||||
raise RuntimeError(f"Worker {p.pid} failed with exit code {p.exitcode}")
|
||||
|
||||
actual_capture = counters["capture"].value
|
||||
actual_replay = counters["replay"].value
|
||||
num_execute_model = counters["exec_model"].value
|
||||
num_dummy_run = counters["dummy_run"].value
|
||||
num_layers = counters["hidden_layers"].value
|
||||
|
||||
num_acl_graphs = num_layers + 1
|
||||
num_comm_groups = sum(1 for s in [dp_size, 1] if s > 1) # dp_size=2, tp_size=1
|
||||
|
||||
# Metric 1: Graph Capture (ACL Graph Construction)
|
||||
# Ref: vllm_ascend.utils.update_aclgraph_sizes
|
||||
max_batch_sizes = math.floor((1800 - num_comm_groups * 40) / num_acl_graphs / (1 + num_comm_groups * 2))
|
||||
|
||||
expected_capture = max_batch_sizes * num_acl_graphs * dp_size
|
||||
assert actual_capture == expected_capture, (
|
||||
f"Capture count mismatch. Expected: {expected_capture}, Got: {actual_capture}"
|
||||
)
|
||||
|
||||
# Metric 2: Model Execution (NPUModelRunner.execute_model)
|
||||
# vLLM Step Breakdown:
|
||||
# 1. First step (prefill, 1 prompt)
|
||||
# 2. Generation steps (max_tokens)
|
||||
# 3. Final step (likely EOS/idle step), no replay here
|
||||
total_steps = max_tokens + 1 # this includes the 1 and 2 above
|
||||
# vllm default enables Async scheduler, this will take 1 more steps
|
||||
expected_exec_model = (total_steps + 1 + 1) * dp_size
|
||||
|
||||
assert num_execute_model == expected_exec_model, (
|
||||
f"Model execution count mismatch. Expected: {expected_exec_model}, Got: {num_execute_model}"
|
||||
)
|
||||
|
||||
# Metric 3: Dummy Runs (Warmup & Alignment)
|
||||
# vLLM synchronizes globally every 32 steps.
|
||||
# Ref: vllm.v1.engine.core.DPEngineCoreProc._has_global_unfinished_reqs
|
||||
aligned_steps = (total_steps + 31) // 32 * 32
|
||||
|
||||
# Part A: Warmup runs (Profile run + 2 runs per captured graph)
|
||||
warmup_runs = 1 + (2 * max_batch_sizes)
|
||||
soc_version = get_ascend_device_type()
|
||||
if soc_version in {AscendDeviceType.A3} and "DeepSeek" in model:
|
||||
# An extra warmup run is needed for MC2 warmup here
|
||||
warmup_runs += 1
|
||||
|
||||
# Part B: Alignment padding (Empty runs to hit the 32-step boundary)
|
||||
padding_runs = aligned_steps - total_steps
|
||||
|
||||
expected_dummy_run = (warmup_runs + padding_runs) * dp_size
|
||||
|
||||
assert num_dummy_run == expected_dummy_run, (
|
||||
f"Dummy run count mismatch. Expected: {expected_dummy_run}, Got: {num_dummy_run}"
|
||||
)
|
||||
|
||||
# Metric 4: Graph Replay (Inference Execution)
|
||||
# Replays happen for every aligned step across all graphs.
|
||||
expected_replay = num_acl_graphs * aligned_steps * dp_size
|
||||
|
||||
assert actual_replay == expected_replay, f"Replay count mismatch. Expected: {expected_replay}, Got: {actual_replay}"
|
||||
0
tests/e2e/pull_request/two_card/lora/__init__.py
Normal file
0
tests/e2e/pull_request/two_card/lora/__init__.py
Normal file
24
tests/e2e/pull_request/two_card/lora/test_ilama_lora_tp2.py
Normal file
24
tests/e2e/pull_request/two_card/lora/test_ilama_lora_tp2.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from tests.e2e.pull_request.one_card.lora.test_ilama_lora import EXPECTED_LORA_OUTPUT, MODEL_PATH, do_sample
|
||||
|
||||
|
||||
@pytest.mark.parametrize("distributed_executor_backend", ["mp"])
|
||||
def test_ilama_lora_tp2(distributed_executor_backend, ilama_lora_files):
|
||||
with VllmRunner(
|
||||
MODEL_PATH,
|
||||
enable_lora=True,
|
||||
max_loras=4,
|
||||
dtype="half",
|
||||
max_model_len=1024,
|
||||
max_num_seqs=16,
|
||||
tensor_parallel_size=2,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
distributed_executor_backend=distributed_executor_backend,
|
||||
enforce_eager=True,
|
||||
) as vllm_model:
|
||||
output = do_sample(vllm_model.model, ilama_lora_files, lora_id=2)
|
||||
|
||||
for i in range(len(EXPECTED_LORA_OUTPUT)):
|
||||
assert output[i] == EXPECTED_LORA_OUTPUT[i]
|
||||
30
tests/e2e/pull_request/two_card/lora/test_llama32_lora_tp2.py
Executable file
30
tests/e2e/pull_request/two_card/lora/test_llama32_lora_tp2.py
Executable file
@@ -0,0 +1,30 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner, wait_until_npu_memory_free
|
||||
from tests.e2e.pull_request.one_card.lora.test_llama32_lora import generate_and_test
|
||||
from vllm_ascend.utils import enable_custom_op
|
||||
|
||||
enable_custom_op()
|
||||
|
||||
# For hk region, we need to use the model from hf to avoid the network issue
|
||||
MODEL_PATH = "vllm-ascend/Llama-3.2-3B-Instruct"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fully_sharded_loras", [False, True])
|
||||
@wait_until_npu_memory_free()
|
||||
def test_llama_lora_tp2(llama32_lora_files, fully_sharded_loras):
|
||||
with VllmRunner(
|
||||
MODEL_PATH,
|
||||
enable_lora=True,
|
||||
# also test odd max_num_seqs
|
||||
max_num_seqs=7,
|
||||
max_model_len=1024,
|
||||
max_loras=4,
|
||||
tensor_parallel_size=2,
|
||||
fully_sharded_loras=fully_sharded_loras,
|
||||
compilation_config={"cudagraph_mode": "PIECEWISE"},
|
||||
) as vllm_model:
|
||||
llm = vllm_model.model
|
||||
generate_and_test(llm, llama32_lora_files)
|
||||
@@ -0,0 +1,68 @@
|
||||
import vllm
|
||||
from vllm.lora.request import LoRARequest
|
||||
|
||||
MODEL_PATH = "Qwen/Qwen3-30B-A3B"
|
||||
|
||||
PROMPT_TEMPLATE = """<|im_start|>user
|
||||
I want you to act as a SQL terminal in front of an example database, you need only to return the sql command to me.Below is an instruction that describes a task, Write a response that appropriately completes the request.
|
||||
"
|
||||
##Instruction:
|
||||
candidate_poll contains tables such as candidate, people. Table candidate has columns such as Candidate_ID, People_ID, Poll_Source, Date, Support_rate, Consider_rate, Oppose_rate, Unsure_rate. Candidate_ID is the primary key.
|
||||
Table people has columns such as People_ID, Sex, Name, Date_of_Birth, Height, Weight. People_ID is the primary key.
|
||||
The People_ID of candidate is the foreign key of People_ID of people.
|
||||
|
||||
|
||||
###Input:
|
||||
{context}
|
||||
|
||||
###Response:<|im_end|>
|
||||
<|im_start|>assistant""" # noqa: E501
|
||||
|
||||
EXPECTED_LORA_OUTPUT = [
|
||||
"<think>\n\n</think>\n\nSELECT count(*) FROM candidate",
|
||||
"<think>\n\n</think>\n\nSELECT count(*) FROM candidate",
|
||||
"<think>\n\n</think>\n\nSELECT poll_source FROM candidate GROUP BY poll_source ORDER BY count(*) DESC LIMIT 1", # noqa: E501
|
||||
"<think>\n\n</think>\n\nSELECT poll_source FROM candidate GROUP BY poll_source ORDER BY count(*) DESC LIMIT 1", # noqa: E501
|
||||
]
|
||||
|
||||
|
||||
def generate_and_test(llm: vllm.LLM, lora_path: str, lora_id: int) -> None:
|
||||
prompts = [
|
||||
PROMPT_TEMPLATE.format(context="How many candidates are there?"),
|
||||
PROMPT_TEMPLATE.format(context="Count the number of candidates."),
|
||||
PROMPT_TEMPLATE.format(
|
||||
context="Which poll resource provided the most number of candidate information?" # noqa: E501
|
||||
),
|
||||
PROMPT_TEMPLATE.format(context="Return the poll resource associated with the most candidates."),
|
||||
]
|
||||
sampling_params = vllm.SamplingParams(temperature=0, max_tokens=64)
|
||||
outputs = llm.generate(
|
||||
prompts,
|
||||
sampling_params,
|
||||
lora_request=LoRARequest(str(lora_id), lora_id, lora_path) if lora_id else None,
|
||||
)
|
||||
# Print the outputs.
|
||||
generated_texts: list[str] = []
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text.strip()
|
||||
generated_texts.append(generated_text)
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
|
||||
for i in range(len(EXPECTED_LORA_OUTPUT)):
|
||||
assert generated_texts[i].startswith(EXPECTED_LORA_OUTPUT[i])
|
||||
|
||||
|
||||
def test_qwen3moe_lora(qwen3moe_lora_files):
|
||||
llm = vllm.LLM(
|
||||
MODEL_PATH,
|
||||
max_model_len=1024,
|
||||
enable_lora=True,
|
||||
max_loras=4,
|
||||
enforce_eager=True,
|
||||
trust_remote_code=True,
|
||||
enable_chunked_prefill=True,
|
||||
tensor_parallel_size=2,
|
||||
)
|
||||
|
||||
generate_and_test(llm, qwen3moe_lora_files, lora_id=1)
|
||||
429
tests/e2e/pull_request/two_card/spec_decode/test_spec_decode.py
Normal file
429
tests/e2e/pull_request/two_card/spec_decode/test_spec_decode.py
Normal file
@@ -0,0 +1,429 @@
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
# Run `pytest tests/e2e/pull_request/two_card/spec_decode/test_spec_decode.py`.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from transformers import AutoTokenizer
|
||||
from vllm import SamplingParams
|
||||
from vllm.config import CompilationConfig
|
||||
from vllm.tokenizers.registry import resolve_tokenizer_args
|
||||
from vllm.v1.metrics.reader import Counter, Vector
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
MODELS = {
|
||||
"eagle3": {
|
||||
"main": "Qwen/Qwen3-8B",
|
||||
"spec": "RedHatAI/Qwen3-8B-speculator.eagle3",
|
||||
},
|
||||
}
|
||||
|
||||
P_EAGLE_MODELS = {
|
||||
"p-eagle": {
|
||||
"main": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
|
||||
"spec": "amazon/Qwen3-Coder-30B-A3B-Instruct-P-EAGLE",
|
||||
},
|
||||
}
|
||||
|
||||
VWN_EAGLE3_MODELS = {
|
||||
"vwn_eagle3": {
|
||||
"main": "Qwen/Qwen3-30B-A3B",
|
||||
"spec": "vllm-ascend/Qwen3-30B-A3B-vwn-eagle-model",
|
||||
},
|
||||
}
|
||||
|
||||
# NOTE: golden may change (eagle_proposer only runs in eager mode currently),
|
||||
# thus please update it if ci fails but you have better acceptance
|
||||
BASELINES_SP = {
|
||||
"eagle3": [0.68, 0.40, 0.18],
|
||||
"p-eagle": [0.5625, 0.25, 0.0625, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
"vwn_eagle3": [0.75, 0.5, 0.3],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="skip test_eagle3_sp_acceptance")
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1"})
|
||||
@pytest.mark.parametrize("method", ["eagle3"])
|
||||
@pytest.mark.parametrize("num_speculative_tokens", [3])
|
||||
@pytest.mark.parametrize("disable_padded_drafter_batch", [True, False])
|
||||
@pytest.mark.parametrize("async_scheduling", [True, False])
|
||||
def test_eagle3_sp_acceptance(
|
||||
method: str,
|
||||
num_speculative_tokens: int,
|
||||
disable_padded_drafter_batch: bool,
|
||||
async_scheduling: bool,
|
||||
):
|
||||
if disable_padded_drafter_batch and async_scheduling:
|
||||
pytest.skip(
|
||||
"skip disable_padded_drafter_batch=True and async_scheduling=True",
|
||||
)
|
||||
|
||||
main_model_name = MODELS[method]["main"]
|
||||
spec_model_name = MODELS[method]["spec"]
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
main_model_name,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0,
|
||||
ignore_eos=False,
|
||||
max_tokens=256,
|
||||
)
|
||||
|
||||
# sp will only be enabled when query_lens > 1000
|
||||
prompts = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": " " * 1000 + "Hello, my name is",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": " " * 1000 + "The president of the United States is",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": " " * 1000 + "The capital of France is",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": " " * 1000 + "The future of AI is",
|
||||
},
|
||||
]
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[prompt],
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
||||
speculative_config = {
|
||||
"enforce_eager": True,
|
||||
"method": method,
|
||||
"num_speculative_tokens": num_speculative_tokens,
|
||||
"disable_padded_drafter_batch": disable_padded_drafter_batch,
|
||||
"model": spec_model_name,
|
||||
}
|
||||
|
||||
compilation_config = CompilationConfig(cudagraph_mode="FULL_DECODE_ONLY", cudagraph_capture_sizes=[12])
|
||||
|
||||
with VllmRunner(
|
||||
main_model_name,
|
||||
enforce_eager=True,
|
||||
max_model_len=8192,
|
||||
disable_log_stats=False,
|
||||
tensor_parallel_size=2,
|
||||
max_num_seqs=256,
|
||||
distributed_executor_backend="mp",
|
||||
gpu_memory_utilization=0.7,
|
||||
speculative_config=speculative_config,
|
||||
compilation_config=compilation_config,
|
||||
async_scheduling=async_scheduling,
|
||||
) as llm:
|
||||
_ = llm.generate(prompts, sampling_params)
|
||||
metrics = llm.model.get_metrics()
|
||||
|
||||
num_drafts = 0
|
||||
num_accepted_tokens_per_pos = [0] * num_speculative_tokens
|
||||
for metric in metrics:
|
||||
if metric.name == "vllm:spec_decode_num_drafts":
|
||||
assert isinstance(metric, Counter)
|
||||
num_drafts += metric.value
|
||||
elif metric.name == "vllm:spec_decode_num_accepted_tokens_per_pos":
|
||||
assert isinstance(metric, Vector)
|
||||
for pos in range(len(metric.values)):
|
||||
num_accepted_tokens_per_pos[pos] += metric.values[pos]
|
||||
|
||||
acceptance_per_pos = [num_accepted_tokens / num_drafts for num_accepted_tokens in num_accepted_tokens_per_pos]
|
||||
golden = BASELINES_SP[method]
|
||||
|
||||
match = all(abs(a - b) < 0.06 for a, b in zip(acceptance_per_pos, golden))
|
||||
if not match:
|
||||
print(f"acceptance_per_pos: {acceptance_per_pos}")
|
||||
print(f"golden: {golden}")
|
||||
|
||||
assert match
|
||||
|
||||
|
||||
def test_qwen3_eagle3_pcp2_tp1():
|
||||
"""
|
||||
Test Qwen3-8B with Eagle3 speculative decoding under PCP + TP1 configuration.
|
||||
This test verifies that eagle3 spec decode works correctly with:
|
||||
- PCP enabled (prefill_context_parallel_size=2)
|
||||
- Tensor Parallel size = 1
|
||||
- num_speculative_tokens = 3
|
||||
- enforce_eager = True
|
||||
"""
|
||||
method = "eagle3"
|
||||
num_speculative_tokens = 3
|
||||
|
||||
main_model_name = MODELS[method]["main"]
|
||||
spec_model_name = MODELS[method]["spec"]
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
main_model_name,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0,
|
||||
ignore_eos=False,
|
||||
max_tokens=256,
|
||||
)
|
||||
|
||||
prompts = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, my name is",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "The president of the United States is",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "The capital of France is",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "The future of AI is",
|
||||
},
|
||||
]
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[prompt],
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
||||
speculative_config = {
|
||||
"method": method,
|
||||
"num_speculative_tokens": num_speculative_tokens,
|
||||
"model": spec_model_name,
|
||||
}
|
||||
|
||||
with VllmRunner(
|
||||
main_model_name,
|
||||
enforce_eager=True,
|
||||
max_model_len=2048,
|
||||
disable_log_stats=False,
|
||||
tensor_parallel_size=1,
|
||||
prefill_context_parallel_size=2,
|
||||
max_num_seqs=256,
|
||||
distributed_executor_backend="mp",
|
||||
gpu_memory_utilization=0.7,
|
||||
speculative_config=speculative_config,
|
||||
) as llm:
|
||||
llm.generate(prompts, sampling_params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", P_EAGLE_MODELS.keys())
|
||||
@pytest.mark.parametrize("num_speculative_tokens", [8])
|
||||
@pytest.mark.parametrize("draft_tensor_parallel_size", [None, 2])
|
||||
def test_p_eagle_acceptance(
|
||||
method: str,
|
||||
num_speculative_tokens: int,
|
||||
draft_tensor_parallel_size: None | int,
|
||||
):
|
||||
"""
|
||||
Test acceptance rate for parallel drafting speculative decoding
|
||||
using a smaller draft model with parallel_drafting enabled.
|
||||
"""
|
||||
main_model_name = P_EAGLE_MODELS[method]["main"]
|
||||
spec_model_name = P_EAGLE_MODELS[method]["spec"]
|
||||
|
||||
tokenizer_path = resolve_tokenizer_args(main_model_name)[1]
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
tokenizer_path,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0,
|
||||
ignore_eos=False,
|
||||
max_tokens=256,
|
||||
)
|
||||
|
||||
prompts = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, your name is",
|
||||
},
|
||||
]
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[prompt],
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
||||
speculative_config = {
|
||||
"method": "eagle3",
|
||||
"model": spec_model_name,
|
||||
"num_speculative_tokens": num_speculative_tokens,
|
||||
"draft_tensor_parallel_size": draft_tensor_parallel_size,
|
||||
"parallel_drafting": True,
|
||||
}
|
||||
|
||||
compilation_config = CompilationConfig(cudagraph_capture_sizes=[12])
|
||||
|
||||
with VllmRunner(
|
||||
main_model_name,
|
||||
max_model_len=4096,
|
||||
disable_log_stats=False,
|
||||
tensor_parallel_size=2,
|
||||
max_num_seqs=256,
|
||||
distributed_executor_backend="mp",
|
||||
gpu_memory_utilization=0.8,
|
||||
speculative_config=speculative_config,
|
||||
compilation_config=compilation_config,
|
||||
enable_prefix_caching=False,
|
||||
) as llm:
|
||||
outputs = llm.model.generate(prompts, sampling_params)
|
||||
metrics = llm.model.get_metrics()
|
||||
|
||||
for output in outputs:
|
||||
prompt = output.prompt
|
||||
generated_text = output.outputs[0].text
|
||||
output_tokens = output.outputs[0].token_ids
|
||||
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
|
||||
print(f"Output tokens: {output_tokens}")
|
||||
|
||||
num_drafts = 0
|
||||
num_accepted_tokens_per_pos = [0] * num_speculative_tokens
|
||||
for metric in metrics:
|
||||
if metric.name == "vllm:spec_decode_num_drafts":
|
||||
assert isinstance(metric, Counter)
|
||||
num_drafts += metric.value
|
||||
elif metric.name == "vllm:spec_decode_num_accepted_tokens_per_pos":
|
||||
assert isinstance(metric, Vector)
|
||||
for pos in range(len(metric.values)):
|
||||
num_accepted_tokens_per_pos[pos] += metric.values[pos]
|
||||
|
||||
acceptance_per_pos = [num_accepted_tokens / num_drafts for num_accepted_tokens in num_accepted_tokens_per_pos]
|
||||
|
||||
golden = BASELINES_SP[method]
|
||||
|
||||
match = all(abs(a - b) < 0.1 for a, b in zip(acceptance_per_pos, golden))
|
||||
if not match:
|
||||
print(f"acceptance_per_pos: {acceptance_per_pos}")
|
||||
print(f"golden: {golden}")
|
||||
|
||||
assert match
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1"})
|
||||
def test_qwen3_vwn_eagle3_tp2():
|
||||
"""
|
||||
Test Qwen3-30B-A3B with VWN-Eagle3 speculative decoding acceptance rate.
|
||||
This test verifies that VWN-Eagle3 spec decode works correctly with:
|
||||
- Tensor Parallel size = 4
|
||||
- Expert Parallel enabled (for MoE)
|
||||
- num_speculative_tokens = 3
|
||||
- enforce_eager = True
|
||||
- Acceptance rate matches baseline (tolerance 0.06)
|
||||
"""
|
||||
num_speculative_tokens = 3
|
||||
main_model_name = VWN_EAGLE3_MODELS["vwn_eagle3"]["main"]
|
||||
spec_model_name = VWN_EAGLE3_MODELS["vwn_eagle3"]["spec"]
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
main_model_name,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0,
|
||||
ignore_eos=False,
|
||||
max_tokens=256,
|
||||
)
|
||||
|
||||
prompts = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, my name is",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "The capital of France is",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "The future of AI is",
|
||||
},
|
||||
]
|
||||
prompts = [
|
||||
tokenizer.apply_chat_template(
|
||||
[prompt],
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
||||
speculative_config = {
|
||||
"method": "eagle3",
|
||||
"num_speculative_tokens": num_speculative_tokens,
|
||||
"model": spec_model_name,
|
||||
}
|
||||
|
||||
with VllmRunner(
|
||||
main_model_name,
|
||||
enforce_eager=True,
|
||||
max_model_len=2048,
|
||||
disable_log_stats=False,
|
||||
tensor_parallel_size=2,
|
||||
max_num_seqs=16,
|
||||
distributed_executor_backend="mp",
|
||||
gpu_memory_utilization=0.92,
|
||||
speculative_config=speculative_config,
|
||||
enable_expert_parallel=True,
|
||||
) as llm:
|
||||
_ = llm.generate(prompts, sampling_params)
|
||||
metrics = llm.model.get_metrics()
|
||||
|
||||
# Check acceptance rate
|
||||
num_drafts = 0
|
||||
num_accepted_tokens_per_pos = [0] * num_speculative_tokens
|
||||
for metric in metrics:
|
||||
if metric.name == "vllm:spec_decode_num_drafts":
|
||||
assert isinstance(metric, Counter)
|
||||
num_drafts += metric.value
|
||||
elif metric.name == "vllm:spec_decode_num_accepted_tokens_per_pos":
|
||||
assert isinstance(metric, Vector)
|
||||
for pos in range(len(metric.values)):
|
||||
num_accepted_tokens_per_pos[pos] += metric.values[pos]
|
||||
|
||||
acceptance_per_pos = [n / num_drafts for n in num_accepted_tokens_per_pos]
|
||||
golden = BASELINES_SP["vwn_eagle3"]
|
||||
|
||||
match = all(abs(a - b) < 0.06 for a, b in zip(acceptance_per_pos, golden))
|
||||
if not match:
|
||||
print(f"acceptance_per_pos: {acceptance_per_pos}")
|
||||
print(f"golden: {golden}")
|
||||
|
||||
assert match
|
||||
79
tests/e2e/pull_request/two_card/test_data_parallel.py
Normal file
79
tests/e2e/pull_request/two_card/test_data_parallel.py
Normal file
@@ -0,0 +1,79 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
"""
|
||||
Compare the outputs of vLLM with and without aclgraph.
|
||||
|
||||
Run `pytest tests/e2e/pull_request/two_card/test_data_parallel.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import wait_until_npu_memory_free
|
||||
|
||||
MODELS = ["Qwen/Qwen3-30B-A3B", "vllm-ascend/Qwen3-30B-A3B-W8A8"]
|
||||
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||
DATA_PARALLEL_SCRIPT = REPO_ROOT / "examples" / "offline_data_parallel.py"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [32])
|
||||
@patch.dict(os.environ, {"ASCEND_RT_VISIBLE_DEVICES": "0,1"})
|
||||
@patch.dict(os.environ, {"HCCL_BUFFSIZE": "1024"})
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.7)
|
||||
def test_qwen3_inference_dp2(model, max_tokens):
|
||||
moe_models = ["Qwen/Qwen3-30B-A3B", "vllm-ascend/Qwen3-30B-A3B-W8A8"]
|
||||
quantization_models = ["vllm-ascend/Qwen3-30B-A3B-W8A8"]
|
||||
env = os.environ.copy()
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(DATA_PARALLEL_SCRIPT),
|
||||
"--model",
|
||||
model,
|
||||
"--dp-size",
|
||||
"2",
|
||||
"--tp-size",
|
||||
"1",
|
||||
"--node-size",
|
||||
"1",
|
||||
"--node-rank",
|
||||
"0",
|
||||
"--trust-remote-code",
|
||||
]
|
||||
|
||||
if model in moe_models:
|
||||
cmd.append("--enable-expert-parallel")
|
||||
if model in quantization_models:
|
||||
cmd.append("--quantization")
|
||||
cmd.append("ascend")
|
||||
|
||||
print(f"Running subprocess: {' '.join(cmd)}")
|
||||
proc = subprocess.run(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=600)
|
||||
output = proc.stdout.decode(errors="ignore")
|
||||
|
||||
print(output)
|
||||
|
||||
assert "DP rank 0 needs to process" in output
|
||||
assert "DP rank 1 needs to process" in output
|
||||
assert "Generated text:" in output
|
||||
assert proc.returncode == 0
|
||||
@@ -0,0 +1,39 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
|
||||
def test_deepseek_multistream_moe_tp2():
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
dtype = "half"
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
"vllm-ascend/DeepSeek-V3-Pruning",
|
||||
dtype=dtype,
|
||||
tensor_parallel_size=2,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
distributed_executor_backend="mp",
|
||||
additional_config={
|
||||
"enable_multistream_moe": True,
|
||||
"refresh": True,
|
||||
},
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
@@ -0,0 +1,97 @@
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
|
||||
import pytest
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
from tests.e2e.conftest import DisaggEpdProxy, RemoteEPDServer
|
||||
from tools.send_mm_request import send_image_request
|
||||
|
||||
MODELS = [
|
||||
"Qwen/Qwen2.5-VL-7B-Instruct",
|
||||
]
|
||||
SHARED_STORAGE_PATH = "/dev/shm/epd/storage"
|
||||
TENSOR_PARALLELS = [1]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("tp_size", TENSOR_PARALLELS)
|
||||
async def test_models(model: str, tp_size: int) -> None:
|
||||
encode_port = get_open_port()
|
||||
pd_port = get_open_port()
|
||||
vllm_server_args = [
|
||||
[
|
||||
"--port",
|
||||
str(encode_port),
|
||||
"--model",
|
||||
model,
|
||||
"--gpu-memory-utilization",
|
||||
"0.01",
|
||||
"--tensor-parallel-size",
|
||||
str(tp_size),
|
||||
"--enforce-eager",
|
||||
"--no-enable-prefix-caching",
|
||||
"--max-model-len",
|
||||
"10000",
|
||||
"--max-num-batched-tokens",
|
||||
"10000",
|
||||
"--max-num-seqs",
|
||||
"1",
|
||||
"--ec-transfer-config",
|
||||
'{"ec_connector_extra_config":{"shared_storage_path":"'
|
||||
+ SHARED_STORAGE_PATH
|
||||
+ '"},"ec_connector":"ECExampleConnector","ec_role": "ec_producer"}',
|
||||
],
|
||||
[
|
||||
"--port",
|
||||
str(pd_port),
|
||||
"--model",
|
||||
model,
|
||||
"--gpu-memory-utilization",
|
||||
"0.95",
|
||||
"--tensor-parallel-size",
|
||||
str(tp_size),
|
||||
"--enforce-eager",
|
||||
"--max-model-len",
|
||||
"10000",
|
||||
"--max-num-batched-tokens",
|
||||
"10000",
|
||||
"--max-num-seqs",
|
||||
"128",
|
||||
"--ec-transfer-config",
|
||||
'{"ec_connector_extra_config":{"shared_storage_path":"'
|
||||
+ SHARED_STORAGE_PATH
|
||||
+ '"},"ec_connector":"ECExampleConnector","ec_role": "ec_consumer"}',
|
||||
],
|
||||
]
|
||||
proxy_port = get_open_port()
|
||||
proxy_args = [
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
str(proxy_port),
|
||||
"--encode-servers-urls",
|
||||
f"http://localhost:{encode_port}",
|
||||
"--decode-servers-urls",
|
||||
f"http://localhost:{pd_port}",
|
||||
"--prefill-servers-urls",
|
||||
"disable",
|
||||
]
|
||||
|
||||
with RemoteEPDServer(vllm_serve_args=vllm_server_args) as _, DisaggEpdProxy(proxy_args=proxy_args) as proxy:
|
||||
send_image_request(model, proxy)
|
||||
229
tests/e2e/pull_request/two_card/test_external_launcher.py
Normal file
229
tests/e2e/pull_request/two_card/test_external_launcher.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
"""
|
||||
Compare the outputs of vLLM with and without aclgraph.
|
||||
|
||||
Run `pytest tests/e2e/pull_request/two_card/test_external_launcher.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import huggingface_hub
|
||||
import pytest
|
||||
import torch_npu
|
||||
from modelscope import snapshot_download # type: ignore
|
||||
|
||||
from tests.e2e.conftest import wait_until_npu_memory_free
|
||||
|
||||
MODELS = ["Qwen/Qwen3-0.6B"]
|
||||
MOE_MODELS = ["Qwen/Qwen3-30B-A3B"]
|
||||
DEVICE_NAME = torch_npu.npu.get_device_name(0)[:10]
|
||||
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||
EXTERNAL_LAUNCHER_SCRIPT = REPO_ROOT / "examples" / "offline_external_launcher.py"
|
||||
EXTERNAL_LAUNCHER_TIMEOUT_S = 720
|
||||
|
||||
|
||||
def _decode_output(output):
|
||||
if output is None:
|
||||
return ""
|
||||
if isinstance(output, bytes):
|
||||
return output.decode(errors="ignore")
|
||||
return output
|
||||
|
||||
|
||||
def _run_external_launcher(cmd, env):
|
||||
env = env.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
|
||||
print(f"Running subprocess: {' '.join(cmd)}")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=EXTERNAL_LAUNCHER_TIMEOUT_S,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
print(f"Subprocess timed out after {EXTERNAL_LAUNCHER_TIMEOUT_S} seconds.")
|
||||
output = _decode_output(exc.output)
|
||||
if output:
|
||||
print(output)
|
||||
else:
|
||||
print("No subprocess output captured before timeout.")
|
||||
raise
|
||||
|
||||
output = _decode_output(proc.stdout)
|
||||
print(output)
|
||||
return proc, output
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@patch.dict(os.environ, {"HCCL_BUFFSIZE": "500"})
|
||||
def test_qwen3_external_launcher(model):
|
||||
env = os.environ.copy()
|
||||
# TODO: Change to 2 when ci machine has 4 cards
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(EXTERNAL_LAUNCHER_SCRIPT),
|
||||
"--model",
|
||||
model,
|
||||
"--tp-size",
|
||||
"1",
|
||||
"--node-size",
|
||||
"1",
|
||||
"--node-rank",
|
||||
"0",
|
||||
"--proc-per-node",
|
||||
"2",
|
||||
"--trust-remote-code",
|
||||
]
|
||||
|
||||
proc, output = _run_external_launcher(cmd, env)
|
||||
|
||||
assert "TP RANKS: [0]" in output
|
||||
assert "TP RANKS: [1]" in output
|
||||
assert "Generated text:" in output
|
||||
assert proc.returncode == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MOE_MODELS)
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.7)
|
||||
def test_qwen3_moe_external_launcher_ep_tp2(model):
|
||||
env = os.environ.copy()
|
||||
# TODO: Change to 2 when ci machine has 4 cards
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(EXTERNAL_LAUNCHER_SCRIPT),
|
||||
"--model",
|
||||
model,
|
||||
"--tp-size",
|
||||
"2",
|
||||
"--node-size",
|
||||
"1",
|
||||
"--node-rank",
|
||||
"0",
|
||||
"--proc-per-node",
|
||||
"2",
|
||||
"--trust-remote-code",
|
||||
"--enable-expert-parallel",
|
||||
]
|
||||
|
||||
proc, output = _run_external_launcher(cmd, env)
|
||||
|
||||
assert "TP RANKS: [0, 1]" in output
|
||||
assert "Generated text:" in output
|
||||
assert proc.returncode == 0
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_NZ": "0"})
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.7)
|
||||
def test_qwen3_external_launcher_with_sleepmode():
|
||||
env = os.environ.copy()
|
||||
# TODO: Change to 2 when ci machine has 4 cards
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(EXTERNAL_LAUNCHER_SCRIPT),
|
||||
"--model",
|
||||
"Qwen/Qwen3-8B",
|
||||
"--tp-size",
|
||||
"1",
|
||||
"--node-size",
|
||||
"1",
|
||||
"--node-rank",
|
||||
"0",
|
||||
"--proc-per-node",
|
||||
"2",
|
||||
"--trust-remote-code",
|
||||
"--enable-sleep-mode",
|
||||
"--temperature",
|
||||
"0",
|
||||
"--model-weight-gib",
|
||||
"16",
|
||||
]
|
||||
|
||||
proc, output = _run_external_launcher(cmd, env)
|
||||
|
||||
assert "Generated text:" in output
|
||||
assert "Sleep and wake up successfully!!" in output
|
||||
assert proc.returncode == 0
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_NZ": "0"})
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.7)
|
||||
def test_qwen3_external_launcher_with_sleepmode_level2():
|
||||
env = os.environ.copy()
|
||||
model_path = snapshot_download(
|
||||
"Qwen/Qwen3-8B",
|
||||
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
||||
)
|
||||
# TODO: Add moe model test
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(EXTERNAL_LAUNCHER_SCRIPT),
|
||||
"--model",
|
||||
model_path,
|
||||
"--tp-size",
|
||||
"1",
|
||||
"--node-size",
|
||||
"1",
|
||||
"--node-rank",
|
||||
"0",
|
||||
"--proc-per-node",
|
||||
"2",
|
||||
"--trust-remote-code",
|
||||
"--enable-sleep-mode",
|
||||
"--temperature",
|
||||
"0",
|
||||
"--model-weight-gib",
|
||||
"16",
|
||||
"--sleep-mode-level",
|
||||
"2",
|
||||
]
|
||||
|
||||
proc, output = _run_external_launcher(cmd, env)
|
||||
|
||||
assert "Generated text:" in output
|
||||
assert "Sleep and wake up successfully!!" in output
|
||||
assert proc.returncode == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
DEVICE_NAME != "Ascend910B",
|
||||
reason="This test is only for Ascend910B devices.",
|
||||
)
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@wait_until_npu_memory_free(target_free_percentage=0.7)
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_MATMUL_ALLREDUCE": "1", "HCCL_BUFFSIZE": "500"})
|
||||
def test_qwen3_external_launcher_with_matmul_allreduce(model):
|
||||
env = os.environ.copy()
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(EXTERNAL_LAUNCHER_SCRIPT),
|
||||
"--model",
|
||||
model,
|
||||
"--trust-remote-code",
|
||||
]
|
||||
|
||||
proc, output = _run_external_launcher(cmd, env)
|
||||
|
||||
assert "Generated text:" in output
|
||||
assert proc.returncode == 0
|
||||
117
tests/e2e/pull_request/two_card/test_flashcomm_distributed.py
Normal file
117
tests/e2e/pull_request/two_card/test_flashcomm_distributed.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
"""Compare the short outputs of HF and vLLM when using greedy sampling.
|
||||
|
||||
Run `pytest tests/e2e/pull_request/two_card/test_flashcomm_distributed.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams
|
||||
from vllm.config import KVTransferConfig
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
QWEN_DENSE_MODELS = [
|
||||
"vllm-ascend/Qwen3-0.6B-W8A8",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="test is broken, fix me")
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1"})
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_FLASHCOMM2_PARALLEL_SIZE": "1"})
|
||||
def test_qwen3_moe_fc2_oshard_tp2() -> None:
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
sampling_params = SamplingParams(max_tokens=5, temperature=0.0, top_k=50, top_p=0.9)
|
||||
|
||||
with VllmRunner(
|
||||
"Qwen/Qwen3-30B-A3B",
|
||||
dtype="auto",
|
||||
tensor_parallel_size=2,
|
||||
distributed_executor_backend="mp",
|
||||
enable_expert_parallel=True,
|
||||
enforce_eager=True,
|
||||
additional_config={"layer_sharding": ["o_proj"]},
|
||||
kv_transfer_config=KVTransferConfig(kv_role="kv_producer"),
|
||||
) as vllm_model:
|
||||
vllm_model.generate(example_prompts, sampling_params)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="test is broken, fix me")
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1"})
|
||||
def test_deepseek_v2_lite_fc1_tp2() -> None:
|
||||
example_prompts = [
|
||||
"test" * 1001,
|
||||
]
|
||||
sampling_params = SamplingParams(max_tokens=5, temperature=0.0, top_k=50, top_p=0.9)
|
||||
with VllmRunner(
|
||||
"vllm-ascend/DeepSeek-V2-Lite-W8A8",
|
||||
dtype="auto",
|
||||
tensor_parallel_size=2,
|
||||
distributed_executor_backend="mp",
|
||||
enable_expert_parallel=True,
|
||||
enforce_eager=True,
|
||||
quantization="ascend",
|
||||
) as vllm_model:
|
||||
vllm_model.generate(example_prompts, sampling_params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", QWEN_DENSE_MODELS)
|
||||
@pytest.mark.skip(reason="test is broken, fix me")
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1"})
|
||||
def test_qwen3_dense_fc1_tp2(model):
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=8192,
|
||||
dtype="auto",
|
||||
tensor_parallel_size=2,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
quantization="ascend",
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", QWEN_DENSE_MODELS)
|
||||
@pytest.mark.skip(reason="test is broken, fix me")
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_FLASHCOMM1": "1"})
|
||||
def test_qwen3_dense_prefetch_mlp_weight_tp2(model):
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=8192,
|
||||
dtype="auto",
|
||||
tensor_parallel_size=2,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
quantization="ascend",
|
||||
additional_config={"weight_prefetch_config": {"enabled": True}},
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
44
tests/e2e/pull_request/two_card/test_gpt_oss_distributed.py
Normal file
44
tests/e2e/pull_request/two_card/test_gpt_oss_distributed.py
Normal file
@@ -0,0 +1,44 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
# Adapted from vllm/tests/basic_correctness/test_basic_correctness.py
|
||||
#
|
||||
"""Compare the short outputs of HF and vLLM when using greedy sampling.
|
||||
|
||||
Run `pytest tests/e2e/pull_request/two_card/test_gpt_oss_distributed.py`.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
GPT_OSS_MODELS = [
|
||||
"unsloth/gpt-oss-20b-BF16",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", GPT_OSS_MODELS)
|
||||
def test_gpt_oss_distributed_tp2(model):
|
||||
example_prompts = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
max_tokens = 5
|
||||
with VllmRunner(
|
||||
model,
|
||||
tensor_parallel_size=2,
|
||||
enforce_eager=True,
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy(example_prompts, max_tokens)
|
||||
342
tests/e2e/pull_request/two_card/test_hccl_weight_transfer.py
Normal file
342
tests/e2e/pull_request/two_card/test_hccl_weight_transfer.py
Normal file
@@ -0,0 +1,342 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
"""End-to-end test for the HCCL weight transfer engine.
|
||||
|
||||
This test starts a vLLM server with dummy weights and the HCCL weight transfer
|
||||
backend enabled, then runs the trainer side of an RLHF-style weight sync from a
|
||||
separate NPU. It exercises the full control plane (HTTP) + data plane (HCCL
|
||||
packed broadcast + layerwise reload) and asserts the server's weights actually
|
||||
change after the broadcast.
|
||||
|
||||
To keep the test self-contained and download-free, the trainer model is built
|
||||
from the architecture config with random weights (only the tiny config/tokenizer
|
||||
are needed, which the server already fetches). The parameter names/shapes/dtypes
|
||||
match the real checkpoint, so the broadcast pipeline is fully exercised; we just
|
||||
don't assert "coherent text" since the broadcast weights are random. Set
|
||||
``WEIGHT_TRANSFER_TEST_MODEL=/path/to/checkpoint`` to instead broadcast real
|
||||
weights from a local checkpoint.
|
||||
|
||||
Topology (requires 2 NPUs):
|
||||
- NPU 0: vLLM inference worker (rank 1 in the HCCL group)
|
||||
- NPU 1: trainer / weight source (rank 0 in the HCCL group)
|
||||
|
||||
Refer to ``examples/rl/rlhf_http_hccl.py`` for the end-user workflow.
|
||||
|
||||
Run with::
|
||||
|
||||
pytest tests/e2e/multicard/2-cards/test_weight_transfer_hccl.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import torch
|
||||
import torch_npu # noqa: F401 # registers the NPU backend
|
||||
from transformers import AutoConfig, AutoModelForCausalLM
|
||||
from vllm.utils.network_utils import get_ip, get_open_port
|
||||
|
||||
from tests.e2e.conftest import RemoteOpenAIServer
|
||||
|
||||
MODEL_NAME = "Qwen/Qwen3-0.6B"
|
||||
|
||||
# Device 0 hosts the inference worker, device 1 hosts the trainer.
|
||||
INFERENCE_WORLD_SIZE = 1
|
||||
TRAINER_DEVICE_INDEX = INFERENCE_WORLD_SIZE
|
||||
|
||||
PROMPTS = [
|
||||
"Hello, my name is",
|
||||
"The capital of France is",
|
||||
]
|
||||
|
||||
# HTTP timeouts (seconds). Weight broadcast can take a while for large models.
|
||||
INIT_TIMEOUT = 120
|
||||
UPDATE_TIMEOUT = 300
|
||||
CONTROL_TIMEOUT = 60
|
||||
|
||||
|
||||
def _log(message: str) -> None:
|
||||
"""Flushed log so step markers show up immediately even when stdout is piped."""
|
||||
print(f"[trainer] {message}", flush=True)
|
||||
|
||||
|
||||
def _build_trainer_model(device_index: int):
|
||||
"""Build the trainer-side model without downloading the checkpoint weights.
|
||||
|
||||
By default the model is instantiated from the architecture config with random
|
||||
weights (no ``model.safetensors`` download required); only the tiny config is
|
||||
read, which the server already fetches. Its ``named_parameters`` carry the
|
||||
same names/shapes/dtypes as the real checkpoint, so the HCCL broadcast +
|
||||
layerwise reload path is exercised exactly as with real weights.
|
||||
|
||||
Set ``WEIGHT_TRANSFER_TEST_MODEL=/path/to/checkpoint`` to broadcast real
|
||||
weights from a local directory instead.
|
||||
"""
|
||||
device = f"npu:{device_index}"
|
||||
override_path = os.getenv("WEIGHT_TRANSFER_TEST_MODEL")
|
||||
if override_path:
|
||||
_log(f"loading real trainer weights from {override_path}")
|
||||
model = AutoModelForCausalLM.from_pretrained(override_path, dtype=torch.bfloat16)
|
||||
else:
|
||||
_log("building trainer model from config with random weights (download-free)")
|
||||
config = AutoConfig.from_pretrained(MODEL_NAME, trust_remote_code=True)
|
||||
model = AutoModelForCausalLM.from_config(config)
|
||||
model = model.to(device=device, dtype=torch.bfloat16)
|
||||
return model
|
||||
|
||||
|
||||
def _post(server: RemoteOpenAIServer, route: str, *, json=None, timeout=CONTROL_TIMEOUT):
|
||||
response = requests.post(server.url_for(route), json=json, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
|
||||
class _BackgroundPost(threading.Thread):
|
||||
"""Run an HTTP POST in a thread while keeping its exception visible.
|
||||
|
||||
The trainer side blocks on collective HCCL ops, so the matching server-side
|
||||
RPC must run concurrently. If that RPC fails, swallowing the exception would
|
||||
deadlock the trainer forever; instead we record it and surface it on join().
|
||||
"""
|
||||
|
||||
def __init__(self, server: RemoteOpenAIServer, route: str, *, json=None, timeout=CONTROL_TIMEOUT):
|
||||
super().__init__(daemon=True)
|
||||
self._server = server
|
||||
self._route = route
|
||||
self._json = json
|
||||
self._timeout = timeout
|
||||
self.error: BaseException | None = None
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
_post(self._server, self._route, json=self._json, timeout=self._timeout)
|
||||
_log(f"background POST /{self._route} done")
|
||||
except BaseException as exc: # noqa: BLE001 - re-raised on join via raise_if_failed
|
||||
self.error = exc
|
||||
_log(f"background POST /{self._route} FAILED: {exc!r}")
|
||||
|
||||
def raise_if_failed(self) -> None:
|
||||
if self.error is not None:
|
||||
raise RuntimeError(f"server-side /{self._route} failed") from self.error
|
||||
|
||||
|
||||
def _generate(client, model, prompts):
|
||||
completions = []
|
||||
for prompt in prompts:
|
||||
response = client.completions.create(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
max_tokens=16,
|
||||
temperature=0,
|
||||
)
|
||||
completions.append(response.choices[0].text)
|
||||
return completions
|
||||
|
||||
|
||||
def _collect_weight_metadata(train_model):
|
||||
"""Collect parameter metadata and size the packed buffer for broadcasting."""
|
||||
names: list[str] = []
|
||||
dtype_names: list[str] = []
|
||||
shapes: list[list[int]] = []
|
||||
max_tensor_bytes = 0
|
||||
for name, parameter in train_model.named_parameters():
|
||||
names.append(name)
|
||||
dtype_names.append(str(parameter.dtype).split(".")[-1])
|
||||
shapes.append(list(parameter.shape))
|
||||
tensor_bytes = parameter.numel() * parameter.element_size()
|
||||
max_tensor_bytes = max(max_tensor_bytes, tensor_bytes)
|
||||
|
||||
# Keep the 1 GiB default unless a single tensor needs more (+128 MiB headroom).
|
||||
packed_buffer_size_bytes = max(max_tensor_bytes + 128 * 2**20, 2**30)
|
||||
return names, dtype_names, shapes, packed_buffer_size_bytes
|
||||
|
||||
|
||||
def _has_lifecycle_endpoints(server: RemoteOpenAIServer) -> bool:
|
||||
"""Detect whether the server exposes the vLLM-main start/finish endpoints.
|
||||
|
||||
On vLLM main, ``/start_weight_update`` and ``/finish_weight_update`` drive
|
||||
the layerwise reload lifecycle. On v0.20.2 these endpoints do not exist and
|
||||
``update_weights`` is self-contained, so a probe returns 404.
|
||||
"""
|
||||
try:
|
||||
response = requests.post(
|
||||
server.url_for("start_weight_update"),
|
||||
json={"is_checkpoint_format": True},
|
||||
timeout=CONTROL_TIMEOUT,
|
||||
)
|
||||
except requests.RequestException:
|
||||
return False
|
||||
if response.status_code == 404:
|
||||
return False
|
||||
response.raise_for_status()
|
||||
return True
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
torch.npu.device_count() < 2,
|
||||
reason="HCCL weight transfer e2e test requires at least 2 NPUs.",
|
||||
)
|
||||
def test_hccl_weight_transfer_updates_server_weights():
|
||||
port = get_open_port()
|
||||
server_args = [
|
||||
"--enforce-eager",
|
||||
"--load-format",
|
||||
"dummy",
|
||||
"--weight-transfer-config",
|
||||
'{"backend": "nccl"}',
|
||||
"--tensor-parallel-size",
|
||||
str(INFERENCE_WORLD_SIZE),
|
||||
"--max-model-len",
|
||||
"1024",
|
||||
"--gpu-memory-utilization",
|
||||
"0.6",
|
||||
"--port",
|
||||
str(port),
|
||||
"--trust-remote-code",
|
||||
]
|
||||
# The dev-mode endpoints (/init_weight_transfer_engine, /update_weights,
|
||||
# /pause, /resume, ...) are only registered when VLLM_SERVER_DEV_MODE=1.
|
||||
# Pin the server to NPU 0 so the trainer can own NPU 1 exclusively.
|
||||
env_dict = {
|
||||
"VLLM_SERVER_DEV_MODE": "1",
|
||||
"ASCEND_RT_VISIBLE_DEVICES": "0",
|
||||
"VLLM_ASCEND_ENABLE_NZ": "0",
|
||||
}
|
||||
|
||||
_log(f"starting server on port {port} (device 0, dummy weights) ...")
|
||||
with RemoteOpenAIServer(
|
||||
MODEL_NAME,
|
||||
vllm_serve_args=server_args,
|
||||
# Health check, OpenAI client and control-plane requests all target this
|
||||
# host; use loopback explicitly so they reach the local server directly.
|
||||
server_host="127.0.0.1",
|
||||
server_port=port,
|
||||
env_dict=env_dict,
|
||||
auto_port=False,
|
||||
) as server:
|
||||
client = server.get_client()
|
||||
|
||||
# 1) Baseline generation with dummy weights (expected to be nonsense).
|
||||
_log("generating baseline outputs (dummy weights) ...")
|
||||
outputs_before = _generate(client, MODEL_NAME, PROMPTS)
|
||||
_log(f"outputs BEFORE weight update: {outputs_before}")
|
||||
|
||||
# 2) Build the trainer model on the trainer NPU (download-free by default).
|
||||
_log(f"preparing trainer model on npu:{TRAINER_DEVICE_INDEX} ...")
|
||||
torch.npu.set_device(TRAINER_DEVICE_INDEX)
|
||||
train_model = _build_trainer_model(TRAINER_DEVICE_INDEX)
|
||||
_log("trainer model ready")
|
||||
|
||||
# Import after the server is up so the HCCL engine plugin is registered.
|
||||
from vllm_ascend.distributed.weight_transfer.hccl_engine import (
|
||||
HCCLTrainerSendWeightsArgs,
|
||||
HCCLWeightTransferEngine,
|
||||
)
|
||||
|
||||
master_address = get_ip()
|
||||
master_port = get_open_port()
|
||||
rank_offset = 1
|
||||
world_size = INFERENCE_WORLD_SIZE + 1 # workers + trainer
|
||||
|
||||
# 3) Build the HCCL process group on both sides. The server side blocks
|
||||
# until the trainer connects, so kick it off in a background thread.
|
||||
init_info = dict(
|
||||
master_address=master_address,
|
||||
master_port=master_port,
|
||||
rank_offset=rank_offset,
|
||||
world_size=world_size,
|
||||
)
|
||||
_log(f"HCCL rendezvous at {master_address}:{master_port} (world_size={world_size}) ...")
|
||||
init_thread = _BackgroundPost(
|
||||
server,
|
||||
"init_weight_transfer_engine",
|
||||
json={"init_info": init_info},
|
||||
timeout=INIT_TIMEOUT,
|
||||
)
|
||||
init_thread.start()
|
||||
model_update_group = HCCLWeightTransferEngine.trainer_init(
|
||||
dict(
|
||||
master_address=master_address,
|
||||
master_port=master_port,
|
||||
world_size=world_size,
|
||||
),
|
||||
)
|
||||
_log("trainer_init returned, waiting for server init RPC ...")
|
||||
init_thread.join()
|
||||
init_thread.raise_if_failed()
|
||||
_log("HCCL process group established")
|
||||
|
||||
# 4) Pause generation and start the weight update lifecycle. On vLLM
|
||||
# main this probe also performs the actual /start_weight_update call,
|
||||
# so we must not call it again below.
|
||||
_post(server, "pause")
|
||||
use_lifecycle = _has_lifecycle_endpoints(server)
|
||||
_log(f"paused; lifecycle endpoints available: {use_lifecycle}")
|
||||
|
||||
names, dtype_names, shapes, packed_buffer_size_bytes = _collect_weight_metadata(train_model)
|
||||
update_info = dict(
|
||||
names=names,
|
||||
dtype_names=dtype_names,
|
||||
shapes=shapes,
|
||||
packed=True,
|
||||
packed_buffer_size_bytes=packed_buffer_size_bytes,
|
||||
)
|
||||
if not use_lifecycle:
|
||||
# v0.20.2 folds the layerwise reload lifecycle into update_weights.
|
||||
update_info["is_checkpoint_format"] = True
|
||||
|
||||
# update_weights blocks on the server while it waits for HCCL broadcasts,
|
||||
# so run it in a thread while the trainer produces the data.
|
||||
_log(f"broadcasting {len(names)} tensors via HCCL (packed) ...")
|
||||
update_thread = _BackgroundPost(
|
||||
server,
|
||||
"update_weights",
|
||||
json={"update_info": update_info},
|
||||
timeout=UPDATE_TIMEOUT,
|
||||
)
|
||||
update_thread.start()
|
||||
|
||||
trainer_args = HCCLTrainerSendWeightsArgs(
|
||||
group=model_update_group,
|
||||
packed=True,
|
||||
packed_buffer_size_bytes=packed_buffer_size_bytes,
|
||||
)
|
||||
HCCLWeightTransferEngine.trainer_send_weights(
|
||||
iterator=train_model.named_parameters(),
|
||||
trainer_args=trainer_args,
|
||||
)
|
||||
_log("trainer finished sending weights, waiting for server update RPC ...")
|
||||
update_thread.join()
|
||||
update_thread.raise_if_failed()
|
||||
_log("weight broadcast complete")
|
||||
|
||||
# 5) Finalize the lifecycle and resume generation.
|
||||
if use_lifecycle:
|
||||
_post(server, "finish_weight_update")
|
||||
_post(server, "resume")
|
||||
|
||||
# 6) Generation after the broadcast weights are loaded.
|
||||
outputs_after = _generate(client, MODEL_NAME, PROMPTS)
|
||||
_log(f"outputs AFTER weight update: {outputs_after}")
|
||||
|
||||
# Reaching here means the full HCCL transfer pipeline succeeded: every
|
||||
# control-plane RPC raised on a non-2xx response and each background POST
|
||||
# re-raised on join(). The broadcast weights differ from the server's dummy
|
||||
# init, so the served model must now produce different generations.
|
||||
assert outputs_after != outputs_before, "server weights did not change after HCCL transfer"
|
||||
38
tests/e2e/pull_request/two_card/test_moe_routing_replay.py
Normal file
38
tests/e2e/pull_request/two_card/test_moe_routing_replay.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from vllm import SamplingParams
|
||||
from vllm.sampling_params import RequestOutputKind
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
|
||||
MODELS = [
|
||||
"Qwen/Qwen3.5-35B-A3B",
|
||||
"Qwen/Qwen3-30B-A3B",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@patch.dict(os.environ, {"OMP_NUM_THREADS": "1"})
|
||||
def test_qwen3_moe_routing_replay(model):
|
||||
prompts = [
|
||||
"Hello, please introduce yourself.",
|
||||
]
|
||||
with VllmRunner(
|
||||
model,
|
||||
tensor_parallel_size=2,
|
||||
enable_expert_parallel=True,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
distributed_executor_backend="mp",
|
||||
enable_return_routed_experts=True,
|
||||
async_scheduling=False,
|
||||
) as vllm_model:
|
||||
sampling_params = SamplingParams(
|
||||
max_tokens=5, temperature=0.8, top_p=0.95, output_kind=RequestOutputKind.FINAL_ONLY
|
||||
)
|
||||
inputs = vllm_model.get_inputs(prompts=prompts)
|
||||
outputs = vllm_model.model.generate(prompts=inputs, sampling_params=sampling_params)
|
||||
assert outputs[0].finished
|
||||
assert len(outputs[0].outputs[0].text) > 0
|
||||
assert outputs[0].outputs[0].routed_experts.size > 0
|
||||
77
tests/e2e/pull_request/two_card/test_offline_weight_load.py
Normal file
77
tests/e2e/pull_request/two_card/test_offline_weight_load.py
Normal file
@@ -0,0 +1,77 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
"""
|
||||
Run `pytest tests/e2e/pull_request/two_card/test_offline_weight_load.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import wait_until_npu_memory_free
|
||||
|
||||
MODELS = ["Qwen/Qwen3-30B-A3B"]
|
||||
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||
EXTERNAL_LAUNCHER_SCRIPT = REPO_ROOT / "examples" / "offline_external_launcher.py"
|
||||
|
||||
|
||||
@pytest.mark.skip("fix me, unstable, timeout")
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@patch.dict(os.environ, {"VLLM_ASCEND_ENABLE_NZ": "0"})
|
||||
@wait_until_npu_memory_free(0.7)
|
||||
def test_qwen3_offline_load_and_sleepmode_tp2(model):
|
||||
env = os.environ.copy()
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(EXTERNAL_LAUNCHER_SCRIPT),
|
||||
"--model",
|
||||
model,
|
||||
"--tp-size",
|
||||
"2",
|
||||
"--node-size",
|
||||
"1",
|
||||
"--node-rank",
|
||||
"0",
|
||||
"--proc-per-node",
|
||||
"2",
|
||||
"--trust-remote-code",
|
||||
"--enable-sleep-mode",
|
||||
"--temperature",
|
||||
"0",
|
||||
"--model-weight-gib",
|
||||
"0.8",
|
||||
]
|
||||
|
||||
print(f"Running subprocess: {' '.join(cmd)}")
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=600,
|
||||
)
|
||||
output = proc.stdout.decode(errors="ignore")
|
||||
|
||||
print(output)
|
||||
|
||||
assert "Generated text:" in output
|
||||
assert "Sleep and wake up successfully!!" in output
|
||||
assert proc.returncode == 0
|
||||
90
tests/e2e/pull_request/two_card/test_prefix_caching.py
Normal file
90
tests/e2e/pull_request/two_card/test_prefix_caching.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Compare the with and without prefix caching."""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import VllmRunner
|
||||
from tests.e2e.model_utils import check_outputs_equal
|
||||
|
||||
MODELS = [
|
||||
# for MHA
|
||||
"Qwen/Qwen3-8B",
|
||||
# for MLA
|
||||
"deepseek-ai/DeepSeek-V2-Lite-Chat",
|
||||
]
|
||||
|
||||
# A prompt containing a large markdown table. The table is randomly generated by GPT-4.
|
||||
# ruff: noqa: E501
|
||||
LONG_PROMPT = (
|
||||
"You are a helpful assistant in recognizes the content of tables in markdown format. Here is a table as follows.\n# Table\n"
|
||||
+ """
|
||||
| ID | Name | Age | Occupation | Country | Email | Phone Number | Address |
|
||||
|-----|---------------|-----|---------------|---------------|------------------------|----------------|------------------------------|
|
||||
| 1 | John Doe | 29 | Engineer | USA | john.doe@example.com | 555-1234 | 123 Elm St, Springfield, IL |
|
||||
| 2 | Jane Smith | 34 | Doctor | Canada | jane.smith@example.com | 555-5678 | 456 Oak St, Toronto, ON |
|
||||
| 3 | Alice Johnson | 27 | Teacher | UK | alice.j@example.com | 555-8765 | 789 Pine St, London, UK |
|
||||
| 4 | Bob Brown | 45 | Artist | Australia | bob.b@example.com | 555-4321 | 321 Maple St, Sydney, NSW |
|
||||
| 5 | Carol White | 31 | Scientist | New Zealand | carol.w@example.com | 555-6789 | 654 Birch St, Wellington, NZ |
|
||||
| 6 | Dave Green | 28 | Lawyer | Ireland | dave.g@example.com | 555-3456 | 987 Cedar St, Dublin, IE |
|
||||
| 7 | Emma Black | 40 | Musician | USA | emma.b@example.com | 555-1111 | 246 Ash St, New York, NY |
|
||||
| 8 | Frank Blue | 37 | Chef | Canada | frank.b@example.com | 555-2222 | 135 Spruce St, Vancouver, BC |
|
||||
| 9 | Grace Yellow | 50 | Engineer | UK | grace.y@example.com | 555-3333 | 864 Fir St, Manchester, UK |
|
||||
| 10 | Henry Violet | 32 | Artist | Australia | henry.v@example.com | 555-4444 | 753 Willow St, Melbourne, VIC|
|
||||
| 11 | Irene Orange | 26 | Scientist | New Zealand | irene.o@example.com | 555-5555 | 912 Poplar St, Auckland, NZ |
|
||||
| 12 | Jack Indigo | 38 | Teacher | Ireland | jack.i@example.com | 555-6666 | 159 Elm St, Cork, IE |
|
||||
| 13 | Karen Red | 41 | Lawyer | USA | karen.r@example.com | 555-7777 | 357 Cedar St, Boston, MA |
|
||||
| 14 | Leo Brown | 30 | Chef | Canada | leo.b@example.com | 555-8888 | 246 Oak St, Calgary, AB |
|
||||
| 15 | Mia Green | 33 | Musician | UK | mia.g@example.com | 555-9999 | 975 Pine St, Edinburgh, UK |
|
||||
| 16 | Noah Yellow | 29 | Doctor | Australia | noah.y@example.com | 555-0000 | 864 Birch St, Brisbane, QLD |
|
||||
| 17 | Olivia Blue | 35 | Engineer | New Zealand | olivia.b@example.com | 555-1212 | 753 Maple St, Hamilton, NZ |
|
||||
| 18 | Peter Black | 42 | Artist | Ireland | peter.b@example.com | 555-3434 | 912 Fir St, Limerick, IE |
|
||||
| 19 | Quinn White | 28 | Scientist | USA | quinn.w@example.com | 555-5656 | 159 Willow St, Seattle, WA |
|
||||
| 20 | Rachel Red | 31 | Teacher | Canada | rachel.r@example.com | 555-7878 | 357 Poplar St, Ottawa, ON |
|
||||
| 21 | Steve Green | 44 | Lawyer | UK | steve.g@example.com | 555-9090 | 753 Elm St, Birmingham, UK |
|
||||
| 22 | Tina Blue | 36 | Musician | Australia | tina.b@example.com | 555-1213 | 864 Cedar St, Perth, WA |
|
||||
| 23 | Umar Black | 39 | Chef | New Zealand | umar.b@example.com | 555-3435 | 975 Spruce St, Christchurch, NZ|
|
||||
| 24 | Victor Yellow | 43 | Engineer | Ireland | victor.y@example.com | 555-5657 | 246 Willow St, Galway, IE |
|
||||
| 25 | Wendy Orange | 27 | Artist | USA | wendy.o@example.com | 555-7879 | 135 Elm St, Denver, CO |
|
||||
| 26 | Xavier Green | 34 | Scientist | Canada | xavier.g@example.com | 555-9091 | 357 Oak St, Montreal, QC |
|
||||
| 27 | Yara Red | 41 | Teacher | UK | yara.r@example.com | 555-1214 | 975 Pine St, Leeds, UK |
|
||||
| 28 | Zack Blue | 30 | Lawyer | Australia | zack.b@example.com | 555-3436 | 135 Birch St, Adelaide, SA |
|
||||
| 29 | Amy White | 33 | Musician | New Zealand | amy.w@example.com | 555-5658 | 159 Maple St, Wellington, NZ |
|
||||
| 30 | Ben Black | 38 | Chef | Ireland | ben.b@example.com | 555-7870 | 246 Fir St, Waterford, IE |
|
||||
"""
|
||||
)
|
||||
|
||||
INPUT_PROMPTS = [
|
||||
LONG_PROMPT + "Question: what is the age of John Doe? Your answer: The age of John Doe is ",
|
||||
LONG_PROMPT + "Question: what is the age of Zack Blue? Your answer: The age of Zack Blue is ",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("max_tokens", [50])
|
||||
def test_models_prefix_cache_tp2(model: str, max_tokens: int) -> None:
|
||||
with VllmRunner(
|
||||
model,
|
||||
max_model_len=2048,
|
||||
tensor_parallel_size=2,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
gpu_memory_utilization=0.7,
|
||||
) as vllm_model:
|
||||
prefix_cache_output = vllm_model.generate_greedy(INPUT_PROMPTS, max_tokens)
|
||||
|
||||
with VllmRunner(
|
||||
model,
|
||||
enable_prefix_caching=False,
|
||||
max_model_len=2048,
|
||||
tensor_parallel_size=2,
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
gpu_memory_utilization=0.7,
|
||||
) as vllm_model:
|
||||
vllm_output = vllm_model.generate_greedy(INPUT_PROMPTS, max_tokens)
|
||||
|
||||
check_outputs_equal(
|
||||
outputs_0_lst=vllm_output,
|
||||
outputs_1_lst=prefix_cache_output,
|
||||
name_0="vllm_output",
|
||||
name_1="prefix_cache_output",
|
||||
)
|
||||
82
tests/e2e/pull_request/two_card/test_qwen3_30b_a3b.py
Normal file
82
tests/e2e/pull_request/two_card/test_qwen3_30b_a3b.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
from tests.e2e.conftest import RemoteOpenAIServer, wait_until_npu_memory_free
|
||||
from vllm_ascend.utils import vllm_version_is
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not vllm_version_is("0.23.0"),
|
||||
reason="broken on main, fix me.",
|
||||
)
|
||||
|
||||
|
||||
@wait_until_npu_memory_free()
|
||||
def test_moe_tp_ep_eplb_full_decode_only():
|
||||
"""Verify MoE serving with TP, EP, EPLB, and full decode only."""
|
||||
model = "Qwen/Qwen3-30B-A3B"
|
||||
port = get_open_port()
|
||||
env_dict = {
|
||||
"DYNAMIC_EPLB": "true",
|
||||
"HCCL_BUFFSIZE": "1024",
|
||||
}
|
||||
server_args = [
|
||||
"--max_model_len",
|
||||
"8192",
|
||||
"--tensor_parallel_size",
|
||||
"2",
|
||||
"--enable_expert_parallel",
|
||||
"--port",
|
||||
str(port),
|
||||
"--compilation-config",
|
||||
json.dumps({"cudagraph_mode": "FULL_DECODE_ONLY", "cudagraph_capture_sizes": [8]}),
|
||||
"--additional-config",
|
||||
json.dumps(
|
||||
{
|
||||
"eplb_config": {
|
||||
"dynamic_eplb": True,
|
||||
"expert_heat_collection_interval": 100,
|
||||
"algorithm_execution_interval": 20,
|
||||
"num_redundant_experts": 2,
|
||||
}
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
with RemoteOpenAIServer(model, server_args, server_port=port, auto_port=False, env_dict=env_dict) as server:
|
||||
response = requests.post(
|
||||
server.url_for("v1", "completions"),
|
||||
json={
|
||||
"model": model,
|
||||
"prompt": "What is deeplearning?",
|
||||
"max_tokens": 400,
|
||||
"temperature": 0.0,
|
||||
"top_p": 1.0,
|
||||
"n": 1,
|
||||
},
|
||||
timeout=600,
|
||||
)
|
||||
response.raise_for_status()
|
||||
output = response.json()
|
||||
|
||||
assert output["choices"][0]["text"]
|
||||
44
tests/e2e/pull_request/two_card/test_qwen3_5_35b_a3b_w8a8.py
Normal file
44
tests/e2e/pull_request/two_card/test_qwen3_5_35b_a3b_w8a8.py
Normal file
@@ -0,0 +1,44 @@
|
||||
#
|
||||
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
# Copyright 2023 The vLLM team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# This file is a part of the vllm-ascend project.
|
||||
#
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.e2e.conftest import VllmRunner, wait_until_npu_memory_free
|
||||
|
||||
EXAMPLE_PROMPTS = [
|
||||
"Hello, my name is",
|
||||
]
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"HCCL_BUFFSIZE": "1024"})
|
||||
@wait_until_npu_memory_free()
|
||||
def test_qwen3_5_35b_a3b_w8a8_tp2_without_ep():
|
||||
with VllmRunner(
|
||||
"Eco-Tech/Qwen3.5-35B-A3B-w8a8-mtp",
|
||||
max_model_len=4096,
|
||||
tensor_parallel_size=2,
|
||||
enable_expert_parallel=False,
|
||||
quantization="ascend",
|
||||
gpu_memory_utilization=0.9,
|
||||
distributed_executor_backend="mp",
|
||||
cudagraph_capture_sizes=[1, 2, 4, 8],
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.generate_greedy(EXAMPLE_PROMPTS, max_tokens=5)
|
||||
|
||||
assert outputs[0][1]
|
||||
103
tests/e2e/pull_request/two_card/test_qwen3_6_27b_fia.py
Normal file
103
tests/e2e/pull_request/two_card/test_qwen3_6_27b_fia.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#
|
||||
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from vllm.assets.image import ImageAsset
|
||||
|
||||
from tests.e2e.conftest import VllmRunner, qwen_prompt, wait_until_npu_memory_free
|
||||
|
||||
MODEL = "Qwen/Qwen3.6-27B"
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"HCCL_BUFFSIZE": "1024"})
|
||||
@wait_until_npu_memory_free()
|
||||
def test_qwen3_6_27b_multimodel_fia_eager():
|
||||
"""Verify multimodal generation with FIA op and eager mode."""
|
||||
image = ImageAsset("cherry_blossom").pil_image.convert("RGB")
|
||||
questions = [
|
||||
"What is the content of this image?",
|
||||
"Describe the content of this image in detail.",
|
||||
"What's in the image?",
|
||||
"Where is this image taken?",
|
||||
]
|
||||
|
||||
images = [image] * len(questions)
|
||||
prompts = qwen_prompt(questions)
|
||||
|
||||
with VllmRunner(
|
||||
MODEL,
|
||||
max_model_len=4096,
|
||||
tensor_parallel_size=2,
|
||||
language_model_only=False,
|
||||
gpu_memory_utilization=0.9,
|
||||
limit_mm_per_prompt={"image": 1},
|
||||
mm_processor_kwargs={
|
||||
"min_pixels": 28 * 28,
|
||||
"max_pixels": 1280 * 28 * 28,
|
||||
"fps": 1,
|
||||
},
|
||||
enforce_eager=True,
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.generate_greedy(
|
||||
prompts=prompts,
|
||||
images=images,
|
||||
max_tokens=64,
|
||||
)
|
||||
|
||||
assert outputs[0][1]
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"HCCL_BUFFSIZE": "1024"})
|
||||
@wait_until_npu_memory_free()
|
||||
def test_qwen3_6_27b_multimodel_fia_acl_graph():
|
||||
"""Verify multimodal generation with FIA op and FULL_AND_PIECEWISE graph mode."""
|
||||
image = ImageAsset("cherry_blossom").pil_image.convert("RGB")
|
||||
questions = [
|
||||
"What is the content of this image?",
|
||||
"Describe the content of this image in detail.",
|
||||
"What's in the image?",
|
||||
"Where is this image taken?",
|
||||
]
|
||||
|
||||
images = [image] * len(questions)
|
||||
prompts = qwen_prompt(questions)
|
||||
|
||||
with VllmRunner(
|
||||
MODEL,
|
||||
max_model_len=4096,
|
||||
tensor_parallel_size=2,
|
||||
language_model_only=False,
|
||||
gpu_memory_utilization=0.9,
|
||||
limit_mm_per_prompt={"image": 1},
|
||||
mm_processor_kwargs={
|
||||
"min_pixels": 28 * 28,
|
||||
"max_pixels": 1280 * 28 * 28,
|
||||
"fps": 1,
|
||||
},
|
||||
compilation_config={
|
||||
"cudagraph_mm_encoder": True,
|
||||
"cudagraph_capture_sizes": [1],
|
||||
"encoder_cudagraph_token_budgets": [128, 256, 512, 1024, 1536, 2048, 2560, 3072, 3584, 4096],
|
||||
},
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.generate_greedy(
|
||||
prompts=prompts,
|
||||
images=images,
|
||||
max_tokens=64,
|
||||
)
|
||||
|
||||
assert outputs[0][1]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user