-
Notifications
You must be signed in to change notification settings - Fork 179
feat(grpc_servicer): implement GetTokenizer RPC for vLLM backend #1142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
abf3432
a443938
deb2b8d
f605936
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| """ | ||
| Shared tokenizer bundle utilities for gRPC servicers. | ||
|
|
||
| Builds and streams tokenizer artifacts as ZIP bundles over gRPC. | ||
| Used by both SGLang and vLLM servicers. | ||
| """ | ||
|
|
||
| import io | ||
| import zipfile | ||
| from pathlib import Path | ||
|
|
||
| # Streaming chunk size (aligned with Rust grpc_client limits) | ||
| CHUNK_SIZE = 64 * 1024 # 64 KB per gRPC chunk | ||
|
|
||
| # Files to include in the tokenizer ZIP bundle. | ||
| # Aligned with crates/tokenizer/src/hub.rs:is_tokenizer_file() plus model config files. | ||
| TOKENIZER_FILES = [ | ||
| "tokenizer.json", | ||
| "tokenizer_config.json", | ||
| "config.json", | ||
| "generation_config.json", | ||
| "special_tokens_map.json", | ||
| "vocab.json", | ||
| "merges.txt", | ||
| "tokenizer.model", # SentencePiece | ||
| "tiktoken.model", # tiktoken | ||
| "chat_template.json", | ||
| ] | ||
|
|
||
| # Glob patterns for additional tokenizer-related files | ||
| TOKENIZER_GLOBS = ["*.tiktoken", "*.jinja", "*.model"] | ||
|
|
||
|
|
||
| def build_tokenizer_zip(tokenizer_dir: Path) -> io.BytesIO: | ||
| """Create an in-memory ZIP archive of tokenizer files from a directory.""" | ||
| buf = io.BytesIO() | ||
| added: set[str] = set() | ||
| with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: | ||
| # Exact-name files | ||
| for name in TOKENIZER_FILES: | ||
| filepath = tokenizer_dir / name | ||
| if filepath.is_file(): | ||
| zf.write(filepath, name) | ||
| added.add(name) | ||
| # Glob patterns (*.tiktoken, *.jinja, *.model) | ||
| for pattern in TOKENIZER_GLOBS: | ||
| for match in tokenizer_dir.glob(pattern): | ||
| if match.is_file() and match.name not in added: | ||
| zf.write(match, match.name) | ||
| added.add(match.name) | ||
| if not added: | ||
| raise FileNotFoundError(f"No tokenizer files found in {tokenizer_dir}") | ||
| buf.seek(0) | ||
| return buf | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,13 +5,16 @@ | |||||||
| Implements the VllmEngine gRPC service on top of vLLM's EngineClient. | ||||||||
| """ | ||||||||
|
|
||||||||
| import hashlib | ||||||||
| import itertools | ||||||||
| import time | ||||||||
| from collections.abc import AsyncGenerator | ||||||||
| from collections.abc import AsyncGenerator, AsyncIterator | ||||||||
| from pathlib import Path | ||||||||
|
|
||||||||
| import grpc | ||||||||
| import torch | ||||||||
| from smg_grpc_proto import vllm_engine_pb2, vllm_engine_pb2_grpc | ||||||||
| from smg_grpc_proto.generated import common_pb2 | ||||||||
| from transformers import BatchFeature | ||||||||
| from vllm import PoolingParams, SamplingParams, TokensPrompt | ||||||||
| from vllm.engine.protocol import EngineClient | ||||||||
|
|
@@ -27,6 +30,8 @@ | |||||||
| from vllm.outputs import CompletionOutput, RequestOutput | ||||||||
| from vllm.sampling_params import RequestOutputKind, StructuredOutputsParams | ||||||||
|
|
||||||||
| from smg_grpc_servicer.tokenizer_bundle import CHUNK_SIZE, build_tokenizer_zip | ||||||||
|
|
||||||||
| logger = init_logger(__name__) | ||||||||
|
|
||||||||
| # Proto dtype string → torch dtype | ||||||||
|
|
@@ -49,13 +54,14 @@ class VllmEngineServicer(vllm_engine_pb2_grpc.VllmEngineServicer): | |||||||
| """ | ||||||||
| gRPC servicer implementing the VllmEngine service. | ||||||||
|
|
||||||||
| Handles 6 RPCs: | ||||||||
| Handles 7 RPCs: | ||||||||
| - Generate: Streaming text generation | ||||||||
| - Embed: Embeddings | ||||||||
| - HealthCheck: Health probe | ||||||||
| - Abort: Cancel requests out-of-band | ||||||||
| - GetModelInfo: Model metadata | ||||||||
| - GetServerInfo: Server state | ||||||||
| - GetTokenizer: Stream tokenizer artifacts | ||||||||
| """ | ||||||||
|
|
||||||||
| def __init__(self, async_llm: EngineClient, start_time: float): | ||||||||
|
|
@@ -358,6 +364,65 @@ async def GetServerInfo( | |||||||
| kv_role=kv_role, | ||||||||
| ) | ||||||||
|
|
||||||||
| async def GetTokenizer( | ||||||||
| self, | ||||||||
| request: common_pb2.GetTokenizerRequest, | ||||||||
| context: grpc.aio.ServicerContext, | ||||||||
| ) -> AsyncIterator[common_pb2.GetTokenizerChunk]: | ||||||||
| """Stream tokenizer artifacts as a ZIP bundle. | ||||||||
|
|
||||||||
| Resolves the tokenizer directory from model_config, zips all relevant | ||||||||
| tokenizer files, and streams them as GetTokenizerChunk messages. | ||||||||
| The final chunk carries the SHA-256 fingerprint of the full archive. | ||||||||
| """ | ||||||||
| logger.info("Receive GetTokenizer request") | ||||||||
|
|
||||||||
| tokenizer_path = self.engine.model_config.tokenizer | ||||||||
| if not tokenizer_path: | ||||||||
| await context.abort( | ||||||||
| grpc.StatusCode.FAILED_PRECONDITION, | ||||||||
| "Tokenizer path is not configured on this server.", | ||||||||
| ) | ||||||||
| tokenizer_dir = Path(tokenizer_path) | ||||||||
|
Comment on lines
+380
to
+386
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment is partially correct: However, this is handled gracefully:
The For production IGW/K8s, models are typically mounted from PVCs at local paths. The HF ID case is an edge case where GetTokenizer gracefully degrades. |
||||||||
|
|
||||||||
| # model_config.tokenizer may be an HF model ID (e.g. "meta-llama/...") | ||||||||
| # rather than a local path. Resolve it to the HF cache directory. | ||||||||
| if not tokenizer_dir.is_dir(): | ||||||||
| try: | ||||||||
| from huggingface_hub import snapshot_download | ||||||||
|
|
||||||||
| tokenizer_dir = Path(snapshot_download(tokenizer_path, local_files_only=True)) | ||||||||
| except Exception: | ||||||||
| pass # Fall through to build_tokenizer_zip which will raise | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Nit: Silently swallowing the A
Suggested change
|
||||||||
|
|
||||||||
| # Build ZIP archive in memory | ||||||||
| try: | ||||||||
| zip_buffer = build_tokenizer_zip(tokenizer_dir) | ||||||||
| except Exception as e: | ||||||||
| logger.exception("Failed to build tokenizer ZIP") | ||||||||
| await context.abort(grpc.StatusCode.INTERNAL, str(e)) | ||||||||
|
CatherineSue marked this conversation as resolved.
CatherineSue marked this conversation as resolved.
|
||||||||
|
|
||||||||
| zip_data = zip_buffer.getbuffer() | ||||||||
| sha256 = hashlib.sha256(zip_data).hexdigest() | ||||||||
|
|
||||||||
| logger.info( | ||||||||
| "Streaming tokenizer bundle: %d bytes, sha256=%s", | ||||||||
| len(zip_data), | ||||||||
| sha256, | ||||||||
| ) | ||||||||
|
|
||||||||
| # Stream chunks; SHA-256 only on the final chunk | ||||||||
| offset = 0 | ||||||||
| total = len(zip_data) | ||||||||
| while offset < total: | ||||||||
| end = min(offset + CHUNK_SIZE, total) | ||||||||
| is_last = end == total | ||||||||
| yield common_pb2.GetTokenizerChunk( | ||||||||
| data=bytes(zip_data[offset:end]), | ||||||||
| sha256=sha256 if is_last else "", | ||||||||
| ) | ||||||||
| offset = end | ||||||||
|
|
||||||||
| # ========== Helper methods ========== | ||||||||
|
|
||||||||
| def _build_preprocessed_mm_inputs( | ||||||||
|
|
||||||||
Uh oh!
There was an error while loading. Please reload this page.