[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: