[Renderer] Shutdown the renderer properly. (#52124)

Signed-off-by: wang.yuqi <yuqi.wang@daocloud.io>

wang.yuqi committed Aug 31, 2026 at 16:40 UTC 5bfd76372d66507d014895376458fd507e124e66
1 file changed +12 -13
vllm/renderers/base.py
+12 -13
@@ -2,9 +2,11 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import asyncio
4 import time
5 +import weakref
6 from abc import ABC, abstractmethod
7 from collections.abc import Mapping, Sequence
7 -from concurrent.futures import Executor, ThreadPoolExecutor
8 +from concurrent.futures import ThreadPoolExecutor
9 +from contextlib import ExitStack
10 from functools import cached_property
11 from typing import TYPE_CHECKING, Any, Generic, overload
12
@@ -77,6 +79,10 @@ class BaseRenderer(ABC, Generic[_T]):
79 self.model_config = config.model_config
80 self.api_process_rank = config.parallel_config._api_process_rank
81
82 + self._resources = ExitStack()
83 + self._resources.callback(logger.debug, f"[shutdown] {self.__class__.__name__}")
84 + self._finalizer = weakref.finalize(self, self._resources.close)
85 +
86 self.tokenizer = tokenizer
87
88 # Thread pool executor for blocking tokenizer operations. The
@@ -84,10 +90,12 @@ class BaseRenderer(ABC, Generic[_T]):
90 # so it is safe to run tokenization and MM preprocessing concurrently.
91 pool_workers = config.model_config.renderer_num_workers
92 self._executor = ThreadPoolExecutor(max_workers=pool_workers)
93 + self._resources.callback(self._executor.shutdown, wait=False)
94
95 # Separate single-worker executor so tokenization never queues behind
96 # MM preprocessing; must stay single-worker per #38418 (P0/P1 order).
90 - self._mm_executor: Executor = ThreadPoolExecutor(max_workers=1)
97 + self._mm_executor = ThreadPoolExecutor(max_workers=1)
98 + self._resources.callback(self._mm_executor.shutdown, wait=False)
99
100 # Offload tokenization to the thread pool. The sync
101 # ``_tokenize_prompt`` already encapsulates the unified ``__call__``
@@ -132,6 +140,7 @@ class BaseRenderer(ABC, Generic[_T]):
140
141 if mm_processor_cache:
142 self._mm_cache_stats = MultiModalCacheStats()
143 + self._resources.callback(mm_processor_cache.close)
144
145 # A second processor with its own processor-only cache.
146 # Used by the tokenize endpoint so that tokenize-only
@@ -288,17 +297,7 @@ class BaseRenderer(ABC, Generic[_T]):
297 await self._clear_mm_cache_async()
298
299 def shutdown(self) -> None:
291 - mm_processor_cache = self.mm_processor_cache
292 - if mm_processor_cache is not None:
293 - mm_processor_cache.close()
294 -
295 - if executor := getattr(self, "_executor", None):
296 - executor.shutdown(wait=False)
297 -
298 - if (
299 - mm_executor := getattr(self, "_mm_executor", None)
300 - ) is not None and mm_executor is not executor:
301 - mm_executor.shutdown(wait=False)
300 + self._resources.close()
301
302 def get_bos_token_id(self) -> int | None:
303 if self.tokenizer is None: