@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 3d2c56b7a9
Fork 0

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