A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
Make `mypy` behave like a proper pre-commit hook (#25313)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Harry Mellor committed
Sep 22, 2025 at 13:23 UTC
3d2c56b7a958aa2c92f61424e828b1d3c3c933d4
9 files changed
+166
-87
.github/CODEOWNERS
+1
@@ -72,6 +72,7 @@ mkdocs.yaml @hmellor
72
# Linting
73
.markdownlint.yaml @hmellor
74
.pre-commit-config.yaml @hmellor
75
+/tools/pre_commit @hmellor
76
77
# CPU
78
/vllm/v1/worker/cpu* @bigPYJ1151
.pre-commit-config.yaml
+14
-20
@@ -60,38 +60,32 @@ repos:
60
files: ^requirements/test\.(in|txt)$
61
- id: mypy-local
62
name: Run mypy for local Python installation
63
- entry: tools/mypy.sh 0 "local"
64
- language: python
65
- types: [python]
66
- additional_dependencies: &mypy_deps [mypy==1.11.1, types-cachetools, types-setuptools, types-PyYAML, types-requests, pydantic]
63
+ entry: python tools/pre_commit/mypy.py 0 "local"
64
stages: [pre-commit] # Don't run in CI
65
+ <<: &mypy_common
66
+ language: python
67
+ types_or: [python, pyi]
68
+ require_serial: true
69
+ additional_dependencies: [mypy==1.11.1, regex, types-cachetools, types-setuptools, types-PyYAML, types-requests, types-torch, pydantic]
70
- id: mypy-3.9 # TODO: Use https://github.com/pre-commit/mirrors-mypy when mypy setup is less awkward
71
name: Run mypy for Python 3.9
70
- entry: tools/mypy.sh 1 "3.9"
71
- language: python
72
- types: [python]
73
- additional_dependencies: *mypy_deps
72
+ entry: python tools/pre_commit/mypy.py 1 "3.9"
73
+ <<: *mypy_common
74
stages: [manual] # Only run in CI
75
- id: mypy-3.10 # TODO: Use https://github.com/pre-commit/mirrors-mypy when mypy setup is less awkward
76
name: Run mypy for Python 3.10
77
- entry: tools/mypy.sh 1 "3.10"
78
- language: python
79
- types: [python]
80
- additional_dependencies: *mypy_deps
77
+ entry: python tools/pre_commit/mypy.py 1 "3.10"
78
+ <<: *mypy_common
79
stages: [manual] # Only run in CI
80
- id: mypy-3.11 # TODO: Use https://github.com/pre-commit/mirrors-mypy when mypy setup is less awkward
81
name: Run mypy for Python 3.11
84
- entry: tools/mypy.sh 1 "3.11"
85
- language: python
86
- types: [python]
87
- additional_dependencies: *mypy_deps
82
+ entry: python tools/pre_commit/mypy.py 1 "3.11"
83
+ <<: *mypy_common
84
stages: [manual] # Only run in CI
85
- id: mypy-3.12 # TODO: Use https://github.com/pre-commit/mirrors-mypy when mypy setup is less awkward
86
name: Run mypy for Python 3.12
91
- entry: tools/mypy.sh 1 "3.12"
92
- language: python
93
- types: [python]
94
- additional_dependencies: *mypy_deps
87
+ entry: python tools/pre_commit/mypy.py 1 "3.12"
88
+ <<: *mypy_common
89
stages: [manual] # Only run in CI
90
- id: shellcheck
91
name: Lint shell scripts
pyproject.toml
-21
@@ -110,27 +110,6 @@ ignore_missing_imports = true
110
check_untyped_defs = true
111
follow_imports = "silent"
112
113
-# After fixing type errors resulting from follow_imports: "skip" -> "silent",
114
-# move the directory here and remove it from tools/mypy.sh
115
-files = [
116
- "vllm/*.py",
117
- "vllm/assets",
118
- "vllm/entrypoints",
119
- "vllm/inputs",
120
- "vllm/logging_utils",
121
- "vllm/multimodal",
122
- "vllm/platforms",
123
- "vllm/transformers_utils",
124
- "vllm/triton_utils",
125
- "vllm/usage",
126
-]
127
-# TODO(woosuk): Include the code from Megatron and HuggingFace.
128
-exclude = [
129
- "vllm/model_executor/parallel_utils/|vllm/model_executor/models/",
130
- # Ignore triton kernels in ops.
131
- 'vllm/attention/ops/.*\.py$'
132
-]
133
-
113
[tool.isort]
114
skip_glob = [
115
".buildkite/*",
tools/mypy.sh
deleted
-35
@@ -1,35 +0,0 @@
1
-#!/bin/bash
2
-
3
-CI=${1:-0}
4
-PYTHON_VERSION=${2:-local}
5
-
6
-if [ "$CI" -eq 1 ]; then
7
- set -e
8
-fi
9
-
10
-if [ $PYTHON_VERSION == "local" ]; then
11
- PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
12
-fi
13
-
14
-run_mypy() {
15
- echo "Running mypy on $1"
16
- if [ "$CI" -eq 1 ] && [ -z "$1" ]; then
17
- mypy --python-version "${PYTHON_VERSION}" "$@"
18
- return
19
- fi
20
- mypy --follow-imports skip --python-version "${PYTHON_VERSION}" "$@"
21
-}
22
-
23
-run_mypy # Note that this is less strict than CI
24
-run_mypy tests
25
-run_mypy vllm/attention
26
-run_mypy vllm/compilation
27
-run_mypy vllm/distributed
28
-run_mypy vllm/engine
29
-run_mypy vllm/executor
30
-run_mypy vllm/inputs
31
-run_mypy vllm/lora
32
-run_mypy --exclude 'vllm/model_executor/layers/fla/ops' vllm/model_executor
33
-run_mypy vllm/plugins
34
-run_mypy vllm/worker
35
-run_mypy vllm/v1
tools/pre_commit/mypy.py
new
+140
@@ -0,0 +1,140 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""
4
+Run mypy on changed files.
5
+
6
+This script is designed to be used as a pre-commit hook. It runs mypy
7
+on files that have been changed. It groups files into different mypy calls
8
+based on their directory to avoid import following issues.
9
+
10
+Usage:
11
+ python tools/pre_commit/mypy.py <ci> <python_version> <changed_files...>
12
+
13
+Args:
14
+ ci: "1" if running in CI, "0" otherwise. In CI, follow_imports is set to
15
+ "silent" for the main group of files.
16
+ python_version: Python version to use (e.g., "3.10") or "local" to use
17
+ the local Python version.
18
+ changed_files: List of changed files to check.
19
+"""
20
+
21
+import subprocess
22
+import sys
23
+from typing import Optional
24
+
25
+import regex as re
26
+
27
+FILES = [
28
+ "vllm/*.py",
29
+ "vllm/assets",
30
+ "vllm/entrypoints",
31
+ "vllm/inputs",
32
+ "vllm/logging_utils",
33
+ "vllm/multimodal",
34
+ "vllm/platforms",
35
+ "vllm/transformers_utils",
36
+ "vllm/triton_utils",
37
+ "vllm/usage",
38
+]
39
+
40
+# After fixing errors resulting from changing follow_imports
41
+# from "skip" to "silent", move the following directories to FILES
42
+SEPARATE_GROUPS = [
43
+ "tests",
44
+ "vllm/attention",
45
+ "vllm/compilation",
46
+ "vllm/distributed",
47
+ "vllm/engine",
48
+ "vllm/executor",
49
+ "vllm/inputs",
50
+ "vllm/lora",
51
+ "vllm/model_executor",
52
+ "vllm/plugins",
53
+ "vllm/worker",
54
+ "vllm/v1",
55
+]
56
+
57
+# TODO(woosuk): Include the code from Megatron and HuggingFace.
58
+EXCLUDE = [
59
+ "vllm/model_executor/parallel_utils",
60
+ "vllm/model_executor/models",
61
+ "vllm/model_executor/layers/fla/ops",
62
+ # Ignore triton kernels in ops.
63
+ "vllm/attention/ops",
64
+]
65
+
66
+
67
+def group_files(changed_files: list[str]) -> dict[str, list[str]]:
68
+ """
69
+ Group changed files into different mypy calls.
70
+
71
+ Args:
72
+ changed_files: List of changed files.
73
+
74
+ Returns:
75
+ A dictionary mapping file group names to lists of changed files.
76
+ """
77
+ exclude_pattern = re.compile(f"^{'|'.join(EXCLUDE)}.*")
78
+ files_pattern = re.compile(f"^({'|'.join(FILES)}).*")
79
+ file_groups = {"": []}
80
+ file_groups.update({k: [] for k in SEPARATE_GROUPS})
81
+ for changed_file in changed_files:
82
+ # Skip files which should be ignored completely
83
+ if exclude_pattern.match(changed_file):
84
+ continue
85
+ # Group files by mypy call
86
+ if files_pattern.match(changed_file):
87
+ file_groups[""].append(changed_file)
88
+ continue
89
+ else:
90
+ for directory in SEPARATE_GROUPS:
91
+ if re.match(f"^{directory}.*", changed_file):
92
+ file_groups[directory].append(changed_file)
93
+ break
94
+ return file_groups
95
+
96
+
97
+def mypy(targets: list[str], python_version: Optional[str],
98
+ follow_imports: Optional[str], file_group: str) -> int:
99
+ """
100
+ Run mypy on the given targets.
101
+
102
+ Args:
103
+ targets: List of files or directories to check.
104
+ python_version: Python version to use (e.g., "3.10") or None to use
105
+ the default mypy version.
106
+ follow_imports: Value for the --follow-imports option or None to use
107
+ the default mypy behavior.
108
+ file_group: The file group name for logging purposes.
109
+
110
+ Returns:
111
+ The return code from mypy.
112
+ """
113
+ args = ["mypy"]
114
+ if python_version is not None:
115
+ args += ["--python-version", python_version]
116
+ if follow_imports is not None:
117
+ args += ["--follow-imports", follow_imports]
118
+ print(f"$ {' '.join(args)} {file_group}")
119
+ return subprocess.run(args + targets, check=False).returncode
120
+
121
+
122
+def main():
123
+ ci = sys.argv[1] == "1"
124
+ python_version = sys.argv[2]
125
+ file_groups = group_files(sys.argv[3:])
126
+
127
+ if python_version == "local":
128
+ python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
129
+
130
+ returncode = 0
131
+ for file_group, changed_files in file_groups.items():
132
+ follow_imports = None if ci and file_group == "" else "skip"
133
+ if changed_files:
134
+ returncode |= mypy(changed_files, python_version, follow_imports,
135
+ file_group)
136
+ return returncode
137
+
138
+
139
+if __name__ == "__main__":
140
+ sys.exit(main())
vllm/entrypoints/llm.py
+2
-2
@@ -1468,7 +1468,7 @@ class LLM:
1468
1469
def _validate_and_add_requests(
1470
self,
1471
- prompts: Union[PromptType, Sequence[PromptType]],
1471
+ prompts: Union[PromptType, Sequence[PromptType], DataPrompt],
1472
params: Union[SamplingParams, Sequence[SamplingParams], PoolingParams,
1473
Sequence[PoolingParams]],
1474
*,
@@ -1478,7 +1478,7 @@ class LLM:
1478
) -> None:
1479
if isinstance(prompts, (str, dict)):
1480
# Convert a single prompt to a list.
1481
- prompts = [prompts]
1481
+ prompts = [prompts] # type: ignore[list-item]
1482
1483
num_requests = len(prompts)
1484
if isinstance(params, Sequence) and len(params) != num_requests:
vllm/entrypoints/renderer.py
+1
-1
@@ -280,7 +280,7 @@ class CompletionRenderer(BaseRenderer):
280
if truncate_prompt_tokens < 0:
281
truncate_prompt_tokens = self.model_config.max_model_len
282
283
- if max_length is not None and truncate_prompt_tokens > max_length:
283
+ if max_length is not None and truncate_prompt_tokens > max_length: # type: ignore[operator]
284
raise ValueError(
285
f"truncate_prompt_tokens ({truncate_prompt_tokens}) "
286
f"cannot be greater than max_length ({max_length}). "
vllm/utils/__init__.py
+5
-4
@@ -551,9 +551,10 @@ class AsyncMicrobatchTokenizer:
551
# If every request uses identical kwargs we can run a single
552
# batched tokenizer call for a big speed-up.
553
if can_batch and len(prompts) > 1:
554
- encode_fn = partial(self.tokenizer, prompts, **kwargs)
554
+ batch_encode_fn = partial(self.tokenizer, prompts,
555
+ **kwargs)
556
results = await self._loop.run_in_executor(
556
- self._executor, encode_fn)
557
+ self._executor, batch_encode_fn)
558
559
for i, fut in enumerate(result_futures):
560
if not fut.done():
@@ -889,7 +890,7 @@ def get_open_port() -> int:
890
891
def get_open_ports_list(count: int = 5) -> list[int]:
892
"""Get a list of open ports."""
892
- ports = set()
893
+ ports = set[int]()
894
while len(ports) < count:
895
ports.add(get_open_port())
896
return list(ports)
@@ -1279,7 +1280,7 @@ def as_list(maybe_list: Iterable[T]) -> list[T]:
1280
1281
def as_iter(obj: Union[T, Iterable[T]]) -> Iterable[T]:
1282
if isinstance(obj, str) or not isinstance(obj, Iterable):
1282
- obj = [obj]
1283
+ return [obj] # type: ignore[list-item]
1284
return obj
1285
1286
vllm/utils/tensor_schema.py
+3
-4
@@ -22,9 +22,8 @@ class TensorShape:
22
self.dims = dims
23
self.dynamic_dims = dynamic_dims if dynamic_dims else set()
24
25
- def resolve(self, **bindings: dict[str,
26
- int]) -> tuple[Union[int, str], ...]:
27
- resolved = []
25
+ def resolve(self, **bindings: int) -> tuple[Union[int, str], ...]:
26
+ resolved = list[Union[int, str]]()
27
for dim in self.dims:
28
if isinstance(dim, str) and dim in bindings:
29
resolved.append(bindings[dim])
@@ -159,7 +158,7 @@ class TensorSchema:
158
159
def validate(self) -> None:
160
type_hints = get_type_hints(self.__class__, include_extras=True)
162
- shape_env = {}
161
+ shape_env = dict[str, int]()
162
163
for field_name, field_type in type_hints.items():
164
# Check if field is missing