chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
This module is named `utils_` instead of `utils` to avoid obscuring
|
||||
`tests/utils.py`.
|
||||
"""
|
||||
@@ -0,0 +1,504 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# ruff: noqa
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from transformers import AutoTokenizer
|
||||
from pydantic import ValidationError
|
||||
|
||||
from vllm.tokenizers.detokenizer_utils import convert_ids_list_to_tokens
|
||||
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
from ..utils import flat_product
|
||||
|
||||
|
||||
# Tests for FlexibleArgumentParser
|
||||
@pytest.fixture
|
||||
def parser():
|
||||
parser = FlexibleArgumentParser()
|
||||
parser.add_argument(
|
||||
"--image-input-type", choices=["pixel_values", "image_features"]
|
||||
)
|
||||
parser.add_argument("--model-name")
|
||||
parser.add_argument("--batch-size", type=int)
|
||||
parser.add_argument("--enable-feature", action="store_true")
|
||||
parser.add_argument("--hf-overrides", type=json.loads)
|
||||
parser.add_argument("-cc", "--compilation-config", type=json.loads)
|
||||
parser.add_argument("--optimization-level", type=int)
|
||||
return parser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser_with_config():
|
||||
parser = FlexibleArgumentParser()
|
||||
parser.add_argument("serve")
|
||||
parser.add_argument("model_tag", nargs="?")
|
||||
parser.add_argument("--model", type=str)
|
||||
parser.add_argument("--served-model-name", type=str)
|
||||
parser.add_argument("--config", type=str)
|
||||
parser.add_argument("--port", type=int)
|
||||
parser.add_argument("--tensor-parallel-size", type=int)
|
||||
parser.add_argument("--trust-remote-code", action="store_true")
|
||||
return parser
|
||||
|
||||
|
||||
def test_underscore_to_dash(parser):
|
||||
args = parser.parse_args(["--image_input_type", "pixel_values"])
|
||||
assert args.image_input_type == "pixel_values"
|
||||
|
||||
|
||||
def test_mixed_usage(parser):
|
||||
args = parser.parse_args(
|
||||
["--image_input_type", "image_features", "--model-name", "facebook/opt-125m"]
|
||||
)
|
||||
assert args.image_input_type == "image_features"
|
||||
assert args.model_name == "facebook/opt-125m"
|
||||
|
||||
|
||||
def test_with_equals_sign(parser):
|
||||
args = parser.parse_args(
|
||||
["--image_input_type=pixel_values", "--model-name=facebook/opt-125m"]
|
||||
)
|
||||
assert args.image_input_type == "pixel_values"
|
||||
assert args.model_name == "facebook/opt-125m"
|
||||
|
||||
|
||||
def test_with_int_value(parser):
|
||||
args = parser.parse_args(["--batch_size", "32"])
|
||||
assert args.batch_size == 32
|
||||
args = parser.parse_args(["--batch-size", "32"])
|
||||
assert args.batch_size == 32
|
||||
|
||||
|
||||
def test_with_bool_flag(parser):
|
||||
args = parser.parse_args(["--enable_feature"])
|
||||
assert args.enable_feature is True
|
||||
args = parser.parse_args(["--enable-feature"])
|
||||
assert args.enable_feature is True
|
||||
|
||||
|
||||
def test_invalid_choice(parser):
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(["--image_input_type", "invalid_choice"])
|
||||
|
||||
|
||||
def test_missing_required_argument(parser):
|
||||
parser.add_argument("--required-arg", required=True)
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args([])
|
||||
|
||||
|
||||
def test_cli_override_to_config(parser_with_config, cli_config_file):
|
||||
args = parser_with_config.parse_args(
|
||||
["serve", "mymodel", "--config", cli_config_file, "--tensor-parallel-size", "3"]
|
||||
)
|
||||
assert args.tensor_parallel_size == 3
|
||||
args = parser_with_config.parse_args(
|
||||
["serve", "mymodel", "--tensor-parallel-size", "3", "--config", cli_config_file]
|
||||
)
|
||||
assert args.tensor_parallel_size == 3
|
||||
assert args.port == 12312
|
||||
args = parser_with_config.parse_args(
|
||||
[
|
||||
"serve",
|
||||
"mymodel",
|
||||
"--tensor-parallel-size",
|
||||
"3",
|
||||
"--config",
|
||||
cli_config_file,
|
||||
"--port",
|
||||
"666",
|
||||
]
|
||||
)
|
||||
assert args.tensor_parallel_size == 3
|
||||
assert args.port == 666
|
||||
|
||||
|
||||
def test_config_args(parser_with_config, cli_config_file):
|
||||
args = parser_with_config.parse_args(
|
||||
["serve", "mymodel", "--config", cli_config_file]
|
||||
)
|
||||
assert args.tensor_parallel_size == 2
|
||||
assert args.trust_remote_code
|
||||
|
||||
|
||||
def test_config_file(parser_with_config):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
parser_with_config.parse_args(
|
||||
["serve", "mymodel", "--config", "test_config.yml"]
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
parser_with_config.parse_args(
|
||||
["serve", "mymodel", "--config", "./data/test_config.json"]
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
parser_with_config.parse_args(
|
||||
[
|
||||
"serve",
|
||||
"mymodel",
|
||||
"--tensor-parallel-size",
|
||||
"3",
|
||||
"--config",
|
||||
"--batch-size",
|
||||
"32",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_no_model_tag(parser_with_config, cli_config_file):
|
||||
with pytest.raises(ValueError):
|
||||
parser_with_config.parse_args(["serve", "--config", cli_config_file])
|
||||
|
||||
|
||||
def test_dict_args(parser):
|
||||
args = [
|
||||
"--model-name=something.something",
|
||||
"--hf-overrides.key1",
|
||||
"val1",
|
||||
# Test nesting
|
||||
"--hf-overrides.key2.key3",
|
||||
"val2",
|
||||
"--hf-overrides.key2.key4",
|
||||
"val3",
|
||||
# Test compile config and compilation mode
|
||||
"-cc.use_inductor_graph_partition=true",
|
||||
"-cc.backend",
|
||||
"custom",
|
||||
"-O1",
|
||||
# Test = sign
|
||||
"--hf-overrides.key5=val4",
|
||||
# Test underscore to dash conversion
|
||||
"--hf_overrides.key_6",
|
||||
"val5",
|
||||
"--hf_overrides.key-7.key_8",
|
||||
"val6",
|
||||
# Test data type detection
|
||||
"--hf_overrides.key9",
|
||||
"100",
|
||||
"--hf_overrides.key10",
|
||||
"100.0",
|
||||
"--hf_overrides.key11",
|
||||
"true",
|
||||
"--hf_overrides.key12.key13",
|
||||
"null",
|
||||
# Test '-' and '.' in value
|
||||
"--hf_overrides.key14.key15",
|
||||
"-minus.and.dot",
|
||||
# Test array values
|
||||
"-cc.custom_ops+",
|
||||
"-quant_fp8",
|
||||
"-cc.custom_ops+=+silu_mul,-rms_norm",
|
||||
]
|
||||
parsed_args = parser.parse_args(args)
|
||||
assert parsed_args.model_name == "something.something"
|
||||
assert parsed_args.hf_overrides == {
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key3": "val2",
|
||||
"key4": "val3",
|
||||
},
|
||||
"key5": "val4",
|
||||
"key_6": "val5",
|
||||
"key-7": {
|
||||
"key_8": "val6",
|
||||
},
|
||||
"key9": 100,
|
||||
"key10": 100.0,
|
||||
"key11": True,
|
||||
"key12": {
|
||||
"key13": None,
|
||||
},
|
||||
"key14": {
|
||||
"key15": "-minus.and.dot",
|
||||
},
|
||||
}
|
||||
assert parsed_args.optimization_level == 1
|
||||
assert parsed_args.compilation_config == {
|
||||
"use_inductor_graph_partition": True,
|
||||
"backend": "custom",
|
||||
"custom_ops": ["-quant_fp8", "+silu_mul", "-rms_norm"],
|
||||
}
|
||||
|
||||
|
||||
def test_duplicate_dict_args(caplog_vllm, parser):
|
||||
args = [
|
||||
"--model-name=something.something",
|
||||
"--hf-overrides.key1",
|
||||
"val1",
|
||||
"--hf-overrides.key1",
|
||||
"val2",
|
||||
"-O1",
|
||||
"-cc.mode",
|
||||
"2",
|
||||
"-O3",
|
||||
]
|
||||
|
||||
parsed_args = parser.parse_args(args)
|
||||
# Should be the last value
|
||||
assert parsed_args.hf_overrides == {"key1": "val2"}
|
||||
assert parsed_args.optimization_level == 3
|
||||
assert parsed_args.compilation_config == {"mode": 2}
|
||||
|
||||
assert len(caplog_vllm.records) == 1
|
||||
assert "duplicate" in caplog_vllm.text
|
||||
assert "--hf-overrides.key1" in caplog_vllm.text
|
||||
assert "--optimization-level" in caplog_vllm.text
|
||||
|
||||
|
||||
def test_model_specification(
|
||||
parser_with_config, cli_config_file, cli_config_file_with_model
|
||||
):
|
||||
# Test model in CLI takes precedence over config
|
||||
args = parser_with_config.parse_args(
|
||||
["serve", "cli-model", "--config", cli_config_file_with_model]
|
||||
)
|
||||
assert args.model_tag == "cli-model"
|
||||
assert args.served_model_name == "mymodel"
|
||||
|
||||
# Test model from config file works
|
||||
args = parser_with_config.parse_args(
|
||||
[
|
||||
"serve",
|
||||
"--config",
|
||||
cli_config_file_with_model,
|
||||
]
|
||||
)
|
||||
assert args.model == "config-model"
|
||||
assert args.served_model_name == "mymodel"
|
||||
|
||||
# Test no model specified anywhere raises error
|
||||
with pytest.raises(ValueError, match="No model specified!"):
|
||||
parser_with_config.parse_args(["serve", "--config", cli_config_file])
|
||||
|
||||
# Test using --model option raises error
|
||||
# with pytest.raises(
|
||||
# ValueError,
|
||||
# match=
|
||||
# ("With `vllm serve`, you should provide the model as a positional "
|
||||
# "argument or in a config file instead of via the `--model` option."),
|
||||
# ):
|
||||
# parser_with_config.parse_args(['serve', '--model', 'my-model'])
|
||||
|
||||
# Test using --model option back-compatibility
|
||||
# (when back-compatibility ends, the above test should be uncommented
|
||||
# and the below test should be removed)
|
||||
args = parser_with_config.parse_args(
|
||||
[
|
||||
"serve",
|
||||
"--tensor-parallel-size",
|
||||
"2",
|
||||
"--model",
|
||||
"my-model",
|
||||
"--trust-remote-code",
|
||||
"--port",
|
||||
"8001",
|
||||
]
|
||||
)
|
||||
assert args.model is None
|
||||
assert args.tensor_parallel_size == 2
|
||||
assert args.trust_remote_code is True
|
||||
assert args.port == 8001
|
||||
|
||||
args = parser_with_config.parse_args(
|
||||
[
|
||||
"serve",
|
||||
"--tensor-parallel-size=2",
|
||||
"--model=my-model",
|
||||
"--trust-remote-code",
|
||||
"--port=8001",
|
||||
]
|
||||
)
|
||||
assert args.model is None
|
||||
assert args.tensor_parallel_size == 2
|
||||
assert args.trust_remote_code is True
|
||||
assert args.port == 8001
|
||||
|
||||
# Test other config values are preserved
|
||||
args = parser_with_config.parse_args(
|
||||
[
|
||||
"serve",
|
||||
"cli-model",
|
||||
"--config",
|
||||
cli_config_file_with_model,
|
||||
]
|
||||
)
|
||||
assert args.tensor_parallel_size == 2
|
||||
assert args.trust_remote_code is True
|
||||
assert args.port == 12312
|
||||
|
||||
|
||||
def test_convert_ids_list_to_tokens():
|
||||
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-1.5B-Instruct")
|
||||
token_ids = tokenizer.encode("Hello, world!")
|
||||
# token_ids = [9707, 11, 1879, 0]
|
||||
assert tokenizer.convert_ids_to_tokens(token_ids) == ["Hello", ",", "Ġworld", "!"]
|
||||
tokens = convert_ids_list_to_tokens(tokenizer, token_ids)
|
||||
assert tokens == ["Hello", ",", " world", "!"]
|
||||
|
||||
|
||||
def test_load_config_file(tmp_path):
|
||||
# Define the configuration data
|
||||
config_data = {
|
||||
"enable-logging": True,
|
||||
"list-arg": ["item1", "item2"],
|
||||
"port": 12323,
|
||||
"tensor-parallel-size": 4,
|
||||
}
|
||||
|
||||
# Write the configuration data to a temporary YAML file
|
||||
config_file_path = tmp_path / "config.yaml"
|
||||
with open(config_file_path, "w") as config_file:
|
||||
yaml.dump(config_data, config_file)
|
||||
|
||||
# Initialize the parser
|
||||
parser = FlexibleArgumentParser()
|
||||
|
||||
# Call the function with the temporary file path
|
||||
processed_args = parser.load_config_file(str(config_file_path))
|
||||
|
||||
# Expected output
|
||||
expected_args = [
|
||||
"--enable-logging",
|
||||
"--list-arg",
|
||||
"item1",
|
||||
"item2",
|
||||
"--port",
|
||||
"12323",
|
||||
"--tensor-parallel-size",
|
||||
"4",
|
||||
]
|
||||
|
||||
# Assert that the processed arguments match the expected output
|
||||
assert processed_args == expected_args
|
||||
os.remove(str(config_file_path))
|
||||
|
||||
|
||||
def test_load_config_file_nested(tmp_path):
|
||||
"""Test that nested dicts in YAML config are converted to JSON strings."""
|
||||
config_data = {
|
||||
"port": 8000,
|
||||
"compilation-config": {
|
||||
"pass_config": {"fuse_allreduce_rms": True},
|
||||
},
|
||||
}
|
||||
config_file_path = tmp_path / "nested_config.yaml"
|
||||
with open(config_file_path, "w") as f:
|
||||
yaml.dump(config_data, f)
|
||||
|
||||
parser = FlexibleArgumentParser()
|
||||
processed_args = parser.load_config_file(str(config_file_path))
|
||||
|
||||
assert processed_args[processed_args.index("--port") + 1] == "8000"
|
||||
cc_value = json.loads(
|
||||
processed_args[processed_args.index("--compilation-config") + 1]
|
||||
)
|
||||
assert cc_value == {"pass_config": {"fuse_allreduce_rms": True}}
|
||||
|
||||
|
||||
def test_nested_config_end_to_end(tmp_path):
|
||||
"""Test end-to-end parsing of nested configs in YAML files."""
|
||||
config_data = {
|
||||
"compilation-config": {
|
||||
"mode": 3,
|
||||
"pass_config": {"fuse_allreduce_rms": True},
|
||||
},
|
||||
}
|
||||
config_file_path = tmp_path / "nested_config.yaml"
|
||||
with open(config_file_path, "w") as f:
|
||||
yaml.dump(config_data, f)
|
||||
|
||||
parser = FlexibleArgumentParser()
|
||||
parser.add_argument("-cc", "--compilation-config", type=json.loads)
|
||||
args = parser.parse_args(["--config", str(config_file_path)])
|
||||
|
||||
assert args.compilation_config == {
|
||||
"mode": 3,
|
||||
"pass_config": {"fuse_allreduce_rms": True},
|
||||
}
|
||||
|
||||
|
||||
def test_compilation_mode_string_values(parser):
|
||||
"""Test that -cc.mode accepts both integer and string mode values."""
|
||||
args = parser.parse_args(["-cc.mode", "0"])
|
||||
assert args.compilation_config == {"mode": 0}
|
||||
|
||||
args = parser.parse_args(["-O3"])
|
||||
assert args.optimization_level == 3
|
||||
|
||||
args = parser.parse_args(["-cc.mode=NONE"])
|
||||
assert args.compilation_config == {"mode": "NONE"}
|
||||
|
||||
args = parser.parse_args(["-cc.mode", "STOCK_TORCH_COMPILE"])
|
||||
assert args.compilation_config == {"mode": "STOCK_TORCH_COMPILE"}
|
||||
|
||||
args = parser.parse_args(["-cc.mode=DYNAMO_TRACE_ONCE"])
|
||||
assert args.compilation_config == {"mode": "DYNAMO_TRACE_ONCE"}
|
||||
|
||||
args = parser.parse_args(["-cc.mode", "VLLM_COMPILE"])
|
||||
assert args.compilation_config == {"mode": "VLLM_COMPILE"}
|
||||
|
||||
args = parser.parse_args(["-cc.mode=none"])
|
||||
assert args.compilation_config == {"mode": "none"}
|
||||
|
||||
args = parser.parse_args(["-cc.mode=vllm_compile"])
|
||||
assert args.compilation_config == {"mode": "vllm_compile"}
|
||||
|
||||
|
||||
def test_compilation_config_mode_validator():
|
||||
"""Test that CompilationConfig.mode field validator converts strings to integers."""
|
||||
from vllm.config.compilation import CompilationConfig, CompilationMode
|
||||
|
||||
config = CompilationConfig(mode=0)
|
||||
assert config.mode == CompilationMode.NONE
|
||||
|
||||
config = CompilationConfig(mode=3)
|
||||
assert config.mode == CompilationMode.VLLM_COMPILE
|
||||
|
||||
config = CompilationConfig(mode="NONE")
|
||||
assert config.mode == CompilationMode.NONE
|
||||
|
||||
config = CompilationConfig(mode="STOCK_TORCH_COMPILE")
|
||||
assert config.mode == CompilationMode.STOCK_TORCH_COMPILE
|
||||
|
||||
config = CompilationConfig(mode="DYNAMO_TRACE_ONCE")
|
||||
assert config.mode == CompilationMode.DYNAMO_TRACE_ONCE
|
||||
|
||||
config = CompilationConfig(mode="VLLM_COMPILE")
|
||||
assert config.mode == CompilationMode.VLLM_COMPILE
|
||||
|
||||
config = CompilationConfig(mode="none")
|
||||
assert config.mode == CompilationMode.NONE
|
||||
|
||||
config = CompilationConfig(mode="vllm_compile")
|
||||
assert config.mode == CompilationMode.VLLM_COMPILE
|
||||
|
||||
with pytest.raises(ValidationError, match="Invalid compilation mode"):
|
||||
CompilationConfig(mode="INVALID_MODE")
|
||||
|
||||
|
||||
def test_flat_product():
|
||||
# Check regular itertools.product behavior
|
||||
result1 = list(flat_product([1, 2, 3], ["a", "b"]))
|
||||
assert result1 == [
|
||||
(1, "a"),
|
||||
(1, "b"),
|
||||
(2, "a"),
|
||||
(2, "b"),
|
||||
(3, "a"),
|
||||
(3, "b"),
|
||||
]
|
||||
|
||||
# check that the tuples get flattened
|
||||
result2 = list(flat_product([(1, 2), (3, 4)], ["a", "b"], [(5, 6)]))
|
||||
assert result2 == [
|
||||
(1, 2, "a", 5, 6),
|
||||
(1, 2, "b", 5, 6),
|
||||
(3, 4, "a", 5, 6),
|
||||
(3, 4, "b", 5, 6),
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.utils.async_utils import merge_async_iterators
|
||||
|
||||
|
||||
async def _mock_async_iterator(idx: int):
|
||||
try:
|
||||
while True:
|
||||
yield f"item from iterator {idx}"
|
||||
await asyncio.sleep(0.1)
|
||||
except asyncio.CancelledError:
|
||||
print(f"iterator {idx} cancelled")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_async_iterators():
|
||||
iterators = [_mock_async_iterator(i) for i in range(3)]
|
||||
merged_iterator = merge_async_iterators(*iterators)
|
||||
|
||||
async def stream_output(generator: AsyncIterator[tuple[int, str]]):
|
||||
async for idx, output in generator:
|
||||
print(f"idx: {idx}, output: {output}")
|
||||
|
||||
task = asyncio.create_task(stream_output(merged_iterator))
|
||||
await asyncio.sleep(0.5)
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
for iterator in iterators:
|
||||
try:
|
||||
await asyncio.wait_for(anext(iterator), 1)
|
||||
except StopAsyncIteration:
|
||||
# All iterators should be cancelled and print this message.
|
||||
print("Iterator was cancelled normally")
|
||||
except (Exception, asyncio.CancelledError) as e:
|
||||
raise AssertionError() from e
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_async_iterators_single_closes_underlying():
|
||||
# The single-iterator fast path must close the underlying generator when
|
||||
# the merged generator is closed, matching the multi-iterator path. On the
|
||||
# buggy fast path the underlying generator is left running.
|
||||
closed = False
|
||||
|
||||
async def gen():
|
||||
nonlocal closed
|
||||
try:
|
||||
while True:
|
||||
yield "x"
|
||||
await asyncio.sleep(0.01)
|
||||
finally:
|
||||
closed = True
|
||||
|
||||
merged = merge_async_iterators(gen())
|
||||
assert await anext(merged) == (0, "x")
|
||||
await merged.aclose()
|
||||
assert closed
|
||||
@@ -0,0 +1,125 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from vllm.utils.cache import CacheInfo, LRUCache
|
||||
|
||||
|
||||
class TestLRUCache(LRUCache):
|
||||
def _on_remove(self, key, value):
|
||||
if not hasattr(self, "_remove_counter"):
|
||||
self._remove_counter = 0
|
||||
self._remove_counter += 1
|
||||
|
||||
|
||||
def test_lru_cache():
|
||||
cache = TestLRUCache(3)
|
||||
assert cache.stat() == CacheInfo(hits=0, total=0)
|
||||
assert cache.stat(delta=True) == CacheInfo(hits=0, total=0)
|
||||
|
||||
cache.put(1, 1)
|
||||
assert len(cache) == 1
|
||||
|
||||
cache.put(1, 1)
|
||||
assert len(cache) == 1
|
||||
|
||||
cache.put(2, 2)
|
||||
assert len(cache) == 2
|
||||
|
||||
cache.put(3, 3)
|
||||
assert len(cache) == 3
|
||||
assert set(cache.cache) == {1, 2, 3}
|
||||
|
||||
cache.put(4, 4)
|
||||
assert len(cache) == 3
|
||||
assert set(cache.cache) == {2, 3, 4}
|
||||
assert cache._remove_counter == 1
|
||||
|
||||
assert cache.get(2) == 2
|
||||
assert cache.stat() == CacheInfo(hits=1, total=1)
|
||||
assert cache.stat(delta=True) == CacheInfo(hits=1, total=1)
|
||||
|
||||
assert cache[2] == 2
|
||||
assert cache.stat() == CacheInfo(hits=2, total=2)
|
||||
assert cache.stat(delta=True) == CacheInfo(hits=1, total=1)
|
||||
|
||||
cache.put(5, 5)
|
||||
assert set(cache.cache) == {2, 4, 5}
|
||||
assert cache._remove_counter == 2
|
||||
|
||||
assert cache.pop(5) == 5
|
||||
assert len(cache) == 2
|
||||
assert set(cache.cache) == {2, 4}
|
||||
assert cache._remove_counter == 3
|
||||
|
||||
assert cache.get(-1) is None
|
||||
assert cache.stat() == CacheInfo(hits=2, total=3)
|
||||
assert cache.stat(delta=True) == CacheInfo(hits=0, total=1)
|
||||
|
||||
cache.pop(10)
|
||||
assert len(cache) == 2
|
||||
assert set(cache.cache) == {2, 4}
|
||||
assert cache._remove_counter == 3
|
||||
|
||||
cache.get(10)
|
||||
assert len(cache) == 2
|
||||
assert set(cache.cache) == {2, 4}
|
||||
assert cache._remove_counter == 3
|
||||
|
||||
cache.put(6, 6)
|
||||
assert len(cache) == 3
|
||||
assert set(cache.cache) == {2, 4, 6}
|
||||
assert 2 in cache
|
||||
assert 4 in cache
|
||||
assert 6 in cache
|
||||
|
||||
cache.remove_oldest()
|
||||
assert len(cache) == 2
|
||||
assert set(cache.cache) == {2, 6}
|
||||
assert cache._remove_counter == 4
|
||||
|
||||
cache.clear()
|
||||
assert len(cache) == 0
|
||||
assert cache._remove_counter == 6
|
||||
assert cache.stat() == CacheInfo(hits=0, total=0)
|
||||
assert cache.stat(delta=True) == CacheInfo(hits=0, total=0)
|
||||
|
||||
cache._remove_counter = 0
|
||||
|
||||
cache[1] = 1
|
||||
assert len(cache) == 1
|
||||
|
||||
cache[1] = 1
|
||||
assert len(cache) == 1
|
||||
|
||||
cache[2] = 2
|
||||
assert len(cache) == 2
|
||||
|
||||
cache[3] = 3
|
||||
assert len(cache) == 3
|
||||
assert set(cache.cache) == {1, 2, 3}
|
||||
|
||||
cache[4] = 4
|
||||
assert len(cache) == 3
|
||||
assert set(cache.cache) == {2, 3, 4}
|
||||
assert cache._remove_counter == 1
|
||||
assert cache[2] == 2
|
||||
|
||||
cache[5] = 5
|
||||
assert set(cache.cache) == {2, 4, 5}
|
||||
assert cache._remove_counter == 2
|
||||
|
||||
del cache[5]
|
||||
assert len(cache) == 2
|
||||
assert set(cache.cache) == {2, 4}
|
||||
assert cache._remove_counter == 3
|
||||
|
||||
cache.pop(10)
|
||||
assert len(cache) == 2
|
||||
assert set(cache.cache) == {2, 4}
|
||||
assert cache._remove_counter == 3
|
||||
|
||||
cache[6] = 6
|
||||
assert len(cache) == 3
|
||||
assert set(cache.cache) == {2, 4, 6}
|
||||
assert 2 in cache
|
||||
assert 4 in cache
|
||||
assert 6 in cache
|
||||
@@ -0,0 +1,47 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import pytest
|
||||
|
||||
from vllm.utils.collection_utils import common_prefix, swap_dict_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("inputs", "expected_output"),
|
||||
[
|
||||
([""], ""),
|
||||
(["a"], "a"),
|
||||
(["a", "b"], ""),
|
||||
(["a", "ab"], "a"),
|
||||
(["a", "ab", "b"], ""),
|
||||
(["abc", "a", "ab"], "a"),
|
||||
(["aba", "abc", "ab"], "ab"),
|
||||
],
|
||||
)
|
||||
def test_common_prefix(inputs, expected_output):
|
||||
assert common_prefix(inputs) == expected_output
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("obj", "key1", "key2"),
|
||||
[
|
||||
# Tests for both keys exist
|
||||
({1: "a", 2: "b"}, 1, 2),
|
||||
# Tests for one key does not exist
|
||||
({1: "a", 2: "b"}, 1, 3),
|
||||
# Tests for both keys do not exist
|
||||
({1: "a", 2: "b"}, 3, 4),
|
||||
],
|
||||
)
|
||||
def test_swap_dict_values(obj, key1, key2):
|
||||
original_obj = obj.copy()
|
||||
|
||||
swap_dict_values(obj, key1, key2)
|
||||
|
||||
if key1 in original_obj:
|
||||
assert obj[key2] == original_obj[key1]
|
||||
else:
|
||||
assert key2 not in obj
|
||||
if key2 in original_obj:
|
||||
assert obj[key1] == original_obj[key2]
|
||||
else:
|
||||
assert key1 not in obj
|
||||
@@ -0,0 +1,40 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# ruff: noqa
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.utils.func_utils import supports_kw
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("callable", "kw_name", "requires_kw_only", "allow_var_kwargs", "is_supported"),
|
||||
[
|
||||
# Tests for positional argument support
|
||||
(lambda foo: None, "foo", True, True, False),
|
||||
(lambda foo: None, "foo", False, True, True),
|
||||
# Tests for positional or keyword / keyword only
|
||||
(lambda foo=100: None, "foo", True, True, False),
|
||||
(lambda *, foo: None, "foo", False, True, True),
|
||||
# Tests to make sure the names of variadic params are NOT supported
|
||||
(lambda *args: None, "args", False, True, False),
|
||||
(lambda **kwargs: None, "kwargs", False, True, False),
|
||||
# Tests for if we allow var kwargs to add support
|
||||
(lambda foo: None, "something_else", False, True, False),
|
||||
(lambda foo, **kwargs: None, "something_else", False, True, True),
|
||||
(lambda foo, **kwargs: None, "kwargs", True, True, False),
|
||||
(lambda foo, **kwargs: None, "foo", True, True, False),
|
||||
],
|
||||
)
|
||||
def test_supports_kw(
|
||||
callable, kw_name, requires_kw_only, allow_var_kwargs, is_supported
|
||||
):
|
||||
assert (
|
||||
supports_kw(
|
||||
callable=callable,
|
||||
kw_name=kw_name,
|
||||
requires_kw_only=requires_kw_only,
|
||||
allow_var_kwargs=allow_var_kwargs,
|
||||
)
|
||||
== is_supported
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from vllm.utils.gc_utils import (
|
||||
GCDebugConfig,
|
||||
_compute_detailed_type,
|
||||
_compute_top_gc_collected_objects,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Normal:
|
||||
v: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListWrapper:
|
||||
vs: list[int]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.vs)
|
||||
|
||||
|
||||
def test_compute_detailed_type():
|
||||
assert (
|
||||
_compute_detailed_type(Normal(v=8))
|
||||
== "<class 'tests.utils_.test_gc_utils.Normal'>"
|
||||
)
|
||||
|
||||
assert _compute_detailed_type([1, 2, 3]) == "<class 'list'>(size:3)"
|
||||
assert _compute_detailed_type({4, 5}) == "<class 'set'>(size:2)"
|
||||
assert _compute_detailed_type({6: 7}) == "<class 'dict'>(size:1)"
|
||||
assert (
|
||||
_compute_detailed_type(ListWrapper(vs=[]))
|
||||
== "<class 'tests.utils_.test_gc_utils.ListWrapper'>(size:0)"
|
||||
)
|
||||
|
||||
|
||||
def test_compute_top_gc_collected_objects():
|
||||
objects: list[Any] = [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
[7, 8, 9],
|
||||
[10, 11, 12],
|
||||
{13, 14},
|
||||
{15: 16, 17: 18},
|
||||
Normal(v=19),
|
||||
Normal(v=20),
|
||||
Normal(v=21),
|
||||
]
|
||||
assert _compute_top_gc_collected_objects(objects, top=-1) == ""
|
||||
assert _compute_top_gc_collected_objects(objects, top=0) == ""
|
||||
assert (
|
||||
_compute_top_gc_collected_objects(objects, top=1)
|
||||
== " 4:<class 'list'>(size:3)"
|
||||
)
|
||||
assert _compute_top_gc_collected_objects(objects, top=2) == "\n".join(
|
||||
[
|
||||
" 4:<class 'list'>(size:3)",
|
||||
" 3:<class 'tests.utils_.test_gc_utils.Normal'>",
|
||||
]
|
||||
)
|
||||
assert _compute_top_gc_collected_objects(objects, top=3) == "\n".join(
|
||||
[
|
||||
" 4:<class 'list'>(size:3)",
|
||||
" 3:<class 'tests.utils_.test_gc_utils.Normal'>",
|
||||
" 1:<class 'set'>(size:2)",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_gc_debug_config():
|
||||
assert not GCDebugConfig(None).enabled
|
||||
assert not GCDebugConfig("").enabled
|
||||
assert not GCDebugConfig("0").enabled
|
||||
|
||||
config = GCDebugConfig("1")
|
||||
assert config.enabled
|
||||
assert config.top_objects == -1
|
||||
|
||||
config = GCDebugConfig('{"top_objects":5}')
|
||||
assert config.enabled
|
||||
assert config.top_objects == 5
|
||||
@@ -0,0 +1,61 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import vllm.utils.gpu_sync_debug as gsd
|
||||
from vllm.utils.gpu_sync_debug import (
|
||||
SYNC_ERROR_MESSAGE,
|
||||
gpu_sync_allowed,
|
||||
with_gpu_sync_check,
|
||||
)
|
||||
|
||||
from ..utils import create_new_process_for_each_test
|
||||
|
||||
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
|
||||
|
||||
def _no_sync():
|
||||
# Pure on-GPU compute, no implicit CPU sync...
|
||||
x = torch.ones(4, device="cuda") + 1
|
||||
# ...plus a sync that we explicitly allow.
|
||||
with gpu_sync_allowed():
|
||||
return x.cpu()
|
||||
|
||||
|
||||
def _causes_sync():
|
||||
x = torch.ones(4, device="cuda")
|
||||
# An allowed sync (suppressed)...
|
||||
with gpu_sync_allowed():
|
||||
x.cpu()
|
||||
# ...then an un-allowed sync that should trip the check.
|
||||
return x.cpu()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["warn", "error"])
|
||||
@create_new_process_for_each_test()
|
||||
def test_with_env_set(monkeypatch, mode):
|
||||
# Env set + gate flipped on: the unguarded sync is detected.
|
||||
monkeypatch.setenv("VLLM_GPU_SYNC_CHECK", mode)
|
||||
monkeypatch.setattr(gsd, "_sync_check_enabled", True)
|
||||
|
||||
# Guarded syncs always pass.
|
||||
with_gpu_sync_check(_no_sync)()
|
||||
|
||||
if mode == "error":
|
||||
# "error" mode turns the stray sync into a RuntimeError.
|
||||
with pytest.raises(RuntimeError, match=SYNC_ERROR_MESSAGE):
|
||||
with_gpu_sync_check(_causes_sync)()
|
||||
else:
|
||||
# "warn" mode only warns, so the call still succeeds.
|
||||
with_gpu_sync_check(_causes_sync)()
|
||||
|
||||
|
||||
@create_new_process_for_each_test()
|
||||
def test_without_env_set(monkeypatch):
|
||||
# Env unset: the decorator is a pass-through, no sync is detected.
|
||||
monkeypatch.delenv("VLLM_GPU_SYNC_CHECK", raising=False)
|
||||
monkeypatch.setattr(gsd, "_sync_check_enabled", True)
|
||||
|
||||
with_gpu_sync_check(_no_sync)()
|
||||
with_gpu_sync_check(_causes_sync)()
|
||||
@@ -0,0 +1,25 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import hashlib
|
||||
import pickle
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.utils.hashing import sha256
|
||||
|
||||
|
||||
@pytest.mark.parametrize("input", [(), ("abc",), (None,), (None, bool, [1, 2, 3])])
|
||||
def test_sha256(input: tuple):
|
||||
digest = sha256(input)
|
||||
assert digest is not None
|
||||
assert isinstance(digest, bytes)
|
||||
assert digest != b""
|
||||
|
||||
input_bytes = pickle.dumps(input, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
assert digest == hashlib.sha256(input_bytes).digest()
|
||||
|
||||
# hashing again, returns the same value
|
||||
assert digest == sha256(input)
|
||||
|
||||
# hashing different input, returns different value
|
||||
assert digest != sha256(input + (1,))
|
||||
@@ -0,0 +1,103 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.utils.import_utils import PlaceholderModule, _has_module
|
||||
|
||||
|
||||
def _raises_module_not_found():
|
||||
return pytest.raises(ModuleNotFoundError, match="No module named")
|
||||
|
||||
|
||||
def test_placeholder_module_error_handling():
|
||||
placeholder = PlaceholderModule("placeholder_1234")
|
||||
|
||||
with _raises_module_not_found():
|
||||
int(placeholder)
|
||||
|
||||
with _raises_module_not_found():
|
||||
placeholder()
|
||||
|
||||
with _raises_module_not_found():
|
||||
_ = placeholder.some_attr
|
||||
|
||||
with _raises_module_not_found():
|
||||
# Test conflict with internal __name attribute
|
||||
_ = placeholder.name
|
||||
|
||||
# OK to print the placeholder or use it in a f-string
|
||||
_ = repr(placeholder)
|
||||
_ = str(placeholder)
|
||||
|
||||
# No error yet; only error when it is used downstream
|
||||
placeholder_attr = placeholder.placeholder_attr("attr")
|
||||
|
||||
with _raises_module_not_found():
|
||||
int(placeholder_attr)
|
||||
|
||||
with _raises_module_not_found():
|
||||
placeholder_attr()
|
||||
|
||||
with _raises_module_not_found():
|
||||
_ = placeholder_attr.some_attr
|
||||
|
||||
with _raises_module_not_found():
|
||||
# Test conflict with internal __module attribute
|
||||
_ = placeholder_attr.module
|
||||
|
||||
|
||||
class TestHasModule:
|
||||
"""Tests for _has_module with trial import verification."""
|
||||
|
||||
def setup_method(self):
|
||||
# Clear the @cache between tests so each test gets a fresh call
|
||||
_has_module.cache_clear()
|
||||
|
||||
def test_returns_true_for_importable_stdlib_module(self):
|
||||
assert _has_module("json") is True
|
||||
|
||||
def test_returns_false_for_nonexistent_module(self):
|
||||
assert _has_module("nonexistent_module_xyz_12345") is False
|
||||
|
||||
def test_returns_false_when_find_spec_succeeds_but_import_fails(self):
|
||||
"""Simulate a native extension whose shared library is missing.
|
||||
|
||||
``find_spec`` finds the package on disk, but the actual import
|
||||
raises ``ImportError`` (e.g. missing ``libcudart.so``).
|
||||
"""
|
||||
fake_spec = MagicMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"vllm.utils.import_utils.importlib.util.find_spec",
|
||||
return_value=fake_spec,
|
||||
),
|
||||
patch(
|
||||
"vllm.utils.import_utils.importlib.import_module",
|
||||
side_effect=ImportError(
|
||||
"libcudart.so.12: cannot open shared object file"
|
||||
),
|
||||
),
|
||||
):
|
||||
assert _has_module("fake_native_ext") is False
|
||||
|
||||
def test_returns_false_when_find_spec_raises(self):
|
||||
"""``find_spec`` itself can raise for dotted names whose parent package
|
||||
fails to import. This should be treated as the module being unavailable.
|
||||
"""
|
||||
with patch(
|
||||
"vllm.utils.import_utils.importlib.util.find_spec",
|
||||
side_effect=ModuleNotFoundError("No module named 'fake_parent'"),
|
||||
):
|
||||
assert _has_module("fake_parent.child") is False
|
||||
|
||||
def test_result_is_cached(self):
|
||||
"""Verify the @cache decorator prevents repeated imports."""
|
||||
_has_module("json") # prime the cache
|
||||
|
||||
with patch("vllm.utils.import_utils.importlib.util.find_spec") as mock_spec:
|
||||
result = _has_module("json") # should hit cache
|
||||
mock_spec.assert_not_called()
|
||||
assert result is True
|
||||
@@ -0,0 +1,32 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from vllm.utils.jsontree import json_count_leaves
|
||||
|
||||
|
||||
def test_json_count_leaves():
|
||||
"""Test json_count_leaves function from jsontree utility."""
|
||||
|
||||
# Single leaf values
|
||||
assert json_count_leaves(42) == 1
|
||||
assert json_count_leaves("hello") == 1
|
||||
assert json_count_leaves(None) == 1
|
||||
|
||||
# Empty containers
|
||||
assert json_count_leaves([]) == 0
|
||||
assert json_count_leaves({}) == 0
|
||||
assert json_count_leaves(()) == 0
|
||||
|
||||
# Flat structures
|
||||
assert json_count_leaves([1, 2, 3]) == 3
|
||||
assert json_count_leaves({"a": 1, "b": 2}) == 2
|
||||
assert json_count_leaves((1, 2, 3)) == 3
|
||||
|
||||
# Nested structures
|
||||
nested_dict = {"a": 1, "b": {"c": 2, "d": 3}}
|
||||
assert json_count_leaves(nested_dict) == 3
|
||||
|
||||
nested_list = [1, [2, 3], 4]
|
||||
assert json_count_leaves(nested_list) == 4
|
||||
|
||||
mixed_nested = {"list": [1, 2], "dict": {"x": 3}, "value": 4}
|
||||
assert json_count_leaves(mixed_nested) == 4
|
||||
@@ -0,0 +1,133 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import torch
|
||||
from vllm_test_utils.monitor import monitor
|
||||
|
||||
from vllm.utils.mem_utils import MemorySnapshot, memory_profiling
|
||||
|
||||
from ..utils import create_new_process_for_each_test
|
||||
|
||||
|
||||
@create_new_process_for_each_test()
|
||||
def test_memory_profiling():
|
||||
# Fake out some model loading + inference memory usage to test profiling
|
||||
# Memory used by other processes will show up as cuda usage outside of torch
|
||||
from vllm.distributed.device_communicators.cuda_wrapper import CudaRTLibrary
|
||||
|
||||
lib = CudaRTLibrary()
|
||||
# 512 MiB allocation outside of this instance
|
||||
handle1 = lib.cudaMalloc(512 * 1024 * 1024)
|
||||
|
||||
# Warm up PyTorch's CUDA/ROCm context so that its internal initialization
|
||||
# overhead (streams, cuBLAS handles, etc.) is included in the baseline and
|
||||
# does not inflate non-torch increase which is larger on ROCm than on CUDA
|
||||
_warmup = torch.zeros(1, device="cuda")
|
||||
del _warmup
|
||||
torch.accelerator.empty_cache()
|
||||
|
||||
baseline_snapshot = MemorySnapshot()
|
||||
|
||||
# load weights
|
||||
|
||||
weights = torch.randn(128, 1024, 1024, device="cuda", dtype=torch.float32)
|
||||
|
||||
weights_memory = 128 * 1024 * 1024 * 4 # 512 MiB
|
||||
|
||||
def measure_current_non_torch():
|
||||
free, total = torch.accelerator.get_memory_info()
|
||||
current_used = total - free
|
||||
current_torch = torch.accelerator.memory_reserved()
|
||||
current_non_torch = current_used - current_torch
|
||||
return current_non_torch
|
||||
|
||||
with (
|
||||
memory_profiling(
|
||||
baseline_snapshot=baseline_snapshot, weights_memory=weights_memory
|
||||
) as result,
|
||||
monitor(measure_current_non_torch) as monitored_values,
|
||||
):
|
||||
# make a memory spike, 1 GiB
|
||||
spike = torch.randn(256, 1024, 1024, device="cuda", dtype=torch.float32)
|
||||
del spike
|
||||
|
||||
# Add some extra non-torch memory 256 MiB (simulate NCCL)
|
||||
handle2 = lib.cudaMalloc(256 * 1024 * 1024)
|
||||
|
||||
# this is an analytic value, it is exact,
|
||||
# we only have 256 MiB non-torch memory increase
|
||||
measured_diff = monitored_values.values[-1] - monitored_values.values[0]
|
||||
assert measured_diff == 256 * 1024 * 1024
|
||||
|
||||
# Check that the memory usage is within 5% of the expected values
|
||||
# 5% tolerance is caused by cuda runtime.
|
||||
# we cannot control cuda runtime in the granularity of bytes,
|
||||
# which causes a small error (<10 MiB in practice)
|
||||
non_torch_ratio = result.non_torch_increase / (256 * 1024 * 1024) # noqa
|
||||
assert abs(non_torch_ratio - 1) <= 0.05
|
||||
assert result.torch_peak_increase == 1024 * 1024 * 1024
|
||||
del weights
|
||||
lib.cudaFree(handle1)
|
||||
lib.cudaFree(handle2)
|
||||
|
||||
|
||||
def test_memory_snapshot_uses_psutil_on_integrated_gpu():
|
||||
"""On integrated (UMA) GPUs, free_memory should come from psutil."""
|
||||
mock_cuda_free = 40 * 1024**3
|
||||
mock_cuda_total = 120 * 1024**3
|
||||
mock_psutil_available = 100 * 1024**3
|
||||
|
||||
with (
|
||||
patch("vllm.utils.mem_utils.current_platform") as mock_platform,
|
||||
patch("vllm.utils.mem_utils.psutil") as mock_psutil,
|
||||
patch("torch.accelerator") as mock_accelerator,
|
||||
):
|
||||
mock_accelerator.get_memory_info.return_value = (
|
||||
mock_cuda_free,
|
||||
mock_cuda_total,
|
||||
)
|
||||
mock_platform.is_integrated_gpu.return_value = True
|
||||
mock_platform.memory_stats.return_value = {
|
||||
"allocated_bytes.all.peak": 0,
|
||||
}
|
||||
mock_accelerator.memory_reserved.return_value = 0
|
||||
mock_accelerator.current_device = lambda: "cuda:0"
|
||||
|
||||
mock_vmem = MagicMock()
|
||||
mock_vmem.available = mock_psutil_available
|
||||
mock_psutil.virtual_memory.return_value = mock_vmem
|
||||
|
||||
snapshot = MemorySnapshot(device="cuda:0")
|
||||
|
||||
assert snapshot.free_memory == mock_psutil_available
|
||||
assert snapshot.total_memory == mock_cuda_total
|
||||
mock_psutil.virtual_memory.assert_called_once()
|
||||
|
||||
|
||||
def test_memory_snapshot_uses_cuda_on_discrete_gpu():
|
||||
"""On discrete GPUs, free_memory should come from accelerator get_memory_info."""
|
||||
mock_cuda_free = 70 * 1024**3
|
||||
mock_cuda_total = 80 * 1024**3
|
||||
|
||||
with (
|
||||
patch("vllm.utils.mem_utils.current_platform") as mock_platform,
|
||||
patch("vllm.utils.mem_utils.psutil") as mock_psutil,
|
||||
patch("torch.accelerator") as mock_accelerator,
|
||||
):
|
||||
mock_accelerator.get_memory_info.return_value = (
|
||||
mock_cuda_free,
|
||||
mock_cuda_total,
|
||||
)
|
||||
mock_platform.is_integrated_gpu.return_value = False
|
||||
mock_accelerator.memory_stats.return_value = {
|
||||
"allocated_bytes.all.peak": 0,
|
||||
}
|
||||
mock_accelerator.memory_reserved.return_value = 0
|
||||
mock_accelerator.current_device = lambda: "cuda:0"
|
||||
|
||||
snapshot = MemorySnapshot(device="cuda:0")
|
||||
|
||||
assert snapshot.free_memory == mock_cuda_free
|
||||
assert snapshot.total_memory == mock_cuda_total
|
||||
mock_psutil.virtual_memory.assert_not_called()
|
||||
@@ -0,0 +1,146 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
import zmq
|
||||
|
||||
from vllm.utils.network_utils import (
|
||||
get_open_port,
|
||||
get_open_ports_list,
|
||||
get_tcp_uri,
|
||||
join_host_port,
|
||||
make_zmq_path,
|
||||
make_zmq_socket,
|
||||
split_host_port,
|
||||
split_zmq_path,
|
||||
)
|
||||
|
||||
|
||||
def test_get_open_port(monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("VLLM_PORT", "5678")
|
||||
# make sure we can get multiple ports, even if the env var is set
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s1:
|
||||
s1.bind(("localhost", get_open_port()))
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s2:
|
||||
s2.bind(("localhost", get_open_port()))
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s3:
|
||||
s3.bind(("localhost", get_open_port()))
|
||||
|
||||
|
||||
def test_get_open_ports_list_with_vllm_port(monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("VLLM_PORT", "5678")
|
||||
ports = get_open_ports_list(5)
|
||||
assert len(ports) == 5
|
||||
assert len(set(ports)) == 5, "ports must be unique"
|
||||
|
||||
# verify every port is actually bindable
|
||||
sockets = []
|
||||
try:
|
||||
for p in ports:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(("localhost", p))
|
||||
sockets.append(s)
|
||||
finally:
|
||||
for s in sockets:
|
||||
s.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected",
|
||||
[
|
||||
("ipc://some_path", ("ipc", "some_path", "")),
|
||||
("tcp://127.0.0.1:5555", ("tcp", "127.0.0.1", "5555")),
|
||||
("tcp://[::1]:5555", ("tcp", "::1", "5555")), # IPv6 address
|
||||
("inproc://some_identifier", ("inproc", "some_identifier", "")),
|
||||
],
|
||||
)
|
||||
def test_split_zmq_path(path, expected):
|
||||
assert split_zmq_path(path) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_path",
|
||||
[
|
||||
"invalid_path", # Missing scheme
|
||||
"tcp://127.0.0.1", # Missing port
|
||||
"tcp://[::1]", # Missing port for IPv6
|
||||
"tcp://:5555", # Missing host
|
||||
],
|
||||
)
|
||||
def test_split_zmq_path_invalid(invalid_path):
|
||||
with pytest.raises(ValueError):
|
||||
split_zmq_path(invalid_path)
|
||||
|
||||
|
||||
def test_make_zmq_socket_ipv6():
|
||||
# Check if IPv6 is supported by trying to create an IPv6 socket
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
sock.close()
|
||||
except OSError:
|
||||
pytest.skip("IPv6 is not supported on this system")
|
||||
|
||||
ctx = zmq.Context()
|
||||
ipv6_path = "tcp://[::]:5555" # IPv6 loopback address
|
||||
socket_type = zmq.REP # Example socket type
|
||||
|
||||
# Create the socket
|
||||
zsock: zmq.Socket = make_zmq_socket(ctx, ipv6_path, socket_type)
|
||||
|
||||
# Verify that the IPV6 option is set
|
||||
assert zsock.getsockopt(zmq.IPV6) == 1, (
|
||||
"IPV6 option should be enabled for IPv6 addresses"
|
||||
)
|
||||
|
||||
# Clean up
|
||||
zsock.close()
|
||||
ctx.term()
|
||||
|
||||
|
||||
def test_make_zmq_path():
|
||||
assert make_zmq_path("tcp", "127.0.0.1", "5555") == "tcp://127.0.0.1:5555"
|
||||
assert make_zmq_path("tcp", "::1", "5555") == "tcp://[::1]:5555"
|
||||
|
||||
|
||||
def test_get_tcp_uri():
|
||||
assert get_tcp_uri("127.0.0.1", 5555) == "tcp://127.0.0.1:5555"
|
||||
assert get_tcp_uri("::1", 5555) == "tcp://[::1]:5555"
|
||||
|
||||
|
||||
def test_split_host_port():
|
||||
# valid ipv4
|
||||
assert split_host_port("127.0.0.1:5555") == ("127.0.0.1", 5555)
|
||||
# invalid ipv4
|
||||
with pytest.raises(ValueError):
|
||||
# multi colon
|
||||
assert split_host_port("127.0.0.1::5555")
|
||||
with pytest.raises(ValueError):
|
||||
# tailing colon
|
||||
assert split_host_port("127.0.0.1:5555:")
|
||||
with pytest.raises(ValueError):
|
||||
# no colon
|
||||
assert split_host_port("127.0.0.15555")
|
||||
with pytest.raises(ValueError):
|
||||
# none int port
|
||||
assert split_host_port("127.0.0.1:5555a")
|
||||
|
||||
# valid ipv6
|
||||
assert split_host_port("[::1]:5555") == ("::1", 5555)
|
||||
# invalid ipv6
|
||||
with pytest.raises(ValueError):
|
||||
# multi colon
|
||||
assert split_host_port("[::1]::5555")
|
||||
with pytest.raises(IndexError):
|
||||
# no colon
|
||||
assert split_host_port("[::1]5555")
|
||||
with pytest.raises(ValueError):
|
||||
# none int port
|
||||
assert split_host_port("[::1]:5555a")
|
||||
|
||||
|
||||
def test_join_host_port():
|
||||
assert join_host_port("127.0.0.1", 5555) == "127.0.0.1:5555"
|
||||
assert join_host_port("::1", 5555) == "[::1]:5555"
|
||||
@@ -0,0 +1,512 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.config import ParallelConfig
|
||||
from vllm.utils import numa_utils
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _disable_pct_by_default(monkeypatch):
|
||||
"""Force PCT detection OFF unless a test opts in via ``_patch_pct_gates``.
|
||||
|
||||
The CI / dev machines themselves can be Xeon 6776P with PCT enabled, so a
|
||||
plain ``cache_clear`` would let the gate auto-detect ``True`` from the
|
||||
live filesystem and silently re-route "baseline" tests through the PCT
|
||||
path. Stub ``/proc/cpuinfo`` and ``acpi_cppc/highest_perf`` to a state
|
||||
that fails the gate; ``_patch_pct_gates`` re-stubs on top when needed.
|
||||
"""
|
||||
from io import StringIO
|
||||
|
||||
real_open = open
|
||||
|
||||
def _no_pct_open(path, *args, **kwargs):
|
||||
if path == numa_utils._PROC_CPUINFO_PATH:
|
||||
return StringIO("processor\t: 0\nmodel name\t: Generic Test CPU\n")
|
||||
if path == numa_utils._PCT_HIGHEST_PERF_PATH:
|
||||
raise OSError("PCT disabled by autouse fixture")
|
||||
return real_open(path, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr("builtins.open", _no_pct_open)
|
||||
numa_utils._pct_sku_config.cache_clear()
|
||||
yield
|
||||
numa_utils._pct_sku_config.cache_clear()
|
||||
|
||||
|
||||
def _make_config(**parallel_kwargs):
|
||||
parallel_defaults = dict(
|
||||
numa_bind=False,
|
||||
numa_bind_nodes=None,
|
||||
numa_bind_cpus=None,
|
||||
distributed_executor_backend="mp",
|
||||
data_parallel_backend="mp",
|
||||
nnodes_within_dp=1,
|
||||
data_parallel_rank_local=0,
|
||||
data_parallel_index=0,
|
||||
pipeline_parallel_size=1,
|
||||
tensor_parallel_size=1,
|
||||
)
|
||||
parallel_defaults.update(parallel_kwargs)
|
||||
parallel_config = SimpleNamespace(**parallel_defaults)
|
||||
return SimpleNamespace(parallel_config=parallel_config)
|
||||
|
||||
|
||||
def test_get_numactl_args_with_node_binding():
|
||||
vllm_config = _make_config(numa_bind=True, numa_bind_nodes=[0, 1])
|
||||
assert (
|
||||
numa_utils._get_numactl_worker_args(vllm_config.parallel_config, local_rank=1)
|
||||
== "--cpunodebind=1 --membind=1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_with_cpu_binding():
|
||||
vllm_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0, 1],
|
||||
numa_bind_cpus=["0-3", "4-7"],
|
||||
)
|
||||
assert (
|
||||
numa_utils._get_numactl_worker_args(vllm_config.parallel_config, local_rank=1)
|
||||
== "--physcpubind=4-7 --membind=1"
|
||||
)
|
||||
|
||||
|
||||
def _patch_pct_gates(
|
||||
monkeypatch,
|
||||
*,
|
||||
model_match: bool,
|
||||
highest_perf: int | None,
|
||||
cpulist: str | None = "0-31,64-95",
|
||||
cpulist_by_node: dict[int, str | None] | None = None,
|
||||
sku: str = "6776P",
|
||||
):
|
||||
"""Force `_pct_sku_config` and node cpulist read to deterministic state.
|
||||
|
||||
``cpulist`` is the default returned for any node not present in
|
||||
``cpulist_by_node``. ``cpulist_by_node`` lets a test return different
|
||||
cpulists for different NUMA nodes (e.g. node 0 vs node 1). ``sku`` lets
|
||||
the test pick which Granite Rapids SKU appears in the fake
|
||||
``/proc/cpuinfo`` ``model name`` (only used when ``model_match=True``).
|
||||
"""
|
||||
import pathlib
|
||||
from io import StringIO
|
||||
|
||||
import regex as re
|
||||
|
||||
cpuinfo = (
|
||||
f"processor\t: 0\nmodel name\t: Intel(R) Xeon(R) Platinum {sku} CPU @ 2.40GHz\n"
|
||||
if model_match
|
||||
else "processor\t: 0\nmodel name\t: Intel(R) Xeon(R) Platinum 8480+\n"
|
||||
)
|
||||
|
||||
real_open = open
|
||||
|
||||
def fake_open(path, *args, **kwargs):
|
||||
if path == numa_utils._PROC_CPUINFO_PATH:
|
||||
return StringIO(cpuinfo)
|
||||
if path == numa_utils._PCT_HIGHEST_PERF_PATH:
|
||||
if highest_perf is None:
|
||||
raise OSError("missing")
|
||||
return StringIO(f"{highest_perf}\n")
|
||||
return real_open(path, *args, **kwargs)
|
||||
|
||||
real_read_text = pathlib.Path.read_text
|
||||
cpulist_by_node = cpulist_by_node or {}
|
||||
|
||||
def fake_read_text(self, *args, **kwargs):
|
||||
path_str = str(self)
|
||||
if path_str.endswith("/cpulist") and "/sys/devices/system/node" in path_str:
|
||||
match = re.search(r"/node(\d+)/cpulist$", path_str)
|
||||
if match:
|
||||
node_id = int(match.group(1))
|
||||
if node_id in cpulist_by_node:
|
||||
val = cpulist_by_node[node_id]
|
||||
if val is None:
|
||||
raise OSError(f"missing cpulist for node{node_id}")
|
||||
return val
|
||||
if cpulist is None:
|
||||
raise OSError("missing cpulist")
|
||||
return cpulist
|
||||
return real_read_text(self, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr("builtins.open", fake_open)
|
||||
monkeypatch.setattr("pathlib.Path.read_text", fake_read_text)
|
||||
numa_utils._pct_sku_config.cache_clear()
|
||||
|
||||
|
||||
def test_pct_binding_filters_cpus(monkeypatch):
|
||||
_patch_pct_gates(monkeypatch, model_match=True, highest_perf=46)
|
||||
assert numa_utils._maybe_get_pct_cpu_binding([0]) == [0, 1, 16, 17, 64, 65, 80, 81]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sku,expected_cpus",
|
||||
[
|
||||
# 64-core SKUs (stride 16): cpus from "0-31,64-95" with cpu_id % 16
|
||||
# in (0, 1) -> 0, 1, 16, 17, 64, 65, 80, 81.
|
||||
("6776P", [0, 1, 16, 17, 64, 65, 80, 81]),
|
||||
("6774P", [0, 1, 16, 17, 64, 65, 80, 81]),
|
||||
# 72-core SKU (stride 18): cpus from "0-31,64-95" with cpu_id % 18
|
||||
# in (0, 1) -> 0, 1, 18, 19, 72, 73, 90, 91.
|
||||
("6962P", [0, 1, 18, 19, 72, 73, 90, 91]),
|
||||
],
|
||||
)
|
||||
def test_pct_binding_fires_on_every_capable_sku(monkeypatch, sku, expected_cpus):
|
||||
"""Each SKU in ``_PCT_CAPABLE_SKUS`` engages the gate at its own
|
||||
expected ``highest_perf`` and uses its own priority-core stride."""
|
||||
sku_config = numa_utils._PCT_CAPABLE_SKUS[sku]
|
||||
_patch_pct_gates(
|
||||
monkeypatch,
|
||||
model_match=True,
|
||||
highest_perf=sku_config.highest_perf,
|
||||
sku=sku,
|
||||
)
|
||||
assert numa_utils._maybe_get_pct_cpu_binding([0]) == expected_cpus
|
||||
|
||||
|
||||
def test_pct_binding_fails_closed_when_sku_perf_mismatch(monkeypatch):
|
||||
"""6962P with 6776P's highest_perf (46 vs expected 44) must fail closed."""
|
||||
_patch_pct_gates(monkeypatch, model_match=True, highest_perf=46, sku="6962P")
|
||||
assert numa_utils._maybe_get_pct_cpu_binding([0]) is None
|
||||
|
||||
|
||||
def test_pct_binding_disabled_when_cpu_model_mismatch(monkeypatch):
|
||||
_patch_pct_gates(monkeypatch, model_match=False, highest_perf=46)
|
||||
assert numa_utils._maybe_get_pct_cpu_binding([0]) is None
|
||||
|
||||
|
||||
def test_pct_binding_disabled_when_highest_perf_does_not_match(monkeypatch):
|
||||
_patch_pct_gates(monkeypatch, model_match=True, highest_perf=42)
|
||||
assert numa_utils._maybe_get_pct_cpu_binding([0]) is None
|
||||
|
||||
|
||||
def test_pct_binding_disabled_when_files_missing(monkeypatch):
|
||||
_patch_pct_gates(monkeypatch, model_match=True, highest_perf=None)
|
||||
assert numa_utils._maybe_get_pct_cpu_binding([0]) is None
|
||||
|
||||
|
||||
def test_pct_binding_returns_none_when_node_cpulist_filter_empty(monkeypatch):
|
||||
_patch_pct_gates(
|
||||
monkeypatch,
|
||||
model_match=True,
|
||||
highest_perf=46,
|
||||
cpulist="2-15,18-31",
|
||||
)
|
||||
assert numa_utils._maybe_get_pct_cpu_binding([0]) is None
|
||||
|
||||
|
||||
def test_pct_binding_returns_none_when_node_cpulist_missing(monkeypatch):
|
||||
_patch_pct_gates(monkeypatch, model_match=True, highest_perf=46, cpulist=None)
|
||||
assert numa_utils._maybe_get_pct_cpu_binding([0]) is None
|
||||
|
||||
|
||||
def test_get_numactl_args_uses_pct_when_user_did_not_specify_cpus(monkeypatch):
|
||||
_patch_pct_gates(monkeypatch, model_match=True, highest_perf=46)
|
||||
vllm_config = _make_config(numa_bind=True, numa_bind_nodes=[0, 1])
|
||||
assert (
|
||||
numa_utils._get_numactl_worker_args(vllm_config.parallel_config, local_rank=1)
|
||||
== "--physcpubind=0,1,16,17,64,65,80,81 --membind=1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_engine_core_baseline_single_node_shard():
|
||||
"""Baseline (no PCT): single-NUMA shard -> single-node bind."""
|
||||
vllm_config = _make_config(numa_bind=True, numa_bind_nodes=[0, 1])
|
||||
assert (
|
||||
numa_utils._get_numactl_enginecore_args(
|
||||
vllm_config.parallel_config, local_rank=0
|
||||
)
|
||||
== "--cpunodebind=0 --membind=0"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_engine_core_baseline_spans_shard_numa_nodes():
|
||||
"""Baseline (no PCT): a TP=4 shard spanning both NUMA nodes -> bind to both."""
|
||||
vllm_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0, 0, 1, 1],
|
||||
tensor_parallel_size=4,
|
||||
)
|
||||
assert (
|
||||
numa_utils._get_numactl_enginecore_args(
|
||||
vllm_config.parallel_config, local_rank=0
|
||||
)
|
||||
== "--cpunodebind=0,1 --membind=0,1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_engine_core_pct_spans_shard_numa_nodes(monkeypatch):
|
||||
"""PCT: EngineCore for a multi-NUMA shard binds to the union of priority
|
||||
cores across all shard nodes, so worker `--physcpubind` is always a
|
||||
subset of EngineCore's `cpus_allowed`."""
|
||||
_patch_pct_gates(
|
||||
monkeypatch,
|
||||
model_match=True,
|
||||
highest_perf=46,
|
||||
cpulist_by_node={0: "0-31,128-159", 1: "64-95,192-223"},
|
||||
)
|
||||
vllm_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0, 0, 1, 1],
|
||||
tensor_parallel_size=4,
|
||||
)
|
||||
assert numa_utils._get_numactl_enginecore_args(
|
||||
vllm_config.parallel_config, local_rank=0
|
||||
) == (
|
||||
"--physcpubind="
|
||||
"0,1,16,17,64,65,80,81,128,129,144,145,192,193,208,209"
|
||||
" --membind=0,1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_engine_core_pct_dp_shard_picks_local_nodes(monkeypatch):
|
||||
"""With DP=2, each shard's EngineCore binds only to its own NUMA nodes."""
|
||||
_patch_pct_gates(
|
||||
monkeypatch,
|
||||
model_match=True,
|
||||
highest_perf=46,
|
||||
cpulist_by_node={0: "0-31,128-159", 1: "64-95,192-223"},
|
||||
)
|
||||
vllm_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0, 0, 1, 1],
|
||||
tensor_parallel_size=2,
|
||||
data_parallel_rank_local=1,
|
||||
)
|
||||
# Shard 1 owns gpu_indices 2 and 3 -> nodes [1, 1] -> {1}.
|
||||
assert (
|
||||
numa_utils._get_numactl_enginecore_args(
|
||||
vllm_config.parallel_config, local_rank=0
|
||||
)
|
||||
== "--physcpubind=64,65,80,81,192,193,208,209 --membind=1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_engine_core_pct_external_launcher_spans_local_nodes(
|
||||
monkeypatch,
|
||||
):
|
||||
"""external_launcher (or multi-node-within-DP, or Ray) hits the
|
||||
fallback branch. EngineCore must still span every local NUMA node
|
||||
so it can mp-spawn its local workers without ``--physcpubind``
|
||||
strict-validation failures."""
|
||||
_patch_pct_gates(
|
||||
monkeypatch,
|
||||
model_match=True,
|
||||
highest_perf=46,
|
||||
cpulist_by_node={0: "0-31,128-159", 1: "64-95,192-223"},
|
||||
)
|
||||
vllm_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0, 0, 0, 0, 1, 1, 1, 1],
|
||||
distributed_executor_backend="external_launcher",
|
||||
tensor_parallel_size=8,
|
||||
)
|
||||
assert numa_utils._get_numactl_enginecore_args(
|
||||
vllm_config.parallel_config, local_rank=0
|
||||
) == (
|
||||
"--physcpubind="
|
||||
"0,1,16,17,64,65,80,81,128,129,144,145,192,193,208,209"
|
||||
" --membind=0,1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_engine_core_baseline_multi_node_within_dp_spans_locals():
|
||||
"""Multi-node-within-DP fallback: bind EngineCore to all local NUMA
|
||||
nodes that the visible ``numa_bind_nodes`` reference."""
|
||||
vllm_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0, 0, 0, 0, 1, 1, 1, 1],
|
||||
nnodes_within_dp=2,
|
||||
tensor_parallel_size=8,
|
||||
)
|
||||
assert (
|
||||
numa_utils._get_numactl_enginecore_args(
|
||||
vllm_config.parallel_config, local_rank=0
|
||||
)
|
||||
== "--cpunodebind=0,1 --membind=0,1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_engine_core_skips_user_cpu_list(monkeypatch):
|
||||
"""EngineCore ignores ``--numa-bind-cpus``.
|
||||
|
||||
Those are per-worker lists; binding EngineCore to any of them would
|
||||
shrink its ``cpus_allowed`` below the strict-superset workers'
|
||||
``--physcpubind`` spawns need. We fall back to ``--cpunodebind`` over
|
||||
the shard's NUMA nodes instead. PCT auto-detect is also bypassed when
|
||||
the user is explicit (its priority-core union may not be a superset
|
||||
of the user's per-worker cores)."""
|
||||
_patch_pct_gates(monkeypatch, model_match=True, highest_perf=46)
|
||||
vllm_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0, 0, 1, 1],
|
||||
numa_bind_cpus=["0-3", "4-7", "64-67", "68-71"],
|
||||
tensor_parallel_size=4,
|
||||
)
|
||||
assert (
|
||||
numa_utils._get_numactl_enginecore_args(
|
||||
vllm_config.parallel_config, local_rank=0
|
||||
)
|
||||
== "--cpunodebind=0,1 --membind=0,1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_user_cpus_override_pct(monkeypatch):
|
||||
_patch_pct_gates(monkeypatch, model_match=True, highest_perf=46)
|
||||
vllm_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0, 1],
|
||||
numa_bind_cpus=["0-3", "4-7"],
|
||||
)
|
||||
assert (
|
||||
numa_utils._get_numactl_worker_args(vllm_config.parallel_config, local_rank=1)
|
||||
== "--physcpubind=4-7 --membind=1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_uses_dp_offset():
|
||||
vllm_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0, 0, 1, 1],
|
||||
data_parallel_rank_local=1,
|
||||
pipeline_parallel_size=1,
|
||||
tensor_parallel_size=2,
|
||||
)
|
||||
assert (
|
||||
numa_utils._get_numactl_worker_args(vllm_config.parallel_config, local_rank=1)
|
||||
== "--cpunodebind=1 --membind=1"
|
||||
)
|
||||
|
||||
|
||||
def test_get_numactl_args_requires_detectable_nodes(monkeypatch):
|
||||
vllm_config = _make_config(numa_bind=True)
|
||||
monkeypatch.setattr(numa_utils, "get_auto_numa_nodes", lambda: None)
|
||||
with pytest.raises(RuntimeError):
|
||||
numa_utils._get_numactl_worker_args(vllm_config.parallel_config, local_rank=0)
|
||||
|
||||
|
||||
def test_configure_subprocess_rejects_unknown_process_kind():
|
||||
"""configure_subprocess only knows 'worker' and 'EngineCore'; anything
|
||||
else must raise ValueError instead of silently routing to the worker
|
||||
path."""
|
||||
vllm_config = _make_config(numa_bind=True, numa_bind_nodes=[0])
|
||||
with (
|
||||
pytest.raises(ValueError, match="process_kind"),
|
||||
numa_utils.configure_subprocess(
|
||||
vllm_config, local_rank=0, process_kind="bogus"
|
||||
),
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def test_log_numactl_show(monkeypatch):
|
||||
log_lines = []
|
||||
|
||||
def fake_debug(msg, *args):
|
||||
log_lines.append(msg % args)
|
||||
|
||||
monkeypatch.setattr(numa_utils.logger, "debug", fake_debug)
|
||||
monkeypatch.setattr(
|
||||
numa_utils.subprocess,
|
||||
"run",
|
||||
lambda *args, **kwargs: SimpleNamespace(
|
||||
stdout="policy: bind\nphyscpubind: 0 1 2 3\n", returncode=0
|
||||
),
|
||||
)
|
||||
|
||||
assert numa_utils._log_numactl_show("Worker_0") is True
|
||||
assert log_lines == [
|
||||
"Worker_0 affinity: policy: bind, physcpubind: 0 1 2 3",
|
||||
]
|
||||
|
||||
|
||||
def test_get_numactl_executable_points_to_fixed_wrapper(monkeypatch):
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/numactl")
|
||||
executable, debug_str = numa_utils._get_numactl_executable()
|
||||
assert executable.endswith("/vllm/utils/numa_wrapper.sh")
|
||||
assert "_VLLM_INTERNAL_NUMACTL_ARGS" in debug_str
|
||||
|
||||
|
||||
def test_set_numa_wrapper_env_restores_previous_values():
|
||||
os.environ[numa_utils._NUMACTL_ARGS_ENV] = "old-args"
|
||||
os.environ[numa_utils._NUMACTL_PYTHON_EXECUTABLE_ENV] = "old-python"
|
||||
|
||||
with numa_utils._set_numa_wrapper_env("new-args", "new-python"):
|
||||
assert os.environ[numa_utils._NUMACTL_ARGS_ENV] == "new-args"
|
||||
assert os.environ[numa_utils._NUMACTL_PYTHON_EXECUTABLE_ENV] == "new-python"
|
||||
|
||||
assert os.environ[numa_utils._NUMACTL_ARGS_ENV] == "old-args"
|
||||
assert os.environ[numa_utils._NUMACTL_PYTHON_EXECUTABLE_ENV] == "old-python"
|
||||
|
||||
|
||||
def test_set_numa_wrapper_env_clears_values_when_unset():
|
||||
os.environ.pop(numa_utils._NUMACTL_ARGS_ENV, None)
|
||||
os.environ.pop(numa_utils._NUMACTL_PYTHON_EXECUTABLE_ENV, None)
|
||||
|
||||
with numa_utils._set_numa_wrapper_env("new-args", "new-python"):
|
||||
assert os.environ[numa_utils._NUMACTL_ARGS_ENV] == "new-args"
|
||||
assert os.environ[numa_utils._NUMACTL_PYTHON_EXECUTABLE_ENV] == "new-python"
|
||||
|
||||
assert numa_utils._NUMACTL_ARGS_ENV not in os.environ
|
||||
assert numa_utils._NUMACTL_PYTHON_EXECUTABLE_ENV not in os.environ
|
||||
|
||||
|
||||
def test_parallel_config_validates_numa_bind_nodes():
|
||||
with pytest.raises(ValueError, match="non-negative"):
|
||||
ParallelConfig(numa_bind_nodes=[0, -1])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cpuset", ["", "abc", "1-", "4-1", "1,,2", "1:2"])
|
||||
def test_parallel_config_rejects_invalid_numa_bind_cpus(cpuset):
|
||||
with pytest.raises(ValueError, match="numa_bind_cpus"):
|
||||
ParallelConfig(numa_bind_cpus=[cpuset])
|
||||
|
||||
|
||||
def _fake_numactl_run(rejected_args):
|
||||
"""Fake ``numactl`` that fails when any of ``rejected_args`` is present."""
|
||||
|
||||
def run(cmd, *args, **kwargs):
|
||||
arg_str = " ".join(cmd[1:-1])
|
||||
ok = not any(bad in arg_str for bad in rejected_args)
|
||||
return SimpleNamespace(returncode=0 if ok else 1)
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def test_configure_subprocess_numa_fallback(monkeypatch):
|
||||
import multiprocessing
|
||||
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/numactl")
|
||||
monkeypatch.setattr(numa_utils.envs, "VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
||||
node_config = _make_config(numa_bind=True, numa_bind_nodes=[0])
|
||||
|
||||
monkeypatch.setattr(numa_utils.subprocess, "run", _fake_numactl_run([]))
|
||||
with numa_utils.configure_subprocess(node_config, local_rank=0):
|
||||
assert os.environ[numa_utils._NUMACTL_ARGS_ENV] == "--cpunodebind=0 --membind=0"
|
||||
|
||||
membind_fails = _fake_numactl_run(["--membind="])
|
||||
monkeypatch.setattr(numa_utils.subprocess, "run", membind_fails)
|
||||
with numa_utils.configure_subprocess(node_config, local_rank=0):
|
||||
assert os.environ[numa_utils._NUMACTL_ARGS_ENV] == "--cpunodebind=0"
|
||||
|
||||
cpu_config = _make_config(
|
||||
numa_bind=True,
|
||||
numa_bind_nodes=[0],
|
||||
numa_bind_cpus=["0-3"],
|
||||
)
|
||||
with numa_utils.configure_subprocess(cpu_config, local_rank=0):
|
||||
assert os.environ[numa_utils._NUMACTL_ARGS_ENV] == "--physcpubind=0-3"
|
||||
|
||||
before = multiprocessing.spawn.get_executable()
|
||||
monkeypatch.setattr(
|
||||
numa_utils.subprocess,
|
||||
"run",
|
||||
_fake_numactl_run(["--cpunodebind=", "--membind="]),
|
||||
)
|
||||
with numa_utils.configure_subprocess(node_config, local_rank=0):
|
||||
assert multiprocessing.spawn.get_executable() == before
|
||||
assert numa_utils._NUMACTL_ARGS_ENV not in os.environ
|
||||
@@ -0,0 +1,100 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.v1.executor.ray_utils import get_bundles_sorted_by_node
|
||||
|
||||
NODE_A = "node_a"
|
||||
NODE_B = "node_b"
|
||||
NODE_C = "node_c"
|
||||
|
||||
IP_A = "10.0.0.1"
|
||||
IP_B = "10.0.0.2"
|
||||
IP_C = "10.0.0.3"
|
||||
|
||||
NODE_ID_TO_IP = {NODE_A: IP_A, NODE_B: IP_B, NODE_C: IP_C}
|
||||
|
||||
MOCK_RAY_NODES = [
|
||||
{"NodeID": NODE_A, "NodeManagerAddress": IP_A, "Alive": True},
|
||||
{"NodeID": NODE_B, "NodeManagerAddress": IP_B, "Alive": True},
|
||||
{"NodeID": NODE_C, "NodeManagerAddress": IP_C, "Alive": True},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bundles_to_node_id,bundle_specs,expected",
|
||||
[
|
||||
pytest.param(
|
||||
{0: NODE_C, 1: NODE_A, 2: NODE_B, 3: NODE_C, 4: NODE_A, 5: NODE_B},
|
||||
[{"GPU": 1}] * 6,
|
||||
[
|
||||
(1, NODE_A, IP_A),
|
||||
(4, NODE_A, IP_A),
|
||||
(2, NODE_B, IP_B),
|
||||
(5, NODE_B, IP_B),
|
||||
(0, NODE_C, IP_C),
|
||||
(3, NODE_C, IP_C),
|
||||
],
|
||||
),
|
||||
pytest.param(
|
||||
{0: NODE_B, 1: NODE_B, 2: NODE_A, 3: NODE_A},
|
||||
[{"GPU": 1}] * 4,
|
||||
[
|
||||
(2, NODE_A, IP_A),
|
||||
(3, NODE_A, IP_A),
|
||||
(0, NODE_B, IP_B),
|
||||
(1, NODE_B, IP_B),
|
||||
],
|
||||
),
|
||||
pytest.param(
|
||||
{0: NODE_C, 1: NODE_B, 2: NODE_C, 3: NODE_B},
|
||||
[{"GPU": 1}] * 4,
|
||||
[
|
||||
(1, NODE_B, IP_B),
|
||||
(3, NODE_B, IP_B),
|
||||
(0, NODE_C, IP_C),
|
||||
(2, NODE_C, IP_C),
|
||||
],
|
||||
),
|
||||
pytest.param(
|
||||
{0: NODE_A, 1: NODE_A, 2: NODE_A},
|
||||
[{"GPU": 1}] * 3,
|
||||
[(0, NODE_A, IP_A), (1, NODE_A, IP_A), (2, NODE_A, IP_A)],
|
||||
),
|
||||
pytest.param(
|
||||
{},
|
||||
[],
|
||||
[],
|
||||
),
|
||||
pytest.param(
|
||||
{0: NODE_A, 1: NODE_B, 2: NODE_A},
|
||||
[{"CPU": 1}, {"GPU": 1}, {"GPU": 1}],
|
||||
[(2, NODE_A, IP_A), (1, NODE_B, IP_B)],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get_bundles_sorted_by_node(bundles_to_node_id, bundle_specs, expected):
|
||||
mock_pg = MagicMock()
|
||||
mock_pg.bundle_specs = bundle_specs
|
||||
|
||||
mock_ctx = MagicMock()
|
||||
mock_ctx.get_node_id.return_value = NODE_A
|
||||
|
||||
with (
|
||||
patch(
|
||||
"vllm.v1.executor.ray_utils.placement_group_table",
|
||||
return_value={"bundles_to_node_id": bundles_to_node_id},
|
||||
),
|
||||
patch("vllm.v1.executor.ray_utils.ray") as mock_ray,
|
||||
patch("vllm.v1.executor.ray_utils.current_platform") as mock_platform,
|
||||
):
|
||||
mock_ray.get_runtime_context.return_value = mock_ctx
|
||||
mock_ray.nodes.return_value = MOCK_RAY_NODES
|
||||
mock_platform.ray_device_key = "GPU"
|
||||
|
||||
result = get_bundles_sorted_by_node(mock_pg)
|
||||
|
||||
assert result == expected
|
||||
@@ -0,0 +1,81 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.models.utils import check_embeddings_close
|
||||
from vllm.utils.serial_utils import (
|
||||
EMBED_DTYPES,
|
||||
ENDIANNESS,
|
||||
MM_METADATA_DTYPES,
|
||||
EmbedDType,
|
||||
Endianness,
|
||||
MmMetadataDType,
|
||||
binary2tensor,
|
||||
tensor2binary,
|
||||
)
|
||||
|
||||
FLOAT_EMBED_DTYPES = tuple(EMBED_DTYPES.keys())
|
||||
INTEGER_EMBED_DTYPES = tuple(MM_METADATA_DTYPES.keys())
|
||||
|
||||
|
||||
def _build_integer_tensor(
|
||||
embed_dtype: MmMetadataDType, shape: tuple[int, ...]
|
||||
) -> torch.Tensor:
|
||||
torch_dtype = MM_METADATA_DTYPES[embed_dtype].torch_dtype
|
||||
|
||||
if torch_dtype is torch.bool:
|
||||
return torch.randint(0, 2, shape, dtype=torch.int32).to(torch.bool)
|
||||
if torch_dtype is torch.uint8:
|
||||
return torch.randint(0, 256, shape, dtype=torch.uint8)
|
||||
if torch_dtype is torch.int32:
|
||||
return torch.randint(-(2**20), 2**20, shape, dtype=torch.int32)
|
||||
if torch_dtype is torch.int64:
|
||||
return torch.randint(-(2**62), 2**62, shape, dtype=torch.int64)
|
||||
|
||||
raise AssertionError(f"Unsupported non-floating embed dtype: {embed_dtype}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("endianness", ENDIANNESS)
|
||||
@pytest.mark.parametrize("embed_dtype", FLOAT_EMBED_DTYPES)
|
||||
@torch.inference_mode()
|
||||
def test_encode_and_decode_floats(embed_dtype: EmbedDType, endianness: Endianness):
|
||||
for i in range(10):
|
||||
tensor = torch.rand(2, 3, 5, 7, 11, 13, device="cpu", dtype=torch.float32)
|
||||
shape = tensor.shape
|
||||
binary = tensor2binary(tensor, embed_dtype, endianness)
|
||||
new_tensor = binary2tensor(binary, shape, embed_dtype, endianness).to(
|
||||
torch.float32
|
||||
)
|
||||
|
||||
if embed_dtype in ["float32", "float16"]:
|
||||
torch.testing.assert_close(tensor, new_tensor, atol=0.001, rtol=0.001)
|
||||
elif embed_dtype == "bfloat16":
|
||||
torch.testing.assert_close(tensor, new_tensor, atol=0.01, rtol=0.01)
|
||||
else: # for fp8
|
||||
torch.testing.assert_close(tensor, new_tensor, atol=0.1, rtol=0.1)
|
||||
|
||||
check_embeddings_close(
|
||||
embeddings_0_lst=tensor.view(1, -1),
|
||||
embeddings_1_lst=new_tensor.view(1, -1),
|
||||
name_0="gt",
|
||||
name_1="new",
|
||||
tol=1e-2,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("endianness", ENDIANNESS)
|
||||
@pytest.mark.parametrize("embed_dtype", INTEGER_EMBED_DTYPES)
|
||||
@torch.inference_mode()
|
||||
def test_encode_and_decode_integers(
|
||||
embed_dtype: MmMetadataDType, endianness: Endianness
|
||||
):
|
||||
shape = (2, 3, 5, 7, 11, 13)
|
||||
|
||||
for i in range(10):
|
||||
tensor = _build_integer_tensor(embed_dtype, shape)
|
||||
binary = tensor2binary(tensor, embed_dtype, endianness)
|
||||
new_tensor = binary2tensor(binary, shape, embed_dtype, endianness)
|
||||
|
||||
assert new_tensor.dtype == MM_METADATA_DTYPES[embed_dtype].torch_dtype
|
||||
torch.testing.assert_close(tensor, new_tensor, atol=0, rtol=0)
|
||||
@@ -0,0 +1,33 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Tests for spawn_new_process_for_each_test decorator."""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.utils import spawn_new_process_for_each_test
|
||||
|
||||
|
||||
@spawn_new_process_for_each_test
|
||||
def test_spawn_decorator_passing():
|
||||
"""Passing function should complete normally."""
|
||||
assert 1 + 1 == 2
|
||||
|
||||
|
||||
@pytest.mark.xfail(raises=RuntimeError, strict=True)
|
||||
@spawn_new_process_for_each_test
|
||||
def test_spawn_decorator_failure_is_caught():
|
||||
"""Failing function should raise RuntimeError, never silently pass."""
|
||||
raise ValueError("intentional failure")
|
||||
|
||||
|
||||
@spawn_new_process_for_each_test
|
||||
def test_spawn_decorator_skip():
|
||||
"""pytest.skip inside subprocess should propagate correctly."""
|
||||
pytest.skip("intentional skip")
|
||||
|
||||
|
||||
@spawn_new_process_for_each_test
|
||||
@pytest.mark.parametrize("x,y,expected", [(1, 2, 3), (0, 0, 0)])
|
||||
def test_spawn_decorator_parametrized(x, y, expected):
|
||||
"""Args and kwargs must be forwarded correctly to subprocess."""
|
||||
assert x + y == expected
|
||||
@@ -0,0 +1,27 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from vllm.utils.system_utils import _maybe_force_spawn, unique_filepath
|
||||
|
||||
|
||||
def test_unique_filepath():
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
path_fn = lambda i: Path(temp_dir) / f"file_{i}.txt"
|
||||
paths = set()
|
||||
for i in range(10):
|
||||
path = unique_filepath(path_fn)
|
||||
path.write_text("test")
|
||||
paths.add(path)
|
||||
assert len(paths) == 10
|
||||
assert len(list(Path(temp_dir).glob("*.txt"))) == 10
|
||||
|
||||
|
||||
def test_numa_bind_forces_spawn(monkeypatch):
|
||||
monkeypatch.delenv("VLLM_WORKER_MULTIPROC_METHOD", raising=False)
|
||||
monkeypatch.setattr("sys.argv", ["vllm", "serve", "--numa-bind"])
|
||||
_maybe_force_spawn()
|
||||
assert os.environ["VLLM_WORKER_MULTIPROC_METHOD"] == "spawn"
|
||||
@@ -0,0 +1,203 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.model_executor.models.glm4_1v import Glm4vImageEmbeddingInputs
|
||||
from vllm.model_executor.models.granite_speech import GraniteSpeechAudioInputs
|
||||
from vllm.model_executor.models.hyperclovax_vision import HCXVisionVideoPixelInputs
|
||||
from vllm.model_executor.models.phi3v import Phi3VImagePixelInputs
|
||||
|
||||
|
||||
def test_tensor_schema_valid_tensor():
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=torch.randn(16, 64, 3, 32, 32),
|
||||
image_sizes=torch.randint(0, 256, (16, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_optional_fields():
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=torch.randn(16, 64, 3, 32, 32),
|
||||
image_sizes=None,
|
||||
)
|
||||
|
||||
Phi3VImagePixelInputs(pixel_values=torch.randn(16, 64, 3, 32, 32))
|
||||
|
||||
|
||||
def test_tensor_schema_constant_dim_failure():
|
||||
with pytest.raises(ValueError, match="dim\\[2\\] expected 3, got 4"):
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=torch.randn(16, 64, 4, 32, 32), # dim[2] = 4
|
||||
image_sizes=torch.randint(0, 256, (16, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_invalid_types_in_list():
|
||||
with pytest.raises(TypeError, match="is not one of the expected types"):
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=[
|
||||
torch.randn(64, 3, 32, 32),
|
||||
"not_a_tensor",
|
||||
torch.randn(64, 3, 32, 32),
|
||||
],
|
||||
image_sizes=torch.randint(0, 256, (3, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_rank_mismatch():
|
||||
with pytest.raises(ValueError, match="has rank 3 but expected 5"):
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=torch.randn(16, 64, 3),
|
||||
image_sizes=torch.randint(0, 256, (16, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_missing_required_field():
|
||||
with pytest.raises(ValueError, match="Required field 'pixel_values' is missing"):
|
||||
Phi3VImagePixelInputs(
|
||||
image_sizes=torch.randint(0, 256, (16, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_symbolic_dim_mismatch():
|
||||
with pytest.raises(ValueError, match="expected 'bn'=12, got 16"):
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=torch.randn(12, 64, 3, 32, 32),
|
||||
image_sizes=torch.randint(0, 256, (16, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_list_tensor_valid():
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=[torch.randn(64, 3, 32, 32) for _ in range(16)],
|
||||
image_sizes=torch.randint(0, 256, (16, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_variable_patch_counts_valid():
|
||||
# Each image has a different number of patches (p)
|
||||
# Each tensor has shape (p, 3, 32, 32)
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=[
|
||||
torch.randn(16, 3, 32, 32), # p = 16
|
||||
torch.randn(32, 3, 32, 32), # p = 32
|
||||
torch.randn(64, 3, 32, 32), # p = 64
|
||||
],
|
||||
image_sizes=torch.randint(0, 256, (3, 2)), # bn = 3
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_tuple_tensor_valid():
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=tuple(torch.randn(64, 3, 32, 32) for _ in range(16)),
|
||||
image_sizes=torch.randint(0, 256, (16, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_double_nested_tensors():
|
||||
x = torch.rand(4, 3, 32, 32)
|
||||
y = torch.rand(2, 3, 32, 32)
|
||||
|
||||
HCXVisionVideoPixelInputs(pixel_values_videos=([x, y, x], [y], [x, y]))
|
||||
|
||||
|
||||
def test_tensor_schema_inconsistent_shapes_in_list():
|
||||
with pytest.raises(ValueError, match="contains inconsistent shapes"):
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=[
|
||||
torch.randn(64, 3, 32, 32),
|
||||
torch.randn(64, 3, 16, 16),
|
||||
*(torch.randn(64, 3, 32, 32) for _ in range(14)),
|
||||
],
|
||||
image_sizes=torch.randint(0, 256, (16, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_empty_list():
|
||||
with pytest.raises(ValueError, match="is an empty sequence"):
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=[],
|
||||
image_sizes=torch.randint(0, 256, (0, 2)),
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_validation_disabled_skips_shape_check():
|
||||
# This should NOT raise, because validation is turned off
|
||||
# This would normally fail (dim[2] should be 3, not 4)
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=torch.randn(16, 64, 4, 32, 32),
|
||||
image_sizes=torch.randint(0, 256, (16, 2)),
|
||||
validate=False,
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_with_valid_resolve_binding_dims():
|
||||
pixel_values = torch.randn(16, 64, 3, 336, 336) # h=336, w=336
|
||||
image_sizes = torch.randint(0, 256, (16, 2))
|
||||
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=pixel_values,
|
||||
image_sizes=image_sizes,
|
||||
resolve_bindings={"h": 336, "w": 336},
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_with_invalid_resolve_binding_dims():
|
||||
pixel_values = torch.randn(16, 64, 3, 36, 36) # h=36, w=36
|
||||
image_sizes = torch.randint(0, 256, (16, 2))
|
||||
|
||||
# Should raise because 'h' and 'w' don't match resolve bindings
|
||||
with pytest.raises(ValueError, match="dim\\[3\\] expected 336, got 36"):
|
||||
Phi3VImagePixelInputs(
|
||||
pixel_values=pixel_values,
|
||||
image_sizes=image_sizes,
|
||||
resolve_bindings={"h": 336, "w": 336},
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_with_list_of_symbolic_dim():
|
||||
input_features = torch.randn(3, 10, 160) # (b=3, fi=10, 160)
|
||||
input_features_mask = torch.randn(3, 8) # (b=3, fo=8)
|
||||
audio_embed_sizes = [8, 8, 8] # len = b = 3
|
||||
|
||||
GraniteSpeechAudioInputs(
|
||||
input_features=input_features,
|
||||
input_features_mask=input_features_mask,
|
||||
audio_embed_sizes=audio_embed_sizes,
|
||||
)
|
||||
|
||||
|
||||
def test_tensor_schema_with_list_of_symbolic_dim_mismatch_in_length():
|
||||
input_features = torch.randn(4, 10, 160) # (b=4, fi=10, 160)
|
||||
input_features_mask = torch.randn(4, 8) # (b=4, fo=8)
|
||||
audio_embed_sizes = [8, 8, 8] # len = 3 ≠ b
|
||||
|
||||
with pytest.raises(ValueError, match="expected 'b'=4, got 3"):
|
||||
GraniteSpeechAudioInputs(
|
||||
input_features=input_features,
|
||||
input_features_mask=input_features_mask,
|
||||
audio_embed_sizes=audio_embed_sizes,
|
||||
)
|
||||
|
||||
|
||||
def test_valid_tensor_schema_with_static_last_dim():
|
||||
image_embeds = torch.randn(256, 1024)
|
||||
image_grid_thw = torch.randint(0, 4, (2, 3))
|
||||
|
||||
Glm4vImageEmbeddingInputs(
|
||||
image_embeds=image_embeds,
|
||||
image_grid_thw=image_grid_thw,
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_tensor_schema_with_static_last_dim():
|
||||
image_embeds = torch.randn(256, 1024)
|
||||
image_grid_thw = torch.randint(0, 4, (2, 4)) # Wrong last dim
|
||||
|
||||
with pytest.raises(ValueError, match="dim\\[1\\] expected 3, got 4"):
|
||||
Glm4vImageEmbeddingInputs(
|
||||
image_embeds=image_embeds,
|
||||
image_grid_thw=image_grid_thw,
|
||||
)
|
||||
@@ -0,0 +1,116 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.utils.torch_utils import (
|
||||
common_broadcastable_dtype,
|
||||
current_stream,
|
||||
is_lossless_cast,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("src_dtype", "tgt_dtype", "expected_result"),
|
||||
[
|
||||
# Different precision_levels
|
||||
(torch.bool, torch.int8, True),
|
||||
(torch.bool, torch.float16, True),
|
||||
(torch.bool, torch.complex32, True),
|
||||
(torch.int64, torch.bool, False),
|
||||
(torch.int64, torch.float16, True),
|
||||
(torch.int64, torch.complex32, True),
|
||||
(torch.float64, torch.bool, False),
|
||||
(torch.float64, torch.int8, False),
|
||||
(torch.float64, torch.complex32, True),
|
||||
(torch.complex128, torch.bool, False),
|
||||
(torch.complex128, torch.int8, False),
|
||||
(torch.complex128, torch.float16, False),
|
||||
# precision_level=0
|
||||
(torch.bool, torch.bool, True),
|
||||
# precision_level=1
|
||||
(torch.int8, torch.int16, True),
|
||||
(torch.int16, torch.int8, False),
|
||||
(torch.uint8, torch.int8, False),
|
||||
(torch.int8, torch.uint8, False),
|
||||
# precision_level=2
|
||||
(torch.float16, torch.float32, True),
|
||||
(torch.float32, torch.float16, False),
|
||||
(torch.bfloat16, torch.float32, True),
|
||||
(torch.float32, torch.bfloat16, False),
|
||||
# precision_level=3
|
||||
(torch.complex32, torch.complex64, True),
|
||||
(torch.complex64, torch.complex32, False),
|
||||
],
|
||||
)
|
||||
def test_is_lossless_cast(src_dtype, tgt_dtype, expected_result):
|
||||
assert is_lossless_cast(src_dtype, tgt_dtype) == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("dtypes", "expected_result"),
|
||||
[
|
||||
([torch.bool], torch.bool),
|
||||
([torch.bool, torch.int8], torch.int8),
|
||||
([torch.bool, torch.int8, torch.float16], torch.float16),
|
||||
([torch.bool, torch.int8, torch.float16, torch.complex32], torch.complex32), # noqa: E501
|
||||
],
|
||||
)
|
||||
def test_common_broadcastable_dtype(dtypes, expected_result):
|
||||
assert common_broadcastable_dtype(dtypes) == expected_result
|
||||
|
||||
|
||||
def _test_stream_thread(main_expected_stream: torch.cuda.Stream):
|
||||
import threading
|
||||
|
||||
child_stream = torch.cuda.Stream()
|
||||
thread_stream_ready = threading.Event()
|
||||
thread_can_exit = threading.Event()
|
||||
|
||||
def child_thread_func():
|
||||
with torch.cuda.stream(child_stream):
|
||||
thread_stream_ready.set()
|
||||
thread_can_exit.wait(timeout=10)
|
||||
|
||||
child_thread = threading.Thread(target=child_thread_func)
|
||||
child_thread.start()
|
||||
|
||||
try:
|
||||
assert thread_stream_ready.wait(timeout=5), (
|
||||
"Child thread failed to enter stream context in time"
|
||||
)
|
||||
|
||||
main_current_stream = current_stream()
|
||||
|
||||
assert main_current_stream != child_stream, (
|
||||
"Main thread's current_stream was contaminated by child thread"
|
||||
)
|
||||
assert main_current_stream == main_expected_stream, (
|
||||
f"Main thread's stream changed unexpectedly. "
|
||||
f"Expected {main_expected_stream}, got {main_current_stream}"
|
||||
)
|
||||
|
||||
thread_can_exit.set()
|
||||
|
||||
finally:
|
||||
child_thread.join(timeout=5)
|
||||
if child_thread.is_alive():
|
||||
pytest.fail("Child thread failed to exit properly")
|
||||
|
||||
|
||||
def test_current_stream_multithread():
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA not available")
|
||||
|
||||
main_dedicated_stream = current_stream()
|
||||
|
||||
assert main_dedicated_stream.cuda_stream != 0, (
|
||||
"ROCm/CUDA should create a dedicated stream, not use default stream (0x0)"
|
||||
)
|
||||
|
||||
main_stream_again = current_stream()
|
||||
assert main_stream_again == main_dedicated_stream, (
|
||||
"Multiple calls to current_stream should return the same dedicated stream"
|
||||
)
|
||||
|
||||
_test_stream_thread(main_dedicated_stream)
|
||||
Reference in New Issue
Block a user