Files
enginex-ascend-910-vllm/tests/e2e/pull_request/four_card/test_graph_mode.py
Sun Ruoxi 7f8a1b1f7a init v0.23.0
Signed-off-by: Sun Ruoxi <sunruoxi@4paradigm.com>
2026-08-27 15:11:51 +08:00

674 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)