[Bugfix] Fix RuntimeError: Already borrowed by adding thread-safe Hugging Face fast-tokenizer wrappers (#41181)

Signed-off-by: Yifan Zong <yzong@redhat.com>
Co-authored-by: wang.yuqi <yuqi.wang@daocloud.io>
This commit is contained in:
yzong-rh
2026-05-05 14:04:01 +00:00
committed by GitHub
co-authored by wang.yuqi
parent 6fca518157
commit 20dcd984f9
5 changed files with 107 additions and 35 deletions
-22
View File
@@ -268,28 +268,6 @@ class InputProcessingContext:
try:
output = hf_processor(**data, **allowed_kwargs)
except Exception as exc:
# See https://github.com/huggingface/tokenizers/issues/537
if (
isinstance(exc, RuntimeError)
and exc
and exc.args[0] == "Already borrowed"
and num_tries < max_tries
):
logger.warning(
"Failed to acquire tokenizer in current thread. "
"Retrying (%d/%d)...",
num_tries,
max_tries,
)
time.sleep(0.5)
return self.call_hf_processor(
hf_processor,
data,
kwargs,
num_tries=num_tries + 1,
max_tries=max_tries,
)
msg = (
f"Failed to apply {type(hf_processor).__name__} "
f"on data={data} with kwargs={allowed_kwargs}"
+2 -11
View File
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import asyncio
import copy
import time
from abc import ABC, abstractmethod
from collections.abc import Mapping, Sequence
@@ -108,17 +107,10 @@ class BaseRenderer(ABC, Generic[_T]):
if mm_registry.supports_multimodal_inputs(config.model_config):
mm_processor_cache = mm_registry.processor_cache_from_config(config)
# Deep-copy the tokenizer so the multimodal processor gets its
# own Rust tokenizer backend. Without this, concurrent access
# from AsyncMicrobatchTokenizer and call_hf_processor causes
# "RuntimeError: Already borrowed" from the Rust RefCell.
# See: https://github.com/huggingface/tokenizers/issues/537
mm_tokenizer = copy.deepcopy(tokenizer)
with set_default_torch_num_threads():
self.mm_processor = mm_registry.create_processor(
config.model_config,
tokenizer=mm_tokenizer,
tokenizer=self.tokenizer,
cache=mm_processor_cache,
)
@@ -130,11 +122,10 @@ class BaseRenderer(ABC, Generic[_T]):
# requests don't pollute the sender cache.
ro_cache = mm_registry.processor_only_cache_from_config(config)
if ro_cache is not None:
ro_tokenizer = copy.deepcopy(tokenizer)
with set_default_torch_num_threads():
self._readonly_mm_processor = mm_registry.create_processor(
config.model_config,
tokenizer=ro_tokenizer,
tokenizer=self.tokenizer,
cache=ro_cache,
)
+15 -1
View File
@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import copy
import inspect
import itertools
import weakref
@@ -42,7 +43,7 @@ from vllm.multimodal.processing.processor import (
apply_token_matches,
find_mm_placeholders,
)
from vllm.tokenizers.hf import HfTokenizer
from vllm.tokenizers.hf import HfTokenizer, maybe_make_thread_pool
from vllm.transformers_utils.chat_templates import get_chat_template_fallback_path
from vllm.transformers_utils.processor import cached_get_processor
from vllm.utils.async_utils import make_async
@@ -785,6 +786,14 @@ class HfRenderer(BaseRenderer[HfTokenizer]):
config: VllmConfig,
tokenizer: HfTokenizer | None,
) -> None:
# Ensure the og tokenizer is never modified by maybe_make_thread_pool
tokenizer = copy.copy(tokenizer)
if (
# Skip for mock configs and tokenizers
getattr(config.model_config, "enable_prompt_embeds", False)
and isinstance(tokenizer, HfTokenizer)
):
_ensure_prompt_embeds_placeholder_token(tokenizer)
super().__init__(config, tokenizer)
self.use_unified_vision_chunk = getattr(
@@ -795,6 +804,11 @@ class HfRenderer(BaseRenderer[HfTokenizer]):
safe_apply_chat_template, executor=self._executor
)
if self.tokenizer is not None:
maybe_make_thread_pool(
self.tokenizer, config.model_config.renderer_num_workers + 1
)
def render_messages(
self,
messages: list[ChatCompletionMessageParam],
+2
View File
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from .hf import maybe_make_thread_pool
from .protocol import TokenizerLike
from .registry import (
TokenizerRegistry,
@@ -15,4 +16,5 @@ __all__ = [
"cached_get_tokenizer",
"get_tokenizer",
"cached_tokenizer_from_config",
"maybe_make_thread_pool",
]
+88 -1
View File
@@ -2,8 +2,9 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import contextlib
import copy
import queue
from pathlib import Path
from typing import TypeAlias
from typing import TypeAlias, TypeVar
from transformers import AutoTokenizer, PreTrainedTokenizer, PreTrainedTokenizerFast
@@ -12,6 +13,92 @@ from vllm.transformers_utils.config import get_sentence_transformer_tokenizer_co
from .protocol import TokenizerLike
HfTokenizer: TypeAlias = PreTrainedTokenizer | PreTrainedTokenizerFast
_T = TypeVar("_T", bound=TokenizerLike)
class ThreadSafeHFTokenizerMixin:
"""Mixin class for thread-safe HF fast tokenizers."""
pass
def maybe_make_thread_pool(tokenizer: _T, copies: int = 1):
"""
If `tokenizer` is a `PreTrainedTokenizerFast`, modify the tokenizer
in-place to make the public interface thread-safe by routing calls
through a deep-copied tokenizer pool.
Note that:
- Only ``TokenizerLike``'s public interface is thread-safe.
This doesn't include ``_tokenizer`` property nor any mutation
methods like ``add_special_tokens`` or ``add_tokens``.
- Adjacent method calls could happen on different deep copies.
"""
if not isinstance(tokenizer, PreTrainedTokenizerFast) or isinstance(
tokenizer, ThreadSafeHFTokenizerMixin
):
return tokenizer
og_tokenizer = copy.copy(tokenizer)
tokenizer_pool: queue.Queue[PreTrainedTokenizerFast] = queue.Queue()
for _ in range(copies):
tokenizer_pool.put(copy.deepcopy(og_tokenizer))
@contextlib.contextmanager
def _borrow_from_pool():
try:
tok = tokenizer_pool.get_nowait()
yield tok
except queue.Empty:
tok = copy.deepcopy(og_tokenizer)
yield tok
finally:
tokenizer_pool.put(tok)
class TokenizerPool(tokenizer.__class__, ThreadSafeHFTokenizerMixin): # type: ignore
def apply_chat_template(self, *args, **kwargs):
with _borrow_from_pool() as tok:
return tok.apply_chat_template(*args, **kwargs)
def batch_decode(self, *args, **kwargs):
with _borrow_from_pool() as tok:
return tok.batch_decode(*args, **kwargs)
def batch_encode(self, *args, **kwargs):
with _borrow_from_pool() as tok:
return tok.batch_encode(*args, **kwargs)
def convert_tokens_to_ids(self, *args, **kwargs):
with _borrow_from_pool() as tok:
return tok.convert_tokens_to_ids(*args, **kwargs)
def convert_ids_to_tokens(self, *args, **kwargs):
with _borrow_from_pool() as tok:
return tok.convert_ids_to_tokens(*args, **kwargs)
def convert_tokens_to_string(self, *args, **kwargs):
with _borrow_from_pool() as tok:
return tok.convert_tokens_to_string(*args, **kwargs)
def decode(self, *args, **kwargs):
with _borrow_from_pool() as tok:
return tok.decode(*args, **kwargs)
def encode(self, *args, **kwargs):
with _borrow_from_pool() as tok:
return tok.encode(*args, **kwargs)
def __call__(self, *args, **kwargs):
with _borrow_from_pool() as tok:
return tok(*args, **kwargs)
def __reduce__(self):
return maybe_make_thread_pool, (og_tokenizer, copies)
TokenizerPool.__name__ = f"TokenizerPool{og_tokenizer.__class__.__name__}"
tokenizer.__class__ = TokenizerPool
def get_cached_tokenizer(tokenizer: HfTokenizer) -> HfTokenizer: