| # |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| |
| # pytype: skip-file |
| |
| import asyncio |
| import logging |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import threading |
| import time |
| import urllib.request |
| import uuid |
| from collections.abc import Callable |
| from collections.abc import Iterable |
| from collections.abc import Sequence |
| from dataclasses import dataclass |
| from typing import Any |
| from typing import Optional |
| |
| from openai import AsyncOpenAI |
| from openai import OpenAI |
| |
| from apache_beam.io.filesystems import FileSystems |
| from apache_beam.ml.inference.base import ModelHandler |
| from apache_beam.ml.inference.base import PredictionResult |
| from apache_beam.utils import subprocess_server |
| |
| try: |
| # VLLM logging config breaks beam logging. |
| os.environ["VLLM_CONFIGURE_LOGGING"] = "0" |
| import vllm # pylint: disable=unused-import |
| logging.info('vllm module successfully imported.') |
| os.environ["VLLM_CONFIGURE_LOGGING"] = "1" |
| except ModuleNotFoundError: |
| msg = 'vllm module was not found. This is ok as long as the specified ' \ |
| 'runner has vllm dependencies installed.' |
| logging.warning(msg) |
| |
| __all__ = [ |
| 'OpenAIChatMessage', |
| 'VLLMCompletionsModelHandler', |
| 'VLLMChatModelHandler', |
| ] |
| |
| |
| @dataclass(frozen=True) |
| class OpenAIChatMessage(): |
| """" |
| Dataclass containing previous chat messages in conversation. |
| Role is the entity that sent the message (either 'user' or 'system'). |
| Content is the contents of the message. |
| """ |
| role: str |
| content: str |
| |
| |
| def start_process(cmd) -> tuple[subprocess.Popen, int]: |
| port, = subprocess_server.pick_port(None) |
| cmd = [arg.replace('{{PORT}}', str(port)) for arg in cmd] # pylint: disable=not-an-iterable |
| logging.info("Starting service with %s", str(cmd).replace("',", "'")) |
| process = subprocess.Popen( |
| cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| |
| # Emit the output of this command as info level logging. |
| def log_stdout(): |
| line = process.stdout.readline() |
| while line: |
| # The log obtained from stdout is bytes, decode it into string. |
| # Remove newline via rstrip() to not print an empty line. |
| logging.info(line.decode(errors='backslashreplace').rstrip()) |
| line = process.stdout.readline() |
| |
| t = threading.Thread(target=log_stdout) |
| t.daemon = True |
| t.start() |
| return process, port |
| |
| |
| def getVLLMClient(port) -> OpenAI: |
| openai_api_key = "EMPTY" |
| openai_api_base = f"http://localhost:{port}/v1" |
| return OpenAI( |
| api_key=openai_api_key, |
| base_url=openai_api_base, |
| ) |
| |
| |
| def getAsyncVLLMClient(port) -> AsyncOpenAI: |
| openai_api_key = "EMPTY" |
| openai_api_base = f"http://localhost:{port}/v1" |
| return AsyncOpenAI( |
| api_key=openai_api_key, |
| base_url=openai_api_base, |
| ) |
| |
| |
| # Embedded Dynamo runtime defaults proven on the smoke test: etcd discovery, |
| # TCP request plane, ZMQ event plane, KV events disabled. KV-aware routing, |
| # disaggregated prefill/decode, and the Planner are not active in this mode. |
| _DYNAMO_FRONTEND_DEFAULT_KWARGS: dict[str, Optional[str]] = { |
| 'discovery-backend': 'etcd', |
| 'request-plane': 'tcp', |
| 'event-plane': 'zmq', |
| 'router-mode': 'round-robin', |
| 'no-router-kv-events': None, |
| } |
| |
| _DYNAMO_ENGINE_DEFAULT_KWARGS: dict[str, Optional[str]] = { |
| 'discovery-backend': 'etcd', |
| 'request-plane': 'tcp', |
| 'event-plane': 'zmq', |
| 'kv-events-config': '{"enable_kv_cache_events": false}', |
| } |
| |
| |
| def _append_kwargs(cmd: list[str], kwargs: dict[str, Optional[str]]) -> None: |
| for k, v in kwargs.items(): |
| cmd.append(f'--{k}') |
| # Only add values for commands with value part. |
| if v is not None: |
| cmd.append(v) |
| |
| |
| def _uses_etcd_discovery(kwargs: dict[str, Optional[str]]) -> bool: |
| return kwargs.get('discovery-backend') == 'etcd' |
| |
| |
| class _VLLMModelServer(): |
| def __init__( |
| self, |
| model_name: str, |
| vllm_server_kwargs: dict[str, Optional[str]], |
| dynamo_frontend_kwargs: Optional[dict[str, Optional[str]]] = None, |
| use_dynamo: bool = False): |
| self._model_name = model_name |
| self._vllm_server_kwargs = vllm_server_kwargs |
| self._dynamo_frontend_kwargs = dynamo_frontend_kwargs or {} |
| self._server_started = False |
| self._server_process = None |
| self._dynamo_process = None |
| self._etcd_process = None |
| self._etcd_data_dir: Optional[str] = None |
| self._managed_etcd_endpoint = None |
| self._server_port: int = -1 |
| self._server_process_lock = threading.RLock() |
| self._use_dynamo = use_dynamo |
| |
| self.start_server() |
| |
| @staticmethod |
| def _stop_process(process: Optional[subprocess.Popen]) -> None: |
| if process is None or process.poll() is not None: |
| return |
| # A process may exit between poll() and terminate() / kill(), in which |
| # case the OS raises ProcessLookupError (or another OSError). Treat that |
| # as already-stopped so we don't bail out of the broader cleanup. |
| try: |
| process.terminate() |
| try: |
| process.wait(timeout=10) |
| except subprocess.TimeoutExpired: |
| process.kill() |
| process.wait() |
| except OSError: |
| pass |
| |
| def _stop_processes(self) -> None: |
| self._stop_process(self._dynamo_process) |
| self._stop_process(self._server_process) |
| self._stop_process(self._etcd_process) |
| if (self._managed_etcd_endpoint is not None and |
| os.environ.get('ETCD_ENDPOINTS') == self._managed_etcd_endpoint): |
| os.environ.pop('ETCD_ENDPOINTS', None) |
| if self._etcd_data_dir is not None: |
| shutil.rmtree(self._etcd_data_dir, ignore_errors=True) |
| self._etcd_data_dir = None |
| self._dynamo_process = None |
| self._server_process = None |
| self._etcd_process = None |
| self._managed_etcd_endpoint = None |
| self._server_started = False |
| self._server_port = -1 |
| |
| def _process_status(self) -> str: |
| process_status = [] |
| if self._server_process is not None: |
| process_status.append( |
| 'frontend/server exit code: %s' % self._server_process.poll()) |
| if self._dynamo_process is not None: |
| process_status.append( |
| 'dynamo worker exit code: %s' % self._dynamo_process.poll()) |
| if self._etcd_process is not None: |
| process_status.append('etcd exit code: %s' % self._etcd_process.poll()) |
| return ', '.join(process_status) or 'no process status available' |
| |
| def __del__(self): |
| # __del__ may run during interpreter shutdown when module globals can |
| # already be torn down; swallow any cleanup failures so we don't print |
| # a noisy traceback. |
| try: |
| self._stop_processes() |
| except Exception: # pylint: disable=broad-except |
| pass |
| |
| def _uses_embedded_etcd(self) -> bool: |
| return ( |
| self._use_dynamo and |
| _uses_etcd_discovery(self._dynamo_frontend_kwargs) and |
| _uses_etcd_discovery(self._vllm_server_kwargs) and |
| 'ETCD_ENDPOINTS' not in os.environ) |
| |
| def _wait_for_etcd(self, endpoint: str, timeout_secs=30) -> None: |
| deadline = time.time() + timeout_secs |
| health_url = endpoint.rstrip('/') + '/health' |
| while time.time() < deadline and self._etcd_process.poll() is None: |
| try: |
| with urllib.request.urlopen(health_url, timeout=2) as response: |
| if response.status < 500: |
| return |
| except Exception: # pylint: disable=broad-except |
| time.sleep(1) |
| |
| process_status = self._process_status() |
| self._stop_processes() |
| raise RuntimeError( |
| "Failed to start embedded etcd for Dynamo. Process status: " |
| f"{process_status}. Install etcd in the worker container or set " |
| "ETCD_ENDPOINTS to an external etcd service.") |
| |
| def _ensure_etcd(self) -> None: |
| if not self._uses_embedded_etcd(): |
| return |
| if shutil.which('etcd') is None: |
| raise RuntimeError( |
| "Embedded Dynamo mode requires etcd when ETCD_ENDPOINTS is not " |
| "set. Install etcd in the worker container or set ETCD_ENDPOINTS " |
| "to an external etcd service.") |
| |
| etcd_name = f'beam-dynamo-etcd-{uuid.uuid4().hex}' |
| self._etcd_data_dir = f'/tmp/{etcd_name}' |
| peer_port, = subprocess_server.pick_port(None) |
| etcd_cmd = [ |
| 'etcd', |
| '--name', |
| etcd_name, |
| '--listen-client-urls', |
| 'http://127.0.0.1:{{PORT}}', |
| '--advertise-client-urls', |
| 'http://127.0.0.1:{{PORT}}', |
| '--listen-peer-urls', |
| f'http://127.0.0.1:{peer_port}', |
| '--initial-advertise-peer-urls', |
| f'http://127.0.0.1:{peer_port}', |
| '--initial-cluster', |
| f'{etcd_name}=http://127.0.0.1:{peer_port}', |
| '--data-dir', |
| self._etcd_data_dir, |
| '--log-level', |
| 'warn', |
| ] |
| self._etcd_process, etcd_port = start_process(etcd_cmd) |
| endpoint = f'http://127.0.0.1:{etcd_port}' |
| os.environ['ETCD_ENDPOINTS'] = endpoint |
| self._managed_etcd_endpoint = endpoint |
| self._wait_for_etcd(endpoint) |
| |
| def start_server(self, retries=3): |
| with self._server_process_lock: |
| if not self._server_started: |
| self._stop_processes() |
| self._ensure_etcd() |
| if self._use_dynamo: |
| # Dynamo embedded mode uses the frontend as its OpenAI-compatible |
| # local endpoint and a separate vLLM worker process. |
| server_cmd = [ |
| sys.executable, |
| '-m', |
| 'dynamo.frontend', |
| '--http-port', |
| '{{PORT}}', |
| ] |
| _append_kwargs(server_cmd, self._dynamo_frontend_kwargs) |
| else: |
| server_cmd = [ |
| sys.executable, |
| '-m', |
| 'vllm.entrypoints.openai.api_server', |
| '--model', |
| self._model_name, |
| '--port', |
| '{{PORT}}', |
| ] |
| _append_kwargs(server_cmd, self._vllm_server_kwargs) |
| self._server_process, self._server_port = start_process(server_cmd) |
| |
| if self._use_dynamo: |
| server_cmd = [ |
| sys.executable, |
| '-m', |
| 'dynamo.vllm', |
| '--model', |
| self._model_name, |
| ] |
| _append_kwargs(server_cmd, self._vllm_server_kwargs) |
| self._dynamo_process, _ = start_process(server_cmd) |
| |
| self.check_connectivity(retries) |
| |
| def get_server_port(self) -> int: |
| if not self._server_started: |
| self.start_server() |
| return self._server_port |
| |
| def check_connectivity(self, retries=3, timeout_secs=600): |
| start_time = time.time() |
| with getVLLMClient(self._server_port) as client: |
| while (time.time() - start_time < timeout_secs and |
| self._server_process.poll() is None and |
| (self._dynamo_process is None or |
| self._dynamo_process.poll() is None) and |
| (self._etcd_process is None or self._etcd_process.poll() is None)): |
| try: |
| models = client.models.list().data |
| logging.info('models: %s' % models) |
| if len(models) > 0: |
| self._server_started = True |
| return |
| except: # pylint: disable=bare-except |
| pass |
| # Sleep while bringing up the process |
| time.sleep(5) |
| |
| process_status = self._process_status() |
| self._stop_processes() |
| if retries == 0: |
| raise Exception( |
| "Failed to start vLLM server. Process status: " |
| f"{process_status}. Next time a request is tried, the server " |
| "will be restarted") |
| else: |
| self.start_server(retries - 1) |
| |
| |
| class VLLMCompletionsModelHandler(ModelHandler[str, |
| PredictionResult, |
| _VLLMModelServer]): |
| def __init__( |
| self, |
| model_name: str, |
| vllm_server_kwargs: Optional[dict[str, Optional[str]]] = None, |
| *, |
| use_dynamo: bool = False, |
| dynamo_frontend_kwargs: Optional[dict[str, Optional[str]]] = None, |
| min_batch_size: Optional[int] = None, |
| max_batch_size: Optional[int] = None, |
| max_batch_duration_secs: Optional[int] = None, |
| max_batch_weight: Optional[int] = None, |
| element_size_fn: Optional[Callable[[Any], int]] = None, |
| batch_length_fn: Optional[Callable[[Any], int]] = None, |
| batch_bucket_boundaries: Optional[list[int]] = None): |
| """Implementation of the ModelHandler interface for vLLM using text as |
| input. |
| |
| Example Usage:: |
| |
| pcoll | RunInference(VLLMModelHandler(model_name='facebook/opt-125m')) |
| |
| Args: |
| model_name: The vLLM model. See |
| https://docs.vllm.ai/en/latest/models/supported_models.html for |
| supported models. |
| vllm_server_kwargs: Any additional kwargs to be passed into your vllm |
| server when it is being created. When ``use_dynamo`` is disabled, |
| this is invoked using ``python -m vllm.entrypoints.openai.api_server |
| <beam provided args> <vllm_server_kwargs>``. When ``use_dynamo`` is |
| enabled, these kwargs are passed to the ``dynamo.vllm`` worker |
| process. For example, you could pass ``{'echo': 'true'}`` to prepend |
| new messages with the previous message. On ~16GB GPUs, pass lower |
| ``max-num-seqs`` and ``gpu-memory-utilization`` values (see |
| ``apache_beam.examples.inference.vllm_text_completion``). For a list |
| of possible kwargs, see |
| https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html#extra-parameters-for-completions-api |
| use_dynamo: Whether to use NVIDIA Dynamo as the underlying vLLM engine. |
| Requires installing Dynamo in your runtime environment |
| (``pip install ai-dynamo[vllm]``). This is an opt-in single-worker |
| embedded mode; KV-aware routing, disaggregated prefill/decode, KVBM |
| offload across nodes, the Planner, and Grove are not active in |
| embedded mode. Dynamo also requires an etcd-style discovery service: |
| when ``ETCD_ENDPOINTS`` is unset, Beam starts a local etcd, which |
| requires the ``etcd`` binary in the worker environment. |
| dynamo_frontend_kwargs: Additional kwargs to be passed to the |
| ``dynamo.frontend`` process when ``use_dynamo`` is enabled. By |
| default, embedded Dynamo uses etcd discovery, TCP request plane, ZMQ |
| event plane, round-robin routing, and disables router KV events. |
| min_batch_size: optional. the minimum batch size to use when batching |
| inputs. |
| max_batch_size: optional. the maximum batch size to use when batching |
| inputs. |
| max_batch_duration_secs: optional. the maximum amount of time to buffer |
| a batch before emitting; used in streaming contexts. |
| max_batch_weight: optional. the maximum total weight of a batch. |
| element_size_fn: optional. a function that returns the size (weight) of |
| an element. |
| batch_length_fn: optional. a callable that returns the length of an |
| element for length-aware batching. |
| batch_bucket_boundaries: optional. a sorted list of positive boundary |
| values for length-aware batching buckets. |
| """ |
| super().__init__( |
| min_batch_size=min_batch_size, |
| max_batch_size=max_batch_size, |
| max_batch_duration_secs=max_batch_duration_secs, |
| max_batch_weight=max_batch_weight, |
| element_size_fn=element_size_fn, |
| batch_length_fn=batch_length_fn, |
| batch_bucket_boundaries=batch_bucket_boundaries) |
| self._model_name = model_name |
| self._vllm_server_kwargs: dict[str, Optional[str]] = ({ |
| **_DYNAMO_ENGINE_DEFAULT_KWARGS, **(vllm_server_kwargs or {}) |
| } if use_dynamo else vllm_server_kwargs or {}) |
| self._dynamo_frontend_kwargs: dict[str, Optional[str]] = { |
| **_DYNAMO_FRONTEND_DEFAULT_KWARGS, **(dynamo_frontend_kwargs or {}) |
| } |
| self._use_dynamo = use_dynamo |
| |
| def load_model(self) -> _VLLMModelServer: |
| return _VLLMModelServer( |
| self._model_name, |
| self._vllm_server_kwargs, |
| self._dynamo_frontend_kwargs, |
| self._use_dynamo) |
| |
| async def _async_run_inference( |
| self, |
| batch: Sequence[str], |
| model: _VLLMModelServer, |
| inference_args: Optional[dict[str, Any]] = None |
| ) -> Iterable[PredictionResult]: |
| inference_args = inference_args or {} |
| |
| async with getAsyncVLLMClient(model.get_server_port()) as client: |
| try: |
| async_predictions = [ |
| client.completions.create( |
| model=self._model_name, prompt=prompt, **inference_args) |
| for prompt in batch |
| ] |
| responses = await asyncio.gather(*async_predictions) |
| except Exception as e: |
| model.check_connectivity() |
| raise e |
| |
| return [PredictionResult(x, y) for x, y in zip(batch, responses)] |
| |
| def run_inference( |
| self, |
| batch: Sequence[str], |
| model: _VLLMModelServer, |
| inference_args: Optional[dict[str, Any]] = None |
| ) -> Iterable[PredictionResult]: |
| """Runs inferences on a batch of text strings. |
| |
| Args: |
| batch: A sequence of examples as text strings. |
| model: A _VLLMModelServer containing info for connecting to the server. |
| inference_args: Any additional arguments for an inference. |
| |
| Returns: |
| An Iterable of type PredictionResult. |
| """ |
| return asyncio.run(self._async_run_inference(batch, model, inference_args)) |
| |
| def validate_inference_args(self, inference_args: Optional[dict[str, Any]]): |
| # Override the base validator so OpenAI-compatible request kwargs such as |
| # ``max_tokens`` can be passed through ``RunInference`` to the vLLM / |
| # Dynamo server. |
| pass |
| |
| def share_model_across_processes(self) -> bool: |
| return True |
| |
| |
| class VLLMChatModelHandler(ModelHandler[Sequence[OpenAIChatMessage], |
| PredictionResult, |
| _VLLMModelServer]): |
| def __init__( |
| self, |
| model_name: str, |
| chat_template_path: Optional[str] = None, |
| vllm_server_kwargs: Optional[dict[str, Optional[str]]] = None, |
| *, |
| use_dynamo: bool = False, |
| dynamo_frontend_kwargs: Optional[dict[str, Optional[str]]] = None, |
| min_batch_size: Optional[int] = None, |
| max_batch_size: Optional[int] = None, |
| max_batch_duration_secs: Optional[int] = None, |
| max_batch_weight: Optional[int] = None, |
| element_size_fn: Optional[Callable[[Any], int]] = None, |
| batch_length_fn: Optional[Callable[[Any], int]] = None, |
| batch_bucket_boundaries: Optional[list[int]] = None): |
| """ Implementation of the ModelHandler interface for vLLM using previous |
| messages as input. |
| |
| Example Usage:: |
| |
| pcoll | RunInference(VLLMModelHandler(model_name='facebook/opt-125m')) |
| |
| Args: |
| model_name: The vLLM model. See |
| https://docs.vllm.ai/en/latest/models/supported_models.html for |
| supported models. |
| chat_template_path: Path to a chat template. This file must be accessible |
| from your runner's execution environment, so it is recommended to use |
| a cloud based file storage system (e.g. Google Cloud Storage). |
| For info on chat templates, see: |
| https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html#chat-template |
| vllm_server_kwargs: Any additional kwargs to be passed into your vllm |
| server when it is being created. When ``use_dynamo`` is disabled, |
| this is invoked using ``python -m vllm.entrypoints.openai.api_server |
| <beam provided args> <vllm_server_kwargs>``. When ``use_dynamo`` is |
| enabled, these kwargs are passed to the ``dynamo.vllm`` worker |
| process. For example, you could pass ``{'echo': 'true'}`` to prepend |
| new messages with the previous message. For a list of possible |
| kwargs, see |
| https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html#extra-parameters-for-chat-api |
| use_dynamo: Whether to use NVIDIA Dynamo as the underlying vLLM engine. |
| Requires installing Dynamo in your runtime environment |
| (``pip install ai-dynamo[vllm]``). This is an opt-in single-worker |
| embedded mode; KV-aware routing, disaggregated prefill/decode, KVBM |
| offload across nodes, the Planner, and Grove are not active in |
| embedded mode. Dynamo also requires an etcd-style discovery service: |
| when ``ETCD_ENDPOINTS`` is unset, Beam starts a local etcd, which |
| requires the ``etcd`` binary in the worker environment. |
| dynamo_frontend_kwargs: Additional kwargs to be passed to the |
| ``dynamo.frontend`` process when ``use_dynamo`` is enabled. By |
| default, embedded Dynamo uses etcd discovery, TCP request plane, ZMQ |
| event plane, round-robin routing, and disables router KV events. |
| min_batch_size: optional. the minimum batch size to use when batching |
| inputs. |
| max_batch_size: optional. the maximum batch size to use when batching |
| inputs. |
| max_batch_duration_secs: optional. the maximum amount of time to buffer |
| a batch before emitting; used in streaming contexts. |
| max_batch_weight: optional. the maximum total weight of a batch. |
| element_size_fn: optional. a function that returns the size (weight) of |
| an element. |
| batch_length_fn: optional. a callable that returns the length of an |
| element for length-aware batching. |
| batch_bucket_boundaries: optional. a sorted list of positive boundary |
| values for length-aware batching buckets. |
| """ |
| super().__init__( |
| min_batch_size=min_batch_size, |
| max_batch_size=max_batch_size, |
| max_batch_duration_secs=max_batch_duration_secs, |
| max_batch_weight=max_batch_weight, |
| element_size_fn=element_size_fn, |
| batch_length_fn=batch_length_fn, |
| batch_bucket_boundaries=batch_bucket_boundaries) |
| self._model_name = model_name |
| self._vllm_server_kwargs: dict[str, Optional[str]] = ({ |
| **_DYNAMO_ENGINE_DEFAULT_KWARGS, **(vllm_server_kwargs or {}) |
| } if use_dynamo else vllm_server_kwargs or {}) |
| self._dynamo_frontend_kwargs: dict[str, Optional[str]] = { |
| **_DYNAMO_FRONTEND_DEFAULT_KWARGS, **(dynamo_frontend_kwargs or {}) |
| } |
| self._chat_template_path = chat_template_path |
| self._chat_file = f'template-{uuid.uuid4().hex}.jinja' |
| self._use_dynamo = use_dynamo |
| |
| def load_model(self) -> _VLLMModelServer: |
| chat_template_contents = '' |
| if self._chat_template_path is not None: |
| local_chat_template_path = os.path.join(os.getcwd(), self._chat_file) |
| if not os.path.exists(local_chat_template_path): |
| with FileSystems.open(self._chat_template_path) as fin: |
| chat_template_contents = fin.read().decode() |
| with open(local_chat_template_path, 'a') as f: |
| f.write(chat_template_contents) |
| self._vllm_server_kwargs['chat_template'] = local_chat_template_path |
| |
| return _VLLMModelServer( |
| self._model_name, |
| self._vllm_server_kwargs, |
| self._dynamo_frontend_kwargs, |
| self._use_dynamo) |
| |
| async def _async_run_inference( |
| self, |
| batch: Sequence[Sequence[OpenAIChatMessage]], |
| model: _VLLMModelServer, |
| inference_args: Optional[dict[str, Any]] = None |
| ) -> Iterable[PredictionResult]: |
| inference_args = inference_args or {} |
| |
| async with getAsyncVLLMClient(model.get_server_port()) as client: |
| try: |
| async_predictions = [ |
| client.chat.completions.create( |
| model=self._model_name, |
| messages=[{ |
| "role": message.role, "content": message.content |
| } for message in messages], |
| **inference_args) for messages in batch |
| ] |
| predictions = await asyncio.gather(*async_predictions) |
| except Exception as e: |
| model.check_connectivity() |
| raise e |
| |
| return [PredictionResult(x, y) for x, y in zip(batch, predictions)] |
| |
| def run_inference( |
| self, |
| batch: Sequence[Sequence[OpenAIChatMessage]], |
| model: _VLLMModelServer, |
| inference_args: Optional[dict[str, Any]] = None |
| ) -> Iterable[PredictionResult]: |
| """Runs inferences on a batch of text strings. |
| |
| Args: |
| batch: A sequence of examples as OpenAI messages. |
| model: A _VLLMModelServer for connecting to the spun up server. |
| inference_args: Any additional arguments for an inference. |
| |
| Returns: |
| An Iterable of type PredictionResult. |
| """ |
| return asyncio.run(self._async_run_inference(batch, model, inference_args)) |
| |
| def validate_inference_args(self, inference_args: Optional[dict[str, Any]]): |
| # Override the base validator so OpenAI-compatible request kwargs such as |
| # ``max_tokens`` can be passed through ``RunInference`` to the vLLM / |
| # Dynamo server. |
| pass |
| |
| def share_model_across_processes(self) -> bool: |
| return True |