11from typing import Any , Dict , List , Optional , Union
22
3+ import jinja2
4+ from vllm .entrypoints .chat_utils import load_chat_template
5+ from vllm .entrypoints .openai .cli_args import FrontendArgs
6+ from vllm .renderers import renderer_from_config
7+ from vllm .renderers .inputs .preprocess import extract_prompt_components
8+ from vllm .renderers .online_renderer import OnlineRenderer
9+
10+ from ray .llm ._internal .serve .core .configs .llm_config import LLMConfig
311from ray .llm ._internal .serve .core .configs .openai_api_models import (
4- ErrorResponse ,
512 TokenizeChatRequest ,
613 TokenizeCompletionRequest ,
714)
15+ from ray .llm ._internal .serve .engines .vllm .vllm_engine import (
16+ _get_vllm_engine_config ,
17+ )
818from ray .llm ._internal .serve .observability .logging import get_logger
9- from ray .serve .handle import DeploymentHandle
1019
1120logger = get_logger (__name__ )
1221
13- # choose_replica kwarg carrying the prompt token IDs to KV-aware routers.
14- REQUEST_TOKEN_IDS_KWARG = "request_token_ids"
15-
1622
1723class TokenizeError (Exception ):
18- """The ``/tokenize`` endpoint rejected the request.
24+ """The request was rejected the same way vLLM's native ASGI route
25+ ``/tokenize`` would reject it.
1926
20- Carries vLLM's HTTP ``status_code``, ``message`` and error ``type``.
27+ Carries the HTTP ``status_code``, ``message`` and error ``type``.
2128 """
2229
2330 def __init__ (self , message : str , * , status_code : int , type : str ):
@@ -27,15 +34,77 @@ def __init__(self, message: str, *, status_code: int, type: str):
2734 self .type = type
2835
2936
37+ def build_tokenize_request (
38+ payload : Dict [str , Any ]
39+ ) -> Optional [Union [TokenizeChatRequest , TokenizeCompletionRequest ]]:
40+ """Build the Tokenize* request for ``payload``, forwarding only the fields
41+ the engine renders the prompt from so routing ids match the prefill tokens.
42+
43+ Returns ``None`` (caller falls back to token-less routing) for a body with
44+ no single string prompt, e.g. a batch ``prompt`` list, since KV-aware
45+ routing scores one request on one token sequence.
46+
47+ TODO (jeffreywang): Support multi-prompt tokenization.
48+ """
49+ try :
50+ if "messages" in payload :
51+ return TokenizeChatRequest .model_validate (
52+ {
53+ k : v
54+ for k , v in payload .items ()
55+ if k in TokenizeChatRequest .model_fields
56+ }
57+ )
58+ if "prompt" in payload :
59+ if not isinstance (payload ["prompt" ], str ):
60+ return None
61+ return TokenizeCompletionRequest .model_validate (
62+ {
63+ k : v
64+ for k , v in payload .items ()
65+ if k in TokenizeCompletionRequest .model_fields
66+ }
67+ )
68+ # Unreachable: LLMRouter only routes bodies with messages or a prompt.
69+ logger .warning (
70+ "Tokenizer got a payload with neither messages nor prompt; "
71+ "falling back to token-less routing."
72+ )
73+ return None
74+ except Exception as e :
75+ logger .warning ("Unsupported tokenize request, falling back: %s" , e )
76+ return None
77+
78+
3079class Tokenizer :
31- """Tokenizes incoming requests via the replica's ``/tokenize`` endpoint.
80+ """Tokenizes requests with vLLM's ``OnlineRenderer``.
81+
82+ Configured from the deployment's frontend args so the tokenizer, chat
83+ template, and trust policy match the engine's.
3284
3385 Args:
34- handle: A handle to the LLMServer deployment .
86+ llm_config: The deployment's LLM config .
3587 """
3688
37- def __init__ (self , handle : DeploymentHandle ):
38- self ._handle = handle
89+ def __init__ (self , llm_config : LLMConfig ):
90+ engine_config = llm_config .get_engine_config ()
91+ _ , vllm_config = _get_vllm_engine_config (llm_config )
92+ self ._model_config = vllm_config .model_config
93+
94+ frontend_args = FrontendArgs (** engine_config .frontend_kwargs )
95+ self ._renderer = OnlineRenderer (
96+ self ._model_config ,
97+ renderer_from_config (vllm_config ),
98+ request_logger = None ,
99+ chat_template = load_chat_template (frontend_args .chat_template ),
100+ chat_template_content_format = frontend_args .chat_template_content_format ,
101+ trust_request_chat_template = frontend_args .trust_request_chat_template ,
102+ default_chat_template_kwargs = frontend_args .default_chat_template_kwargs ,
103+ )
104+ logger .info (
105+ "In-process pre-routing tokenizer ready for %s" ,
106+ self ._model_config .model ,
107+ )
39108
40109 async def tokenize (self , payload : Dict [str , Any ]) -> Optional [List [int ]]:
41110 """Tokenize a request ``payload`` into prompt token IDs.
@@ -49,76 +118,60 @@ async def tokenize(self, payload: Dict[str, Any]) -> Optional[List[int]]:
49118 Raises:
50119 TokenizeError: The ``/tokenize`` endpoint rejected the request.
51120 """
52- tok_req = self . _build_tokenize_request (payload )
53- if tok_req is None :
121+ request = build_tokenize_request (payload )
122+ if request is None :
54123 return None
55124
56- # /tokenize yields a single response; drain the stream fully so the
57- # handle response is cleaned up.
58- resp = None
59- async for chunk in self ._handle .options (stream = True ).tokenize .remote (
60- tok_req , None
61- ):
62- resp = chunk
63- if resp is None :
64- raise TokenizeError (
65- "/tokenize returned no response" ,
66- status_code = 500 ,
67- type = "internal_error" ,
68- )
69- if isinstance (resp , ErrorResponse ):
70- raise TokenizeError (
71- resp .error .message ,
72- status_code = resp .error .code ,
73- type = resp .error .type ,
74- )
75- return list (resp .tokens )
76-
77- def _build_tokenize_request (
78- self , payload : Dict [str , Any ]
79- ) -> Optional [Union [TokenizeChatRequest , TokenizeCompletionRequest ]]:
80- """Build the Tokenize* request for ``payload``.
81-
82- KV-aware routing sends each request to one replica, scored on a single
83- prompt's token sequence, so we return ``None`` (the caller falls back to
84- token-less routing) for bodies that don't have exactly one prompt:
85- - A non-string ``prompt``: an OpenAI *batch* completion where ``prompt``
86- is a list, e.g. ``{"prompt": ["q1", "q2"]}`` (or pre-tokenized id
87- lists). N prompts give N token sequences, so there's no single key to
88- route the one request on.
89-
90- TODO (jeffreywang): Support multi-prompt tokenization.
91- """
92125 try :
93- if "messages" in payload :
94- # Forward every request field the engine renders the prompt from
95- # so the routing token IDs match the prefill tokens.
96- return TokenizeChatRequest .model_validate (
97- {
98- k : v
99- for k , v in payload .items ()
100- if k in TokenizeChatRequest .model_fields
101- }
126+ if isinstance (request , TokenizeChatRequest ):
127+ engine_inputs = await self ._render_chat (request )
128+ else :
129+ engine_inputs = await self ._renderer .preprocess_completion (
130+ request ,
131+ prompt_input = request .prompt ,
132+ prompt_embeds = None ,
133+ skip_mm_cache = True ,
102134 )
103- if "prompt" in payload :
104- if not isinstance (payload ["prompt" ], str ):
105- # TODO (jeffreywang): Multi-prompt (list) tokenization is unsupported;
106- # fall back to token-less routing.
107- return None
108- return TokenizeCompletionRequest .model_validate (
109- {
110- k : v
111- for k , v in payload .items ()
112- if k in TokenizeCompletionRequest .model_fields
113- }
114- )
115- # Should be unreachable: LLMRouter only routes bodies with messages
116- # or a prompt (see _parse_routing_payload).
117- logger .warning (
118- "Tokenizer got a payload with neither messages nor prompt; "
119- "falling back to token-less routing."
135+ except TokenizeError :
136+ raise
137+ except (ValueError , jinja2 .TemplateError ) as e :
138+ # /tokenize maps bad inputs and chat-template errors to 400; other
139+ # exceptions are real bugs and should surface, not degrade routing.
140+ raise TokenizeError (str (e ), status_code = 400 , type = "BadRequestError" )
141+
142+ input_ids : List [int ] = []
143+ for engine_input in engine_inputs :
144+ components = extract_prompt_components (self ._model_config , engine_input )
145+ if components .token_ids is not None :
146+ input_ids .extend (components .token_ids )
147+ return input_ids
148+
149+ async def _render_chat (self , request : TokenizeChatRequest ):
150+ # Refuse a request-supplied chat template unless the deployment opted in.
151+ error = self ._renderer .validate_chat_template (
152+ request_chat_template = request .chat_template ,
153+ chat_template_kwargs = request .chat_template_kwargs ,
154+ trust_request_chat_template = self ._renderer .trust_request_chat_template ,
155+ )
156+ if error is not None :
157+ raise TokenizeError (
158+ error .error .message ,
159+ status_code = error .error .code ,
160+ type = error .error .type ,
120161 )
121- return None
122- except Exception as e :
123- logger .debug ("Unsupported tokenize request, falling back: %s" , e )
124- return None
162+
163+ tool_dicts = (
164+ None
165+ if request .tools is None
166+ else [tool .model_dump () for tool in request .tools ]
167+ )
168+ _ , engine_inputs = await self ._renderer .preprocess_chat (
169+ request ,
170+ request .messages ,
171+ default_template = self ._renderer .chat_template ,
172+ default_template_content_format = self ._renderer .chat_template_content_format ,
173+ default_template_kwargs = self ._renderer .default_chat_template_kwargs ,
174+ tool_dicts = tool_dicts ,
175+ skip_mm_cache = True ,
176+ )
177+ return engine_inputs
0 commit comments