main
py 327 lines 13.5 KB
Raw
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: Apache-2.0
3 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4 import sys
5 from dataclasses import dataclass, field
6
7 import regex as re
8
9 # Hub entry points that must go through the vLLM-tagged repo_utils helpers.
10 _HF_NAMES = (
11 r"HfApi|HfFileSystem|hf_hub_download|snapshot_download"
12 r"|list_repo_files|file_exists|try_to_load_from_cache"
13 r"|list_repo_refs|repo_exists"
14 )
15
16 # Transformers v4 names kept as aliases in v5.
17 _TRANSFORMERS_LEGACY_NAMES = (
18 r"PretrainedConfig|PreTrainedTokenizer|PreTrainedTokenizerFast"
19 r"|BaseImageProcessorFast|\w+ImageProcessorFast"
20 )
21 _TRANSFORMERS_LEGACY_MODULES = (
22 r"tokenization_utils|tokenization_utils_fast|image_processing_utils_fast"
23 r"|models\.\w+\.\w+_fast"
24 )
25
26
27 @dataclass
28 class ForbiddenImport:
29 pattern: str
30 tip: str
31 allowed_pattern: re.Pattern = re.compile(r"^$") # matches nothing by default
32 allowed_files: set[str] = field(default_factory=set)
33 allowed_dirs: set[str] = field(default_factory=set)
34
35
36 CHECK_IMPORTS = {
37 "pickle/cloudpickle": ForbiddenImport(
38 pattern=(
39 r"^\s*(import\s+(pickle|cloudpickle)(\s|$|\sas)"
40 r"|from\s+(pickle|cloudpickle)\s+import\b)"
41 ),
42 tip=(
43 "Avoid using pickle or cloudpickle or add this file to "
44 "tools/pre_commit/check_forbidden_imports.py."
45 ),
46 allowed_files={
47 # pickle
48 "vllm/multimodal/hasher.py",
49 "vllm/transformers_utils/config.py",
50 "vllm/model_executor/models/registry.py",
51 "vllm/compilation/caching.py",
52 "vllm/env_override.py",
53 "vllm/compilation/piecewise_backend.py",
54 "vllm/distributed/utils.py",
55 "vllm/distributed/parallel_state.py",
56 "vllm/distributed/device_communicators/all_reduce_utils.py",
57 "vllm/distributed/device_communicators/shm_broadcast.py",
58 "vllm/distributed/device_communicators/shm_object_storage.py",
59 "vllm/distributed/weight_transfer/ipc_engine.py",
60 "vllm/distributed/weight_transfer/clients.py",
61 "vllm/model_executor/model_loader/weight_cache/protocol.py",
62 "tests/distributed/test_shm_broadcast.py",
63 "tests/distributed/test_weight_transfer.py",
64 "vllm/utils/hashing.py",
65 "tests/multimodal/media/test_base.py",
66 "tests/tokenizers_/test_hf.py",
67 "tests/utils_/test_hashing.py",
68 "tests/compile/test_aot_compile.py",
69 "benchmarks/kernels/graph_machete_bench.py",
70 "benchmarks/kernels/benchmark_lora.py",
71 "benchmarks/kernels/benchmark_machete.py",
72 "benchmarks/fused_kernels/layernorm_rms_benchmarks.py",
73 # cloudpickle
74 "vllm/v1/executor/multiproc_executor.py",
75 "vllm/v1/executor/ray_executor.py",
76 "vllm/entrypoints/llm.py",
77 "tests/utils.py",
78 # pickle and cloudpickle
79 "vllm/v1/serial_utils.py",
80 },
81 ),
82 "base64": ForbiddenImport(
83 pattern=r"^\s*(?:import\s+base64(?:$|\s|,)|from\s+base64\s+import)",
84 tip=(
85 "Replace 'import base64' with 'import pybase64' "
86 "or 'import pybase64 as base64'."
87 ),
88 allowed_pattern=re.compile(r"^\s*import\s+pybase64(\s*|\s+as\s+base64\s*)$"),
89 ),
90 "re": ForbiddenImport(
91 pattern=r"^\s*(?:import\s+re(?:$|\s|,)|from\s+re\s+import)",
92 tip="Replace 'import re' with 'import regex as re' or 'import regex'.",
93 allowed_pattern=re.compile(r"^\s*import\s+regex(\s*|\s+as\s+re\s*)$"),
94 allowed_files={"setup.py"},
95 ),
96 "triton": ForbiddenImport(
97 pattern=r"^(from|import)\s+triton(\s|\.|$)",
98 tip="Use 'from vllm.triton_utils import triton' instead.",
99 allowed_pattern=re.compile(
100 "from vllm.triton_utils import (triton|tl|tl, triton)"
101 ),
102 allowed_files={"vllm/triton_utils/importing.py"},
103 ),
104 "tilelang": ForbiddenImport(
105 pattern=r"^(from|import)\s+tilelang(\s|\.|$)",
106 tip="Use 'from vllm.tilelang_utils import tilelang, T' instead.",
107 allowed_pattern=re.compile(
108 r"from\s+vllm\.tilelang_utils\s+import\s+"
109 r"(tilelang|T|T, tilelang|tilelang, T)\b"
110 ),
111 allowed_files={"vllm/tilelang_utils/__init__.py"},
112 ),
113 "huggingface_hub repo API": ForbiddenImport(
114 # Catch `from huggingface_hub import <fn>`, including parenthesized,
115 # multi-line imports.
116 pattern=(
117 r"^\s*from\s+huggingface_hub\s+import\s*\([^)]*\b(?:" + _HF_NAMES + r")\b"
118 r"|"
119 r"^\s*from\s+huggingface_hub\s+import\b[^\n]*\b(?:" + _HF_NAMES + r")\b"
120 ),
121 tip=(
122 "Use the shared, vLLM-tagged helpers from "
123 "vllm.transformers_utils.repo_utils (e.g. hf_api(), hf_fs(), "
124 "list_repo_files, file_exists) instead of calling "
125 "huggingface_hub directly."
126 ),
127 allowed_files={"vllm/transformers_utils/repo_utils.py"},
128 allowed_dirs={"examples/"},
129 ),
130 "transformers legacy names": ForbiddenImport(
131 pattern=(
132 r"^\s*from\s+transformers(?:\.[\w.]+)?\s+import\s*\([^)]*\b(?:"
133 + _TRANSFORMERS_LEGACY_NAMES
134 + r")\b"
135 r"|"
136 r"^\s*from\s+transformers(?:\.[\w.]+)?\s+import\b[^\n]*\b(?:"
137 + _TRANSFORMERS_LEGACY_NAMES
138 + r")\b"
139 r"|"
140 r"^\s*(?:from|import)\s+transformers\.(?:"
141 + _TRANSFORMERS_LEGACY_MODULES
142 + r")\b"
143 ),
144 tip=(
145 "Use the Transformers v5 names, e.g. PreTrainedConfig, PythonBackend, "
146 "TokenizersBackend, TorchvisionBackend, tokenization_utils_tokenizers, "
147 "tokenization_utils_sentencepiece and image_processing_backends."
148 ),
149 ),
150 }
151
152
153 def check_file(path: str) -> int:
154 with open(path, encoding="utf-8") as f:
155 content = f.read()
156 return_code = 0
157 # Check all patterns in the whole file
158 for import_name, forbidden_import in CHECK_IMPORTS.items():
159 # Skip files that are allowed for this import
160 if path in forbidden_import.allowed_files:
161 continue
162 # Skip directories that are allowed for this import
163 if any(path.startswith(prefix) for prefix in forbidden_import.allowed_dirs):
164 continue
165 # Search for forbidden imports
166 for match in re.finditer(forbidden_import.pattern, content, re.MULTILINE):
167 # Check if it's allowed
168 if forbidden_import.allowed_pattern.match(match.group()):
169 continue
170 # Skip matches inside a comment
171 line_start = content.rfind("\n", 0, match.start()) + 1
172 if "#" in content[line_start : match.start()]:
173 continue
174 # Calculate line number from match position
175 line_num = content[: match.start() + 1].count("\n") + 1
176 print(
177 f"{path}:{line_num}: "
178 "\033[91merror:\033[0m " # red color
179 f"Found forbidden import: {import_name}. {forbidden_import.tip}"
180 )
181 return_code = 1
182 return return_code
183
184
185 def main():
186 returncode = 0
187 for path in sys.argv[1:]:
188 returncode |= check_file(path)
189 return returncode
190
191
192 def test_regex():
193 def matches(rule: str, content: str) -> bool:
194 return bool(re.search(CHECK_IMPORTS[rule].pattern, content, re.MULTILINE))
195
196 pickle_cases = [
197 # Should match
198 ("import pickle", True),
199 ("import cloudpickle", True),
200 ("import pickle as pkl", True),
201 ("import cloudpickle as cpkl", True),
202 ("from pickle import *", True),
203 ("from cloudpickle import dumps", True),
204 ("from pickle import dumps, loads", True),
205 ("from cloudpickle import (dumps, loads)", True),
206 (" import pickle", True),
207 ("\timport cloudpickle", True),
208 ("from pickle import loads", True),
209 # Should not match
210 ("import somethingelse", False),
211 ("from somethingelse import pickle", False),
212 ("# import pickle", False),
213 ("print('import pickle')", False),
214 ("import pickleas as asdf", False),
215 ]
216 for i, (content, should_match) in enumerate(pickle_cases):
217 result = matches("pickle/cloudpickle", content)
218 assert result == should_match, (
219 f"pickle case {i} failed: {content!r} "
220 f"(expected {should_match}, got {result})"
221 )
222
223 tilelang_cases = [
224 # Should match
225 ("import tilelang", True),
226 ("import tilelang.language as T", True),
227 ("from tilelang.jit import JITImpl", True),
228 ("from tilelang.jit.kernel import JITKernel", True),
229 # Should not match: indented (local) imports are allowed, mirroring
230 # the "triton" rule, so mocked-module test imports are not flagged.
231 (" import tilelang", False),
232 (" from tilelang.jit import JITImpl", False),
233 ("from vllm.tilelang_utils import tilelang", False),
234 ("from vllm.tilelang_utils import T", False),
235 ("from vllm.tilelang_utils import T, tilelang", False),
236 ("from vllm.tilelang_utils import tilelang, T", False),
237 ("import tilelang_kernels", False),
238 ]
239 for i, (content, should_match) in enumerate(tilelang_cases):
240 result = matches("tilelang", content)
241 assert result == should_match, (
242 f"tilelang case {i} failed: {content!r} "
243 f"(expected {should_match}, got {result})"
244 )
245
246 hf_cases = [
247 # Should match
248 ("from huggingface_hub import snapshot_download", True),
249 ("from huggingface_hub import hf_hub_download", True),
250 ("from huggingface_hub import HfApi", True),
251 ("from huggingface_hub import HfFileSystem", True),
252 ("from huggingface_hub import list_repo_files", True),
253 ("from huggingface_hub import try_to_load_from_cache", True),
254 (" from huggingface_hub import snapshot_download", True),
255 ("from huggingface_hub import PyTorchModelHubMixin, hf_hub_download", True),
256 ("from huggingface_hub import (snapshot_download)", True),
257 # Parenthesized multi-line import must not bypass the hook
258 ("from huggingface_hub import (\n snapshot_download,\n)", True),
259 (
260 "from huggingface_hub import (\n PyTorchModelHubMixin,\n HfApi,\n)",
261 True,
262 ),
263 # Should not match
264 ("import huggingface_hub", False),
265 ("import huggingface_hub as hf", False),
266 ("from huggingface_hub import PyTorchModelHubMixin", False),
267 ("from huggingface_hub.constants import HF_HUB_CACHE", False),
268 ("from huggingface_hub.utils import EntryNotFoundError", False),
269 ("from vllm.transformers_utils.repo_utils import hf_api", False),
270 ("from huggingface_hub import (\n PyTorchModelHubMixin,\n)", False),
271 ("# from huggingface_hub import snapshot_download", False),
272 ]
273 for i, (content, should_match) in enumerate(hf_cases):
274 result = matches("huggingface_hub repo API", content)
275 assert result == should_match, (
276 f"huggingface_hub case {i} failed: {content!r} "
277 f"(expected {should_match}, got {result})"
278 )
279
280 transformers_cases = [
281 # Should match
282 ("from transformers import PretrainedConfig", True),
283 ("from transformers import AutoConfig, PretrainedConfig", True),
284 ("from transformers import PreTrainedTokenizer", True),
285 ("from transformers import PreTrainedTokenizerFast", True),
286 ("from transformers.models.siglip import SiglipImageProcessorFast", True),
287 ("from transformers import (\n PreTrainedTokenizerFast,\n)", True),
288 ("from transformers.configuration_utils import PretrainedConfig", True),
289 (" from transformers import PretrainedConfig", True),
290 ("from transformers.tokenization_utils import AddedToken", True),
291 ("from transformers.tokenization_utils_fast import X", True),
292 ("from transformers.image_processing_utils_fast import X", True),
293 (
294 "from transformers.models.qwen2_vl.image_processing_qwen2_vl_fast import X",
295 True,
296 ),
297 ("import transformers.tokenization_utils_fast", True),
298 # Should not match
299 ("from transformers import PreTrainedConfig", False),
300 ("from transformers import PreTrainedTokenizerBase", False),
301 ("from transformers import PythonBackend, TokenizersBackend", False),
302 ("from transformers.tokenization_utils_base import BatchEncoding", False),
303 ("from transformers.tokenization_utils_tokenizers import X", False),
304 (
305 "from transformers.image_processing_backends import TorchvisionBackend",
306 False,
307 ),
308 ("from transformers.models.qwen2_vl import Qwen2VLImageProcessor", False),
309 ("from vllm.foo import GLM4VImageProcessorFast", False),
310 ("from transformers import (\n PreTrainedConfig,\n)", False),
311 ("# from transformers import PretrainedConfig", False),
312 ]
313 for i, (content, should_match) in enumerate(transformers_cases):
314 result = matches("transformers legacy names", content)
315 assert result == should_match, (
316 f"transformers case {i} failed: {content!r} "
317 f"(expected {should_match}, got {result})"
318 )
319
320 print("All regex tests passed.")
321
322
323 if __name__ == "__main__":
324 if "--test-regex" in sys.argv:
325 test_regex()
326 else:
327 sys.exit(main())